diff --git a/.gitignore b/.gitignore
index ae5f085df619e4faf11263c8af42121396ec16d9..cddf03602a35709c94e0d191f9fe37af69e5757e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,6 @@
 .gradle/
 .idea/
 build/dependencyUpdates/
+venv/
+*/out/
+logs/
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 7e77a76053345e34f70811b6498a8758dbf7d305..918f65ab8164ff97f01d7e37dc1fa86b7bfd3766 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -4,15 +4,23 @@ stages:
 - report
 
 variables:
+  # Instruct Testcontainers to use the daemon of DinD.
+  DOCKER_HOST: "unix:///var/run/docker.sock"
+#  # Improve performance with overlayfs.
+#  DOCKER_DRIVER: overlay2
   GRADLE_OPTS: "-Dorg.gradle.daemon=false"
   TEST_REPORTS: "/builds/OpenLicht/eraser/eraser-base/build/reports/tests/test/"
+  TEST_LOG: "/builds/OpenLicht/eraser/eraser-base/logs/eraser-test.log"
   JACOCO_REPORT: "/builds/OpenLicht/eraser/eraser-base/build/reports/jacoco/test/jacocoTestReport.xml"
+  TESTCONTAINERS_RYUK_DISABLED: "true"
 
 before_script:
   - export GRADLE_USER_HOME=`pwd`/.gradle
 
 build:
-  image: gradle:jdk8
+  image: openjdk:8
+  tags:
+    - docker
   stage: build
   script:
     - ./gradlew --console=plain --build-cache assemble
@@ -24,7 +32,9 @@ build:
       - .gradle
 
 test:
-  image: gradle:jdk8
+  image: openjdk:8
+  tags:
+    - docker
   stage: test
   script:
     - ./gradlew --continue --console=plain check jacocoTestReport
@@ -37,11 +47,14 @@ test:
   artifacts:
     when: always
     paths:
+      - $TEST_LOG
       - $TEST_REPORTS
       - $JACOCO_REPORT
 
 coverage:
   image: python:3.7.1-alpine
+  tags:
+    - docker
   stage: report
   dependencies:
   - test
diff --git a/.gitmodules b/.gitmodules
new file mode 100644
index 0000000000000000000000000000000000000000..e77282f413a7cf57a9ad0b61b13d7397531c013f
--- /dev/null
+++ b/.gitmodules
@@ -0,0 +1,3 @@
+[submodule "ragdoc-view"]
+	path = ragdoc-view
+	url = ../../jastadd/ragdoc-view/
diff --git a/README.md b/README.md
index 547af86940b786d79ef41894c0c19ba03bcd636a..ce1aa70bdc28d9403a709ccd0bfa1379a2903265 100644
--- a/README.md
+++ b/README.md
@@ -22,10 +22,7 @@ There are a few subprojects in this repository:
 
 ## Setup and Building
 
-This project uses Gradle as the build tool. Please use the provided Gradle wrappers instead of an installed one.
-
-- Clone the repository, either using [https](https://git-st.inf.tu-dresden.de/OpenLicht/eraser.git) or [ssh](git@git-st.inf.tu-dresden.de:OpenLicht/eraser.git)
-- (Optional) Build all subprojects: `./gradlew build`
+This project uses Gradle as the build tool. For detailed information, see [setup guidelines](/../wikis/setup)
 
 ## Running the demo
 
diff --git a/benchmark/.gitignore b/benchmark/.gitignore
new file mode 100644
index 0000000000000000000000000000000000000000..54c5c0b024dab04da2a43d4135ed67ed2a670b46
--- /dev/null
+++ b/benchmark/.gitignore
@@ -0,0 +1,2 @@
+/build/
+logs/
diff --git a/benchmark/build.gradle b/benchmark/build.gradle
new file mode 100644
index 0000000000000000000000000000000000000000..dfcc1d42edbaf81885917bd8b31573056ba29dbb
--- /dev/null
+++ b/benchmark/build.gradle
@@ -0,0 +1,36 @@
+apply plugin: 'application'
+
+dependencies {
+    compile project(':eraser-base')
+    compile project(':feedbackloop.learner_backup')
+    compile project(':datasets')
+    testImplementation 'junit:junit:4.12'
+    // https://mvnrepository.com/artifact/org.hamcrest/java-hamcrest
+    // https://mvnrepository.com/artifact/org.hamcrest/hamcrest-core
+    testCompile group: 'org.hamcrest', name: 'hamcrest-core', version: '1.3'
+    implementation group: 'com.opencsv', name: 'opencsv', version: '4.1'
+    // https://mvnrepository.com/artifact/org.apache.httpcomponents/httpclient
+    compile group: 'org.apache.httpcomponents', name: 'httpclient', version: '4.0-alpha4'
+    testCompile group: 'org.apache.logging.log4j', name: 'log4j-slf4j-impl', version: '2.11.2'
+    // https://mvnrepository.com/artifact/org.apache.httpcomponents/fluent-hc
+    compile group: 'org.apache.httpcomponents', name: 'fluent-hc', version: '4.2.1'
+    // https://mvnrepository.com/artifact/junit/junit
+    testCompile group: 'junit', name: 'junit', version: '4.4'
+    
+}
+
+run {
+    mainClassName = 'de.tudresden.inf.st.eraser.benchmark.Main'
+    standardInput = System.in
+    if (project.hasProperty("appArgs")) {
+        args Eval.me(appArgs)
+    }
+}
+
+sourceSets {
+    main {
+        java {
+            srcDir 'src/main/java'
+        }
+    }
+}
diff --git a/benchmark/src/main/java/de/tudresden/inf/st/eraser/benchmark/Benchmark.java b/benchmark/src/main/java/de/tudresden/inf/st/eraser/benchmark/Benchmark.java
new file mode 100644
index 0000000000000000000000000000000000000000..2c1d64d5be93238589167e0c474218d3b22a952f
--- /dev/null
+++ b/benchmark/src/main/java/de/tudresden/inf/st/eraser/benchmark/Benchmark.java
@@ -0,0 +1,146 @@
+package de.tudresden.inf.st.eraser.benchmark;
+
+import com.opencsv.CSVReader;
+import org.apache.http.HttpResponse;
+import org.apache.http.client.fluent.Request;
+import org.apache.http.entity.ContentType;
+import org.apache.http.util.EntityUtils;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import java.io.File;
+import java.io.FileReader;
+import java.util.Arrays;
+
+public class Benchmark {
+
+    //read every 5 s from csv
+    //activity CSV
+    /**
+     * Col 1: smartphone acceleration x
+     * Col 2: smartphone acceleration y
+     * Col 3: smartphone acceleration z
+     * Col 4: smartphone rotation x
+     * Col 5: smartphone rotation y
+     * Col 6: smartphone rotation z
+     * Col 7: watch acceleration x
+     * Col 8: watch acceleration y
+     * Col 9: watch acceleration z
+     * Col 10: watch rotation x
+     * Col 11: watch rotation y
+     * Col 12: watch rotation z/*/
+    //preference CSV
+    /**
+     * Col 1: Activity
+     * Col 2: watch brightness range "bright, medium, dimmer, dark"*/
+
+    private String a_csv_file_path;
+    private String p_csv_file_path;
+    private static final Logger logger = LogManager.getLogger(Benchmark.class);
+    private static final String ERASER_ITEM_URI = "http://localhost:4567/model/items/";
+    //TODO ITEM_NAME HAS TO BE DISCUSSED
+    private static final String[] ACTIVITY_ITEM_NAME = {
+            "m_accel_x", "m_accel_y", "m_accel_z", "m_rotation_x",
+            "m_rotation_y", "m_rotation_z", "w_accel_x", "w_accel_y",
+            "w_accel_z", "w_rotation_x", "w_rotation_y", "w_rotation_z"};
+    private static final String[] PREFERENCE_ITEM_NAME = {
+           "w_brightness"
+    };
+    private static boolean flag2 = true;
+    //csv_type is activity or preference
+    Benchmark(String a_csv_file_path, String p_csv_file_path) {
+        this.a_csv_file_path = a_csv_file_path;
+        this.p_csv_file_path = p_csv_file_path;
+    }
+
+    void start(){
+        String PREFERENCE_URL = "http://localhost:4567/model/items/iris1_item/state";
+        String ACTIVITY_URL = "http://localhost:4567/activity/current";
+        int TIME_PERIOD = 5000;
+        File a_file;
+        File p_file;
+        FileReader a_file_reader;
+        FileReader p_file_reader;
+        CSVReader a_csv_reader;
+        CSVReader p_csv_reader;
+        String[] a_next_record;
+        String[] p_next_record;
+        boolean flag1;
+        a_file=new File(a_csv_file_path);
+        p_file= new File(p_csv_file_path);
+        try {
+            a_file_reader =new FileReader(a_file);
+            p_file_reader=new FileReader(p_file);
+            a_csv_reader = new CSVReader(a_file_reader);
+            p_csv_reader = new CSVReader(p_file_reader);
+            while ((((a_next_record = a_csv_reader.readNext())!= null) && flag2)){
+                try{Thread.sleep(TIME_PERIOD);}catch (InterruptedException e){e.printStackTrace();}
+                String[] values = Arrays.copyOf(a_next_record,12);
+                setNewValue(values, ACTIVITY_ITEM_NAME,"activity");
+                HttpResponse response= Request.Get(ACTIVITY_URL).execute().returnResponse();
+                String status = response.getStatusLine().toString();
+                if(status.contains("200")){
+                    flag1 = true;
+                    logger.info("activity should be (read direct from CSV): " + a_next_record[12]);
+                    logger.info(EntityUtils.toString(response.getEntity()));
+                    logger.info("get activity from web server: response 200 ok");
+                }else{
+                    flag1 = false;
+                    flag2 = false;
+                    logger.info("can not get the activity from the web server");
+                }
+                while((((p_next_record = p_csv_reader.readNext()) != null) && flag1)) {
+                    try{Thread.sleep(TIME_PERIOD);}catch (InterruptedException e){e.printStackTrace();}
+                    String[] values1 = Arrays.copyOf(p_next_record,2);
+                    setNewValue(values1, PREFERENCE_ITEM_NAME,"preference");
+                    HttpResponse response1= Request.Get(PREFERENCE_URL).execute().returnResponse();
+                    String status1=response1.getStatusLine().toString();
+                    if(status1.contains("200")){
+                        flag2 = true;
+                        logger.info("get the iris1_item preference from web server: response 200 ok, value is: "+EntityUtils.toString(response1.getEntity()));
+                    }else {flag2 = false;
+                    logger.info("can not get the iris1_item from the web server");}
+                    break;
+                }
+            }
+        }
+        catch (Exception e){
+            e.printStackTrace();
+        }
+    }
+    private void setNewValue(String[] values, String[] name, String file_typ){
+        if(file_typ.equals("activity"))
+        {
+            int i = 0;
+            for(String value : values){
+                String uri = ERASER_ITEM_URI + name[i] + "/state";
+                try {
+                    HttpResponse httpResponse = Request.Put(uri)
+                            .bodyString(value, ContentType.TEXT_PLAIN)
+                            .execute().returnResponse();
+                    String status=httpResponse.getStatusLine().toString();
+                    if(status.contains("200")){
+                        logger.info("put activity input name: "+name[i]+", value: "+value+"to web server: response 200 ok");
+                    }else{
+                        logger.info("can not put activity inputs to rest server");
+                    }
+                }catch (Exception e){
+                    e.printStackTrace();
+                }
+                i+=1;
+            }
+        }else{
+            String uri= ERASER_ITEM_URI + "w_brightness" +"/state";
+            try {
+                HttpResponse httpResponse = Request.Put(uri)
+                        .bodyString(values[1], ContentType.TEXT_PLAIN)
+                        .execute().returnResponse();
+                String put_response=httpResponse.getStatusLine().toString();
+                if (put_response.contains("200")){logger.info("put w_brightness to web server: response 200 ok");}else{
+                    logger.info("can not put w_brightness to rest server");
+                }
+            }catch (Exception e){
+                e.printStackTrace();
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/benchmark/src/main/java/de/tudresden/inf/st/eraser/benchmark/Main.java b/benchmark/src/main/java/de/tudresden/inf/st/eraser/benchmark/Main.java
new file mode 100644
index 0000000000000000000000000000000000000000..ac7536cae53749132be31ebe028e5ec0e55c30f4
--- /dev/null
+++ b/benchmark/src/main/java/de/tudresden/inf/st/eraser/benchmark/Main.java
@@ -0,0 +1,12 @@
+package de.tudresden.inf.st.eraser.benchmark;
+
+public class Main {
+
+    public static void main(String[] args) {
+        String A_CSV_FILE_PATH = "../datasets/backup/activity_data.csv";
+        String P_CSV_FILE_PATH = "../datasets/backup/preference_data.csv";
+        Benchmark benchmark=new Benchmark(A_CSV_FILE_PATH,P_CSV_FILE_PATH);
+        benchmark.start();
+    }
+}
+
diff --git a/benchmark/src/main/resources/log4j2.xml b/benchmark/src/main/resources/log4j2.xml
new file mode 100644
index 0000000000000000000000000000000000000000..867ec439d0a32dcb5f8b3e2d0c7485d7d8da418c
--- /dev/null
+++ b/benchmark/src/main/resources/log4j2.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Configuration>
+    <Appenders>
+        <Console name="Console">
+            <PatternLayout pattern="%highlight{%d{HH:mm:ss.SSS} %-5level} %c{1.} - %msg%n"/>
+        </Console>
+        <RollingFile name="RollingFile" fileName="logs/eraser.log"
+                    filePattern="logs/eraser-%i.log">
+            <PatternLayout pattern="%d{HH:mm:ss.SSS} %-5level %logger{36} - %msg%n"/>
+            <Policies>
+                <OnStartupTriggeringPolicy/>
+            </Policies>
+            <DefaultRolloverStrategy max="20"/>
+        </RollingFile>
+    </Appenders>
+    <Loggers>
+        <Root level="debug">
+            <AppenderRef ref="Console"/>
+            <AppenderRef ref="RollingFile"/>
+        </Root>
+    </Loggers>
+</Configuration>
diff --git a/build.gradle b/build.gradle
index 7348f5134a49d4e0231a68894601c7c8684af1d6..f1f3b408f121f388615eca5d85cd3f128128f17c 100644
--- a/build.gradle
+++ b/build.gradle
@@ -4,7 +4,7 @@ plugins {
 
 allprojects  {
 	group = 'de.tudresden.inf.st'
-	version = '1.0.0-SNAPSHOT'
+	version = '0.1'
 }
 
 subprojects {
@@ -31,4 +31,16 @@ subprojects {
 	artifacts {
 		testArtifacts testJar
 	}
+
+	repositories {
+		mavenCentral()
+	}
+
+	dependencies {
+		compile group: 'org.apache.logging.log4j', name: 'log4j-api', version: '2.11.2'
+		compile group: 'org.apache.logging.log4j', name: 'log4j-core', version: '2.11.2'
+		testCompile group: 'junit', name: 'junit', version: '4.12'
+		testCompile group: 'org.hamcrest', name: 'hamcrest-junit', version: '2.0.0.0'
+	}
+
 }
diff --git a/cloc/.gitignore b/cloc/.gitignore
index b80f313c862d202eba757744403aaa366f6ba557..e76b2bf0f754f40018bff9aefac38201ee2a7c95 100644
--- a/cloc/.gitignore
+++ b/cloc/.gitignore
@@ -2,3 +2,4 @@ eraser-base.*
 eraser-base-*
 feedbackloop.txt
 my_definitions.txt
+logs/
diff --git a/commons.color/.gitignore b/commons.color/.gitignore
index 83ccc54d0286590d31587a71aaa789e780bff41e..70b583e34c3316bcd77c807e2d6b85db5e7d49f6 100644
--- a/commons.color/.gitignore
+++ b/commons.color/.gitignore
@@ -1,2 +1,3 @@
 /build/
 /bin/
+logs/
diff --git a/commons.color/build.gradle b/commons.color/build.gradle
index 135a0194474bfeab48418cd7589d117a4f360805..30ce209c40610e390f709d9ec5a36c7324543c23 100644
--- a/commons.color/build.gradle
+++ b/commons.color/build.gradle
@@ -1,18 +1,5 @@
-repositories {
-    mavenCentral()
-}
-
-apply plugin: 'java'
-
-sourceCompatibility = 1.8
-
 dependencies {
-    compile group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: '2.9.8'
-    compile group: 'org.apache.logging.log4j', name: 'log4j-api', version: '2.11.1'
-    compile group: 'org.apache.logging.log4j', name: 'log4j-core', version: '2.11.1'
-    compile 'org.apache.commons:commons-math3:3.6.1'
-    testCompile group: 'junit', name: 'junit', version: '4.12'
-    testCompile group: 'org.hamcrest', name: 'hamcrest-junit', version: '2.0.0.0'
+    compile group: 'org.apache.commons', name: 'commons-math3', version: '3.6.1'
 }
 
 sourceSets {
diff --git a/eraser-base/.gitignore b/eraser-base/.gitignore
index a00ac8701e237817ed76681b9ea8db2e6229b818..19ad8f831544c2661c133c48b7da7a78df83a50d 100644
--- a/eraser-base/.gitignore
+++ b/eraser-base/.gitignore
@@ -3,3 +3,5 @@ src/gen/
 src/gen-res/
 .test-tmp
 /bin/
+logs/
+/doc/
diff --git a/eraser-base/build.gradle b/eraser-base/build.gradle
index 3e5da9c2b15f01d65d652a3e99f4d45325777afe..fbe5f76ec7b7097ca4ef1ca48181b4c26eb71433 100644
--- a/eraser-base/build.gradle
+++ b/eraser-base/build.gradle
@@ -1,27 +1,24 @@
-repositories {
-    mavenCentral()
-}
-
 apply plugin: 'jastadd'
 apply plugin: 'application'
 apply plugin: 'jacoco'
+apply plugin: 'idea'
+apply plugin: 'distribution'
 
 dependencies {
     compile group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: '2.9.8'
     compile group: 'net.sf.beaver', name: 'beaver-rt', version: '0.9.11'
-    compile group: 'org.apache.logging.log4j', name: 'log4j-api', version: '2.11.1'
-    compile group: 'org.apache.logging.log4j', name: 'log4j-core', version: '2.11.1'
-    compile 'org.fusesource.mqtt-client:mqtt-client:1.14'
-    compile 'org.apache.commons:commons-math3:3.6.1'
-    compile 'org.influxdb:influxdb-java:2.14'
-    testCompile group: 'junit', name: 'junit', version: '4.12'
+    compile group: 'org.fusesource.mqtt-client', name: 'mqtt-client', version: '1.15'
+    compile group: 'org.influxdb', name: 'influxdb-java', version: '2.15'
+    testCompile group: 'org.testcontainers', name: 'testcontainers', version: '1.11.2'
+    testCompile group: 'org.testcontainers', name: 'influxdb', version: '1.11.2'
+    testCompile group: 'org.apache.logging.log4j', name: 'log4j-slf4j-impl', version: '2.11.2'
 }
 
 buildscript {
     repositories.mavenLocal()
     repositories.mavenCentral()
     dependencies {
-        classpath 'org.jastadd:jastaddgradle:1.13.2'
+        classpath group: 'org.jastadd', name: 'jastaddgradle', version: '1.13.2'
     }
 }
 
@@ -47,18 +44,22 @@ jacocoTestReport {
     }
 }
 
+def relastFiles = fileTree('src/main/jastadd/') {
+    include '**/*.relast' }.toList().toArray()
+String[] relastArguments = [
+        "libs/relast.jar",
+        "--grammarName=./src/main/jastadd/mainGen",
+        "--useJastAddNames",
+        "--listClass=RefList",
+        "--jastAddList=JastAddList",
+        "--file"
+]
 task preprocess(type: JavaExec) {
     group = 'Build'
     main = "-jar"
-    args = [
-            "libs/relast-compiler.jar",
-            "./src/main/jastadd/main.relast",
-            "--listClass=RefList",
-            "--jastAddList=JastAddList",
-            "--file"
-    ]
-
-    inputs.files file("./src/main/jastadd/main.relast")
+    args relastArguments + relastFiles
+
+    inputs.files relastFiles
     outputs.files file("./src/main/jastadd/mainGen.ast"), file("./src/main/jastadd/mainGen.jadd")
 }
 
@@ -79,6 +80,8 @@ jastadd {
     parser.genDir = "src/gen/java/de/tudresden/inf/st/eraser/jastadd/parser"
 }
 
+idea.module.generatedSourceDirs += file('src/gen/java')
+
 sourceSets.main {
     java {
         srcDir 'src/gen/java'
@@ -90,32 +93,32 @@ javadoc {
     failOnError = false
 }
 
-task RagDoll(type: Javadoc, dependsOn:javadoc) {
-    doFirst {
-        options.addStringOption("ragroot", "./src/main/jastadd")
-    }
-    group = "documentation"
-    description = 'create a RagDoll documentation'
-    classpath = javadoc.classpath
-    destinationDir = javadoc.destinationDir
-    excludes = javadoc.excludes
-    executable = javadoc.executable
-    failOnError = false
-    includes = javadoc.includes
-    options.doclet = "ragdoll.RagDollDoclet"
-    options.docletpath = files('libs/RagDoll.jar').asList()
+String[] arguments = ["libs/rd-builder.jar", "-d", "doc/"]
+def allSrcFiles = sourceSets.main.allSource.findAll { it.name.endsWith('java') }.toArray()
+def ragdocViewSrcData = '../ragdoc-view/src/data/'
 
-    source = javadoc.source
-    options.linkSource = true
+task ragdoc(type: JavaExec, dependsOn: assemble, overwrite: true) {
+    group = 'documentation'
+    description = 'Create ragdoc json documentation files'
+    main = "-jar"
+    args arguments + allSrcFiles
+}
 
-    // title not working for some reason
-    title = ""
-    doLast {
-        println "Visit: file://" + destinationDir + "/index.html"
-    }
+task cleanRagdoc(type: Delete) {
+    group = 'documentation'
+    delete fileTree(ragdocViewSrcData + '/*')
+}
+
+task copyRagdoc(type: Copy, dependsOn: cleanRagdoc) {
+    group = 'documentation'
+    description = 'Copy ragdoc json documentation files to ragdoc-viewer'
+    from 'doc/'
+    into ragdocViewSrcData
+    eachFile { println it.file }
 }
 
 generateAst.dependsOn preprocess
+generateAst.inputs.files file("./src/main/jastadd/mainGen.ast"), file("./src/main/jastadd/mainGen.jadd")
 //compileJava.dependsOn jastadd
 //
 //// always run jastadd
diff --git a/eraser-base/libs/rd-builder.jar b/eraser-base/libs/rd-builder.jar
new file mode 100644
index 0000000000000000000000000000000000000000..d5f8eed931b820b41f1fa22425b2833a4f90cd2c
Binary files /dev/null and b/eraser-base/libs/rd-builder.jar differ
diff --git a/eraser-base/libs/relast-compiler.jar b/eraser-base/libs/relast-compiler.jar
deleted file mode 100644
index a1a9b656a4eb4f990decbcb8a522b063332133bf..0000000000000000000000000000000000000000
Binary files a/eraser-base/libs/relast-compiler.jar and /dev/null differ
diff --git a/eraser-base/libs/relast.jar b/eraser-base/libs/relast.jar
new file mode 100644
index 0000000000000000000000000000000000000000..9b52866a399cfdd0c4fe29c102125beab5d77989
Binary files /dev/null and b/eraser-base/libs/relast.jar differ
diff --git a/eraser-base/src/main/jastadd/AdditionalTypes.jadd b/eraser-base/src/main/jastadd/AdditionalTypes.jadd
index dbb1e695f544918c6e2019bcea5c32237ea0ec4d..32651117408970af5b3a57f4ddc48e2f7f2a0d8c 100644
--- a/eraser-base/src/main/jastadd/AdditionalTypes.jadd
+++ b/eraser-base/src/main/jastadd/AdditionalTypes.jadd
@@ -1,13 +1,29 @@
 aspect AdditionalTypes {
   public class StringList extends beaver.Symbol implements Iterable<String> {
-    private java.util.List<String> delegatee = new java.util.ArrayList<String>();
+    private java.util.Deque<String> delegatee = new java.util.ArrayDeque<>();
 
     public java.util.Iterator<String> iterator() {
-      return delegatee.iterator();
+      return delegatee.descendingIterator();
     }
 
     public void add(String s) {
       delegatee.add(s);
     }
   }
+
+  public class TypedKeyMap<T> extends beaver.Symbol implements Iterable<AbstractMap.SimpleEntry<T, String>> {
+    private java.util.Deque<AbstractMap.SimpleEntry<T, String>> delegatee = new java.util.ArrayDeque<>();
+
+    public java.util.Iterator<AbstractMap.SimpleEntry<T, String>> iterator() {
+      return delegatee.descendingIterator();
+    }
+
+    public void put(T key, String value) {
+      delegatee.add(new AbstractMap.SimpleEntry<>(key, value));
+    }
+  }
+
+  public class StringKeyMap extends TypedKeyMap<String> { }
+
+  public class IntegerKeyMap extends TypedKeyMap<Integer> { }
 }
diff --git a/eraser-base/src/main/jastadd/DecisionTree.jrag b/eraser-base/src/main/jastadd/DecisionTree.jrag
index ed23d8ad22f1cb65acd34e2e5b2919571e788945..f50ca0562a920844e46a838c0f655f97bf804a01 100644
--- a/eraser-base/src/main/jastadd/DecisionTree.jrag
+++ b/eraser-base/src/main/jastadd/DecisionTree.jrag
@@ -4,26 +4,28 @@ aspect DecisionTree {
   public class DecisionTreeLeaf implements Leaf { }
 
   //--- classify ---
-  syn Leaf DecisionTreeRoot.classify() {
+  syn DecisionTreeLeaf DecisionTreeRoot.classify() {
     return getRootRule().classify();
   }
 
-  syn Leaf DecisionTreeElement.classify();
+  syn DecisionTreeLeaf DecisionTreeElement.classify();
 
-  syn Leaf DecisionTreeRule.classify();
+  syn DecisionTreeLeaf DecisionTreeRule.classify();
 
-  syn Leaf ItemStateCheckRule.classify() {
+  syn DecisionTreeLeaf ItemStateCheckRule.classify() {
     boolean chooseLeft = getItemStateCheck().holds();
     return (chooseLeft ? getLeft() : getRight()).classify();
   }
 
-  syn Leaf DecisionTreeLeaf.classify() = this;
+  syn DecisionTreeLeaf DecisionTreeLeaf.classify() = this;
 
   //--- holds ---
-  syn boolean ItemStateCheck.holds();
+  syn boolean ItemStateCheck.holds() = holdsFor(getItem());
 
-  syn boolean ItemStateNumberCheck.holds() {
-    double actual = getItem().getStateAsDouble();
+  //--- holdsFor ---
+  syn boolean ItemStateCheck.holdsFor(Item item);
+  eq ItemStateNumberCheck.holdsFor(Item item) {
+    double actual = item.getStateAsDouble();
     double expected = getValue();
     boolean result;
     switch (getComparator()) {
@@ -38,12 +40,33 @@ aspect DecisionTree {
     return result;
   }
 
-  syn boolean ItemStateStringCheck.holds() {
+  eq ItemStateStringCheck.holdsFor(Item item) {
     // TODO implement this
     System.err.println("ItemStateStringCheck in Decision Tree not implemented!");
     return false;
   }
 
+  //--- computePreferences ---
+  syn List<ItemPreference> DecisionTreeLeaf.computePreferences() {
+    // iterate over preference of this leaf, and all its parents and ancestors
+    List<ItemPreference> result = new ArrayList<>();
+    Set<Item> seenItems = new HashSet<>();
+    List<DecisionTreeElement> ancestors = ancestors();
+    for (ItemPreference pref : getPreferenceList()) {
+      result.add(pref);
+      seenItems.add(pref.getItem());
+    }
+    for (DecisionTreeElement ancestor : ancestors) {
+      for (ItemPreference pref : ancestor.getPreferenceList()) {
+        if (!seenItems.contains(pref.getItem())) {
+          result.add(pref);
+          seenItems.add(pref.getItem());
+        }
+      }
+    }
+    return result;
+  }
+
   //--- ancestors ---
   inh List<DecisionTreeElement> DecisionTreeElement.ancestors();
   eq DecisionTreeRule.getLeft().ancestors() {
@@ -57,5 +80,11 @@ aspect DecisionTree {
     return result;
   }
   eq DecisionTreeRoot.getRootRule().ancestors() = new ArrayList();
+  eq DummyMachineLearningModel.getCurrent().ancestors() = Collections.emptyList();
+
+  public void DecisionTreeRoot.connectItems(List<String> itemNames) {
+    // TODO walk through the tree using depth-first-search
+    throw new UnsupportedOperationException();
+  }
 
 }
diff --git a/eraser-base/src/main/jastadd/DecisionTree.relast b/eraser-base/src/main/jastadd/DecisionTree.relast
new file mode 100644
index 0000000000000000000000000000000000000000..17a1e65c1c6cd12ba2d49eeceba1eb920dae31bd
--- /dev/null
+++ b/eraser-base/src/main/jastadd/DecisionTree.relast
@@ -0,0 +1,12 @@
+// ----------------    Decision Tree    ------------------------------
+DecisionTreeRoot : InternalMachineLearningModel ::= RootRule:DecisionTreeRule ;
+abstract DecisionTreeElement ::= Preference:ItemPreference*;
+abstract DecisionTreeRule : DecisionTreeElement ::= Left:DecisionTreeElement Right:DecisionTreeElement <Label:String> ;
+ItemStateCheckRule : DecisionTreeRule ::= ItemStateCheck ;
+
+abstract ItemStateCheck ::= <Comparator:ComparatorType> ;
+rel ItemStateCheck.Item -> Item ;
+
+ItemStateNumberCheck : ItemStateCheck ::= <Value:double> ;
+ItemStateStringCheck : ItemStateCheck ::= <Value:String> ;
+DecisionTreeLeaf : DecisionTreeElement ::= <ActivityIdentifier:int> <Label:String> ;
diff --git a/eraser-base/src/main/jastadd/Expression.jrag b/eraser-base/src/main/jastadd/Expression.jrag
new file mode 100644
index 0000000000000000000000000000000000000000..e3d6a6b6b01effb2b014bbe85b7360aea5314a3a
--- /dev/null
+++ b/eraser-base/src/main/jastadd/Expression.jrag
@@ -0,0 +1,33 @@
+aspect Expression {
+
+  //--- eval (logical) ---
+  syn boolean LogicalExpression.eval();
+  eq ParenthesizedLogicalExpression.eval() = getOperand().eval();
+  eq NotExpression.eval() = !getOperand().eval();
+  eq ComparingExpression.eval() {
+    double leftValue = getLeftOperand().eval();
+    double rightValue = getRightOperand().eval();
+    switch (getComparator()) {
+      case LessThan:           return leftValue <  rightValue;
+      case LessOrEqualThan:    return leftValue <= rightValue;
+      case Equals:             return leftValue == rightValue;
+      case NotEquals:          return leftValue != rightValue;
+      case GreaterThan:        return leftValue >  rightValue;
+      case GreaterOrEqualThan: return leftValue >= rightValue;
+    }
+    return false;
+  }
+  eq AndExpression.eval() = getLeftOperand().eval() && getRightOperand().eval();
+  eq OrExpression.eval() = getLeftOperand().eval() || getRightOperand().eval();
+
+  //--- eval (number) ---
+  syn double NumberExpression.eval();
+  eq AddExpression.eval() = getLeftOperand().eval() + getRightOperand().eval();
+  eq SubExpression.eval() = getLeftOperand().eval() - getRightOperand().eval();
+  eq MultExpression.eval() = getLeftOperand().eval() * getRightOperand().eval();
+  eq DivExpression.eval() = getLeftOperand().eval() / getRightOperand().eval();
+  eq PowerExpression.eval() = Math.pow(getLeftOperand().eval(), getRightOperand().eval());
+  eq Designator.eval() = getItem().getStateAsDouble();
+  eq ParenthesizedNumberExpression.eval() = getOperand().eval();
+  eq NumberLiteralExpression.eval() = getValue();
+}
diff --git a/eraser-base/src/main/jastadd/Expression.relast b/eraser-base/src/main/jastadd/Expression.relast
new file mode 100644
index 0000000000000000000000000000000000000000..591e112a21b904c88a450e3a2cf92c982a6a4c03
--- /dev/null
+++ b/eraser-base/src/main/jastadd/Expression.relast
@@ -0,0 +1,25 @@
+abstract Expression ;
+
+abstract NumberExpression : Expression ;
+ParenthesizedNumberExpression : NumberExpression ::= Operand:NumberExpression ;
+NumberLiteralExpression : NumberExpression ::= <Value:double> ;
+
+// variation point of the expression language:
+Designator : NumberExpression ;
+rel Designator.Item -> Item ;
+
+abstract BinaryNumberExpression : NumberExpression ::= LeftOperand:NumberExpression RightOperand:NumberExpression ;
+AddExpression : BinaryNumberExpression ;
+SubExpression : BinaryNumberExpression ;
+MultExpression : BinaryNumberExpression ;
+DivExpression : BinaryNumberExpression ;
+PowerExpression : BinaryNumberExpression ;
+
+abstract LogicalExpression : Expression ;
+ParenthesizedLogicalExpression : LogicalExpression ::= Operand:LogicalExpression ;
+NotExpression : LogicalExpression ::= Operand:LogicalExpression ;
+ComparingExpression : LogicalExpression ::= LeftOperand:NumberExpression RightOperand:NumberExpression <Comparator:ComparatorType> ;
+
+abstract BinaryLogicalExpression : LogicalExpression ::= LeftOperand:LogicalExpression RightOperand:LogicalExpression ;
+AndExpression : BinaryLogicalExpression ;
+OrExpression : BinaryLogicalExpression ;
diff --git a/eraser-base/src/main/jastadd/Imports.jadd b/eraser-base/src/main/jastadd/Imports.jadd
index 376d3aac516036dd8664b1870571f57688598120..319b024d1698c7efaabc89feed700f98fa24fa9c 100644
--- a/eraser-base/src/main/jastadd/Imports.jadd
+++ b/eraser-base/src/main/jastadd/Imports.jadd
@@ -1,5 +1,7 @@
 import java.util.*;
-import de.tudresden.inf.st.eraser.util.PrinterUtils;
+import java.time.Instant;
+import de.tudresden.inf.st.eraser.util.MemberPrinter;
+import de.tudresden.inf.st.eraser.util.JavaUtils;
 
 aspect Imports {
 
diff --git a/eraser-base/src/main/jastadd/Item.jrag b/eraser-base/src/main/jastadd/Item.jrag
index 916dd0b786bbca7b3710244e55d631e0c2dd1a12..0b370142cf57c377775294bb6c758dafa7db036f 100644
--- a/eraser-base/src/main/jastadd/Item.jrag
+++ b/eraser-base/src/main/jastadd/Item.jrag
@@ -1,25 +1,39 @@
 aspect ItemHandling {
 
-  private static final java.text.DateFormat DateTimeItem.FORMAT = new java.text.SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS") {{
-    setTimeZone(TimeZone.getTimeZone("UTC"));
-  }};
+  protected boolean Item.isFrozen = false;
+  public void Item.freeze() { isFrozen = true; }
+  public void Item.unfreeze() { isFrozen = false; }
+  public final boolean Item.isFrozen() { return isFrozen; }
+
+  protected boolean Item.sendState = true;
+  public void Item.disableSendState() { sendState = false; }
+  public void Item.enableSendState() { sendState = true; }
+  public final boolean Item.isSendState() { return sendState; }
+
+  //--- DateTimeItem.ALTERNATIVE_FORMAT ---
+  private static final java.time.format.DateTimeFormatter DateTimeItem.ALTERNATIVE_FORMAT = new java.time.format.DateTimeFormatterBuilder()
+    .appendPattern("yyyy-MM-dd'T'HH:mm:ss.SSSZ")
+    .toFormatter()
+    .withZone(java.time.ZoneId.of("UTC"));
 
   //--- getStateAsString ---
   syn String Item.getStateAsString();
   eq ColorItem.getStateAsString() = getState().toString();
-  eq DateTimeItem.getStateAsString() = FORMAT.format(getState());
+  eq DateTimeItem.getStateAsString() = getState().toString();
   eq ItemWithBooleanState.getStateAsString() = Boolean.toString(getState());
+  eq SwitchItem.getStateAsString() = getState() ? "ON" : "OFF";
+  eq ContactItem.getStateAsString() = getState() ? "OPEN" : "CLOSED";
   eq ItemWithDoubleState.getStateAsString() = Double.toString(getState());
-  eq ItemWithStringState.getStateAsString() = getState().toString();
+  eq ItemWithStringState.getStateAsString() = getState();
 
   //--- getStateAsDouble ---
   syn double Item.getStateAsDouble();
   // TupleHSB and String work like default
   eq ColorItem.getStateAsDouble() {
     logger.warn("getStateAsDouble called on item " + getLabel() + ". Using brightness.");
-    return getState().brightness;
+    return getState().getBrightness();
   }
-  eq DateTimeItem.getStateAsDouble() = getState().getTime();
+  eq DateTimeItem.getStateAsDouble() = getState().toEpochMilli();
   eq ItemWithBooleanState.getStateAsDouble() = getState() ? 1 : 0;
   eq ItemWithDoubleState.getStateAsDouble() = getState();
   eq ItemWithStringState.getStateAsDouble() {
@@ -33,161 +47,274 @@ aspect ItemHandling {
 
   //--- setStateFromString ---
   public abstract void Item.setStateFromString(String value);
-  // TupleHSB
   public void ColorItem.setStateFromString(String value) {
-    this.setState(TupleHSB.parse(value));
+    if (value.indexOf(",") == -1) {
+      // no commata, thus only one value, interpret as brightness
+      setBrightness(Integer.valueOf(value));
+    } else {
+      // interpret as HSB value
+      setState(TupleHSB.parse(value));
+    }
   }
-  // Date
   public void DateTimeItem.setStateFromString(String value) {
-    try{
-      this.setState(FORMAT.parse(value));
-    } catch (java.text.ParseException e1) {
-      // try to read input as number and use that
-      try {
-        long time = Long.parseLong(value);
-        this.setState(new Date(time));
-        // exit the method to avoid printing the error message for e1
-        return;
-      } catch (NumberFormatException e2) {
-        logger.catching(e2);
-      }
-      logger.catching(e1);
+    Exception exception = null;
+    // try normal instant parsing. Format example: 2019-07-22T12:58:08.960Z
+    try {
+      this.setState(Instant.parse(value));
+      return;
+    } catch (java.time.format.DateTimeParseException e) { /* empty */ }
+    // try openHAB parsing. Format example: 2019-07-22T12:58:08.960+0000
+    try {
+      this.setState(ALTERNATIVE_FORMAT.parse(value, Instant::from));
+      return;
+    } catch (java.time.format.DateTimeParseException e) { /* empty */ }
+    // try to read input as number and use that
+    try {
+      long time = Long.parseLong(value);
+      this.setStateFromLong(time);
+      return;
+    } catch (NumberFormatException e) {
+      logger.warn("Could not parse date value from {}", value);
     }
   }
-  // boolean
   public void ItemWithBooleanState.setStateFromString(String value) {
     this.setState(Boolean.parseBoolean(value));
   }
-  // double
+  public void SwitchItem.setStateFromString(String value) {
+    switch (value) {
+      case "ON": this.setState(true); break;
+      case "OFF": this.setState(false); break;
+      default: super.setStateFromString(value);
+    }
+  }
+  public void ContactItem.setStateFromString(String value) {
+    switch (value) {
+      case "OPEN": this.setState(true); break;
+      case "CLOSED": this.setState(false); break;
+      default: super.setStateFromString(value);
+    }
+  }
+  public void RollerShutterItem.setStateFromString(String value) {
+    switch (value) {
+      case "UP": this.setState(0); break;
+      case "DOWN": this.setState(100); break;
+      default: super.setStateFromString(value);
+    }
+  }
   public void ItemWithDoubleState.setStateFromString(String value) {
     this.setState(Double.parseDouble(value));
   }
-  // String
   public void ItemWithStringState.setStateFromString(String value) {
     this.setState(value);
   }
-
   public void Item.setStateFromString(String value, boolean shouldSendState) {
-    boolean before = getDefaultShouldSendState();
-    setDefaultShouldSendState(shouldSendState);
+    boolean before = sendState;
+    sendState = shouldSendState;
     setStateFromString(value);
-    setDefaultShouldSendState(before);
+    sendState = before;
   }
 
   //--- setStateFromLong ---
   public abstract void Item.setStateFromLong(long value);
-  // TupleHSB
   public void ColorItem.setStateFromLong(long value) {
     // only set brightness
-    this.setState(TupleHSB.of(getState().hue, getState().saturation, Math.toIntExact(value)));
+    this.setState(getState().withDifferentBrightness(Math.toIntExact(value)));
   }
-  // Date
   public void DateTimeItem.setStateFromLong(long value) {
-    this.setState(new Date(value));
+    this.setState(Instant.ofEpochMilli(value));
   }
-  // boolean
   public void ItemWithBooleanState.setStateFromLong(long value) {
     this.setState(value != 0);
   }
-  // double
   public void ItemWithDoubleState.setStateFromLong(long value) {
     this.setState(value);
   }
-  // String
   public void ItemWithStringState.setStateFromLong(long value) {
     this.setState(Long.toString(value));
   }
-
   public void Item.setStateFromLong(long value, boolean shouldSendState) {
-    boolean before = getDefaultShouldSendState();
-    setDefaultShouldSendState(shouldSendState);
+    boolean before = sendState;
+    sendState = shouldSendState;
     setStateFromLong(value);
-    setDefaultShouldSendState(before);
+    sendState = before;
   }
 
-//  public void setState(String value) {
-//    this.setState(value, getDefaultShouldSendState());
-//  }
+  //--- setStateFromBoolean ---
+  public abstract void Item.setStateFromBoolean(boolean value);
+  public void ColorItem.setStateFromBoolean(boolean value) {
+    setBrightness(value ? 100 : 0);
+  }
+  public void DateTimeItem.setStateFromBoolean(boolean value) {
+    // there is no good way here
+    logger.warn("Ignoring boolean update using {} for {}", value, this);
+  }
+  public void ItemWithBooleanState.setStateFromBoolean(boolean value) {
+    this.setState(value);
+  }
+  public void ItemWithDoubleState.setStateFromBoolean(boolean value) {
+    this.setState(value ? 1.0 : 0.0);
+  }
+  public void ItemWithStringState.setStateFromBoolean(boolean value) {
+    this.setState(Boolean.toString(value));
+  }
+  public void Item.setStateFromBoolean(boolean value, boolean shouldSendState) {
+    boolean before = sendState;
+    sendState = shouldSendState;
+    setStateFromBoolean(value);
+    sendState = before;
+  }
 
-//  public void Item.setState(String value, boolean shouldSendState) {
-//    // attention: call to super.setState() without shouldSendState
-//    this.setState(value);
-//    if (shouldSendState) {
-//      try {
-//        if (getTopic() != null) {
-//          sendState();
-//        }
-//      } catch (Exception e) {
-//        logger.catching(e);
-//      }
-//    }
-//    if (hasObserver()) {
-//      getObserver().apply();
-//    }
-//  }
+  //--- setStateFromDouble ---
+  public abstract void Item.setStateFromDouble(double value);
+  public void ColorItem.setStateFromDouble(double value) {
+    setBrightness((int) value);
+  }
+  public void DateTimeItem.setStateFromDouble(double value) {
+    this.setStateFromLong((long) value);
+  }
+  public void ItemWithBooleanState.setStateFromDouble(double value) {
+    this.setState(value != 0);
+  }
+  public void ItemWithDoubleState.setStateFromDouble(double value) {
+    this.setState(value);
+  }
+  public void ItemWithStringState.setStateFromDouble(double value) {
+    this.setState(Double.toString(value));
+  }
+  public void Item.setStateFromDouble(double value, boolean shouldSendState) {
+    boolean before = sendState;
+    sendState = shouldSendState;
+    setStateFromDouble(value);
+    sendState = before;
+  }
+
+  //--- setStateFromColor ---
+  public abstract void Item.setStateFromColor(TupleHSB value);
+  public void ColorItem.setStateFromColor(TupleHSB value) {
+    this.setState(value.clone());
+  }
+  public void DateTimeItem.setStateFromColor(TupleHSB value) {
+    // there is no good way here
+    logger.warn("Ignoring color update using {} for {}", value, this);
+  }
+  public void ItemWithBooleanState.setStateFromColor(TupleHSB value) {
+    this.setState(value != null && value.getBrightness() > 0);
+  }
+  public void ItemWithDoubleState.setStateFromColor(TupleHSB value) {
+    this.setState(value.getBrightness());
+  }
+  public void ItemWithStringState.setStateFromColor(TupleHSB value) {
+    this.setState(value.toString());
+  }
+  public void Item.setStateFromColor(TupleHSB value, boolean shouldSendState) {
+    boolean before = sendState;
+    sendState = shouldSendState;
+    setStateFromColor(value);
+    sendState = before;
+  }
+
+  //--- setStateFromInstant ---
+  public abstract void Item.setStateFromInstant(Instant value);
+  public void ColorItem.setStateFromInstant(Instant value) {
+    // there is no good way here
+    logger.warn("Ignoring date update using {} for {}", this, value);
+  }
+  public void DateTimeItem.setStateFromInstant(Instant value) {
+    this.setState(value);
+  }
+  public void ItemWithBooleanState.setStateFromInstant(Instant value) {
+    this.setState(value != null);
+  }
+  public void ItemWithDoubleState.setStateFromInstant(Instant value) {
+    this.setState(value.toEpochMilli());
+  }
+  public void ItemWithStringState.setStateFromInstant(Instant value) {
+    this.setState(value.toString());
+  }
+  public void Item.setStateFromInstant(Instant value, boolean shouldSendState) {
+    boolean before = sendState;
+    sendState = shouldSendState;
+    setStateFromInstant(value);
+    sendState = before;
+  }
 
   //--- getState ---
   public boolean ItemWithBooleanState.getState() { return get_state(); }
   public String ItemWithStringState.getState() {   return get_state(); }
   public double ItemWithDoubleState.getState() {   return get_state(); }
   public TupleHSB ColorItem.getState() {           return get_state(); }
-  public Date DateTimeItem.getState() {            return get_state(); }
-
-  //--- copyState ---
-  public abstract Object Item.copyState();
-  // boolean can be copied
-  public Boolean ItemWithBooleanState.copyState() { return get_state(); }
-  public String ItemWithStringState.copyState() { return new String(get_state()); }
-  // long can be copied
-  public Double ItemWithDoubleState.copyState() { return get_state(); }
-  public TupleHSB ColorItem.copyState() { return get_state().clone(); }
-  public Object DateTimeItem.copyState() { return get_state().clone(); }
+  public Instant DateTimeItem.getState() {         return get_state(); }
 
   //--- setState(value) ---
-  public void ItemWithBooleanState.setState(boolean value) { setState(value, getDefaultShouldSendState()); }
-  public void ItemWithStringState.setState(String value) { setState(value, getDefaultShouldSendState()); }
-  public void ItemWithDoubleState.setState(double value) { setState(value, getDefaultShouldSendState()); }
-  public void ColorItem.setState(TupleHSB value) { setState(value, getDefaultShouldSendState()); }
-  public void DateTimeItem.setState(Date value) { setState(value, getDefaultShouldSendState()); }
+  public void ItemWithBooleanState.setState(boolean value) { setState(value, sendState); }
+  public void ItemWithStringState.setState(String value) { setState(value, sendState); }
+  public void ItemWithDoubleState.setState(double value) { setState(value, sendState); }
+  public void ColorItem.setState(TupleHSB value) { setState(value, sendState); }
+  public void DateTimeItem.setState(Instant value) { setState(value, sendState); }
 
   //--- setState(value,shouldSendState) ---
   public void ItemWithBooleanState.setState(boolean value, boolean shouldSendState) {
+    if (isFrozen || stateEquals(value)) { return; }
     set_state(value);
-    sendState0(shouldSendState);
+    stateUpdated(shouldSendState);
   }
 
   public void ItemWithStringState.setState(String value, boolean shouldSendState) {
+    if (isFrozen || stateEquals(value)) { return; }
     set_state(value);
-    sendState0(shouldSendState);
+    stateUpdated(shouldSendState);
   }
 
   public void ItemWithDoubleState.setState(double value, boolean shouldSendState) {
+    if (isFrozen || stateEquals(value)) { return; }
     set_state(value);
-    sendState0(shouldSendState);
+    stateUpdated(shouldSendState);
   }
 
   public void ColorItem.setState(TupleHSB value, boolean shouldSendState) {
+    if (isFrozen || stateEquals(value)) { return; }
     set_state(value);
-    sendState0(shouldSendState);
+    stateUpdated(shouldSendState);
   }
 
-  public void DateTimeItem.setState(Date value, boolean shouldSendState) {
+  public void DateTimeItem.setState(Instant value, boolean shouldSendState) {
+    if (isFrozen || stateEquals(value)) { return; }
     set_state(value);
-    sendState0(shouldSendState);
+    stateUpdated(shouldSendState);
   }
 
-  //--- sendState0 ---
-  protected void Item.sendState0(boolean shouldSendState) {
+  //--- stateUpdated ---
+  /**
+   * Called, whenever the state of an item is updated. Does various things including:
+   * <ul>
+   *   <li>Send the new state via MQTT</li>
+   *   <li>Send the new state to Influx DB</li>
+   *   <li>Notify the attached {@link ItemObserver}, if any</li>
+   *   <li>Update state of controlled items</li>
+   * </ul>
+   * @param shouldSendState whether to send the new state (currently affects MQTT and Influx)
+   */
+  protected void Item.stateUpdated(boolean shouldSendState) {
     if (shouldSendState) {
       try {
-        // sendState() defined in MQTT aspect
+        // sendState() refined in MQTT and Influx aspect
         sendState();
       } catch (Exception e) {
         logger.catching(e);
       }
     }
-    if (hasObserver()) {
-      getObserver().apply();
+    if (hasItemObserver()) {
+      getItemObserver().apply();
+    }
+    for (Item controlled : getControllingList()) {
+      doUpdateFor(controlled);
+    }
+  }
+
+  //--- sendState ---
+  protected void Item.sendState() throws Exception {
+    for (MachineLearningModel model : getRelevantInMachineLearningModels()) {
+      model.getEncoder().newData(Collections.singletonList(this));
     }
   }
 
@@ -201,6 +328,9 @@ aspect ItemHandling {
   }
 
   public boolean ItemWithStringState.stateEquals(Object otherState) {
+    if (getState() == null) {
+      return otherState == null;
+    }
     return getState().equals(otherState);
   }
 
@@ -212,18 +342,88 @@ aspect ItemHandling {
   }
 
   public boolean ColorItem.stateEquals(Object otherState) {
+    if (getState() == null) {
+      return otherState == null;
+    }
     return getState().equals(otherState);
   }
 
   public boolean DateTimeItem.stateEquals(Object otherState) {
+    if (getState() == null) {
+      return otherState == null;
+    }
     return getState().equals(otherState);
   }
 
+  //--- setStateToDefault ---
   public abstract void Item.setStateToDefault();
   public void ColorItem.setStateToDefault() { this.setState(TupleHSB.of(0, 0, 0)); }
-  public void DateTimeItem.setStateToDefault() { this.setState(new Date(0)); }
+  public void DateTimeItem.setStateToDefault() { this.setState(Instant.ofEpochSecond(0)); }
   public void ItemWithBooleanState.setStateToDefault() { this.setState(false); }
   public void ItemWithDoubleState.setStateToDefault() { this.setState(0.0); }
   public void ItemWithStringState.setStateToDefault() { this.setState(""); }
 
+  //--- as$ItemType ---
+  // those attributes will raise a ClassCastException if called on the wrong item type. But what else can we do?
+  syn ColorItem Item.asColorItem() = (ColorItem) this;
+  syn ColorItem ColorItem.asColorItem() = this;
+  syn ItemWithBooleanState Item.asItemWithBooleanState() = (ItemWithBooleanState) this;
+  syn ItemWithBooleanState ItemWithBooleanState.asItemWithBooleanState() = this;
+  syn ItemWithStringState Item.asItemWithStringState() = (ItemWithStringState) this;
+  syn ItemWithStringState ItemWithStringState.asItemWithStringState() = this;
+  syn ItemWithDoubleState Item.asItemWithDoubleState() = (ItemWithDoubleState) this;
+  syn ItemWithDoubleState ItemWithDoubleState.asItemWithDoubleState() = this;
+  syn DateTimeItem Item.asDateTimeItem() = (DateTimeItem) this;
+  syn DateTimeItem DateTimeItem.asDateTimeItem() = this;
+
+  //--- doUpdateFor ---
+  protected abstract void Item.doUpdateFor(Item controlling);
+  protected void ItemWithBooleanState.doUpdateFor(Item controlling) {
+    controlling.setStateFromBoolean(this.getState());
+  }
+  protected void ItemWithStringState.doUpdateFor(Item controlling) {
+    controlling.setStateFromString(this.getState());
+  }
+  protected void ItemWithDoubleState.doUpdateFor(Item controlling) {
+    controlling.setStateFromDouble(this.getState());
+  }
+  protected void ColorItem.doUpdateFor(Item controlling) {
+    controlling.setStateFromColor(this.getState());
+  }
+  protected void DateTimeItem.doUpdateFor(Item controlling) {
+    controlling.setStateFromInstant(this.getState());
+  }
+
+  private void ColorItem.setBrightness(int value) {
+    setState(getState().withDifferentBrightness(value));
+  }
+
+  //--- ItemPreference.apply ---
+  public abstract void ItemPreference.apply();
+  public void ItemPreferenceColor.apply() {
+    logger.debug("Apply color preference {} -> {}", getItem().getID(), getPreferredHSB());
+    getItem().setStateFromColor(getPreferredHSB());
+    getItem().freeze();
+    for (Item controller : getItem().getControlledByList()) {
+      controller.setStateFromColor(getPreferredHSB());
+    }
+    getItem().unfreeze();
+  }
+  public void ItemPreferenceDouble.apply() {
+    logger.debug("Apply double preference {} -> {}", getItem().getID(), getPreferredValue());
+    getItem().setStateFromDouble(getPreferredValue());
+    getItem().freeze();
+    for (Item controller : getItem().getControlledByList()) {
+      controller.setStateFromDouble(getPreferredValue());
+    }
+    getItem().unfreeze();
+  }
+
+//  // override Item.init$Children from JastAdd's own ASTNode aspect
+//  refine ASTNode public void Item.init$Children() {
+//    refined();
+//    setDefaultShouldSendState(true);
+//  }
+
+
 }
diff --git a/eraser-base/src/main/jastadd/ItemHistory.jrag b/eraser-base/src/main/jastadd/ItemHistory.jrag
index aff6c228e6c2b14d3fb982997559b83e33abbae0..02f226f8d2a7c59ed7ad5d2d383609096af9d049 100644
--- a/eraser-base/src/main/jastadd/ItemHistory.jrag
+++ b/eraser-base/src/main/jastadd/ItemHistory.jrag
@@ -96,7 +96,7 @@ aspect ItemHistory {
   }
 
   // override Item.sendState from MQTT aspect
-  refine MQTT public void Item.sendState() throws Exception {
+  refine MQTT protected void Item.sendState() throws Exception {
     refined();
     getRoot().getInfluxRoot().influxAdapter().write(pointFromState());
   }
diff --git a/eraser-base/src/main/jastadd/ItemHistoryPoints.jadd b/eraser-base/src/main/jastadd/ItemHistoryPoints.jadd
index f349743611f46e326a5514ae0ba3418653c70fc0..dcb050d0c8e09887a0809689ddfaa0a854d1edef 100644
--- a/eraser-base/src/main/jastadd/ItemHistoryPoints.jadd
+++ b/eraser-base/src/main/jastadd/ItemHistoryPoints.jadd
@@ -8,8 +8,8 @@ aspect ItemHistory{
           .build();
     }
     protected abstract org.influxdb.dto.Point.Builder createMeasurement();
-    public abstract java.time.Instant getTime();
-    public abstract void setTime(java.time.Instant time);
+    public abstract Instant getTime();
+    public abstract void setTime(Instant time);
     public abstract String getId();
     public abstract void setId(String id);
     public abstract T getState();
@@ -23,18 +23,18 @@ aspect ItemHistory{
 
   @org.influxdb.annotation.Measurement(name = BooleanStatePoint.NAME)
   public class BooleanStatePoint extends AbstractItemPoint<Boolean> {
-    @org.influxdb.annotation.Column(name = "time") protected java.time.Instant time;
+    @org.influxdb.annotation.Column(name = "time") protected Instant time;
     @org.influxdb.annotation.Column(name = "state") protected Boolean state;
     @org.influxdb.annotation.Column(name = "id", tag = true) protected String id;
-    public java.time.Instant getTime() { return time; }
-    public void setTime(java.time.Instant time) { this.time = time; }
+    public Instant getTime() { return time; }
+    public void setTime(Instant time) { this.time = time; }
     public String getId() { return id; }
     public void setId(String id) { this.id = id; }
     public void setState(Boolean state) { this.state = state; }
     public Boolean getState() { return state; }
 
     public static final String NAME = "ItemB";
-    public static BooleanStatePoint of(java.time.Instant time, Boolean state, String id) {
+    public static BooleanStatePoint of(Instant time, Boolean state, String id) {
       BooleanStatePoint result = new BooleanStatePoint();
       result.setTime(time); result.setState(state); result.setId(id);
       return result;
@@ -46,18 +46,18 @@ aspect ItemHistory{
 
   @org.influxdb.annotation.Measurement(name = StringStatePoint.NAME)
   public class StringStatePoint extends AbstractItemPoint<String> {
-    @org.influxdb.annotation.Column(name = "time") protected java.time.Instant time;
+    @org.influxdb.annotation.Column(name = "time") protected Instant time;
     @org.influxdb.annotation.Column(name = "state") protected String state;
     @org.influxdb.annotation.Column(name = "id", tag = true) protected String id;
-    public java.time.Instant getTime() { return time; }
-    public void setTime(java.time.Instant time) { this.time = time; }
+    public Instant getTime() { return time; }
+    public void setTime(Instant time) { this.time = time; }
     public String getId() { return id; }
     public void setId(String id) { this.id = id; }
     public void setState(String state) { this.state = state; }
     public String getState() { return state; }
 
     public static final String NAME = "ItemS";
-    public static StringStatePoint of(java.time.Instant time, String state, String id) {
+    public static StringStatePoint of(Instant time, String state, String id) {
       StringStatePoint result = new StringStatePoint();
       result.setTime(time); result.setState(state); result.setId(id);
       return result;
@@ -69,18 +69,18 @@ aspect ItemHistory{
 
   @org.influxdb.annotation.Measurement(name = DoubleStatePoint.NAME)
   public class DoubleStatePoint extends AbstractItemPoint<Double> {
-    @org.influxdb.annotation.Column(name = "time") protected java.time.Instant time;
+    @org.influxdb.annotation.Column(name = "time") protected Instant time;
     @org.influxdb.annotation.Column(name = "state") protected Double state;
     @org.influxdb.annotation.Column(name = "id", tag = true) protected String id;
-    public java.time.Instant getTime() { return time; }
-    public void setTime(java.time.Instant time) { this.time = time; }
+    public Instant getTime() { return time; }
+    public void setTime(Instant time) { this.time = time; }
     public String getId() { return id; }
     public void setId(String id) { this.id = id; }
     public void setState(Double state) { this.state = state; }
     public Double getState() { return state; }
 
     public static final String NAME = "ItemD";
-    public static DoubleStatePoint of(java.time.Instant time, Double state, String id) {
+    public static DoubleStatePoint of(Instant time, Double state, String id) {
       DoubleStatePoint result = new DoubleStatePoint();
       result.setTime(time); result.setState(state); result.setId(id);
       return result;
@@ -92,18 +92,18 @@ aspect ItemHistory{
 
   @org.influxdb.annotation.Measurement(name = ColorStatePoint.NAME)
   public class ColorStatePoint extends AbstractItemPoint<TupleHSB> {
-    @org.influxdb.annotation.Column(name = "time") protected java.time.Instant time;
+    @org.influxdb.annotation.Column(name = "time") protected Instant time;
     @org.influxdb.annotation.Column(name = "state") protected String state;
     @org.influxdb.annotation.Column(name = "id", tag = true) protected String id;
-    public java.time.Instant getTime() { return time; }
-    public void setTime(java.time.Instant time) { this.time = time; }
+    public Instant getTime() { return time; }
+    public void setTime(Instant time) { this.time = time; }
     public String getId() { return id; }
     public void setId(String id) { this.id = id; }
     public void setState(TupleHSB state) { this.state = state.toString(); }
     public TupleHSB getState() { return TupleHSB.parse(state); }
 
     public static final String NAME = "ItemC";
-    public static ColorStatePoint of(java.time.Instant time, TupleHSB state, String id) {
+    public static ColorStatePoint of(Instant time, TupleHSB state, String id) {
       ColorStatePoint result = new ColorStatePoint();
       result.setTime(time); result.setState(state); result.setId(id);
       return result;
@@ -114,19 +114,19 @@ aspect ItemHistory{
   }
 
   @org.influxdb.annotation.Measurement(name = DateTimeStatePoint.NAME)
-  public class DateTimeStatePoint extends AbstractItemPoint<Date> {
-    @org.influxdb.annotation.Column(name = "time") protected java.time.Instant time;
+  public class DateTimeStatePoint extends AbstractItemPoint<Instant> {
+    @org.influxdb.annotation.Column(name = "time") protected Instant time;
     @org.influxdb.annotation.Column(name = "state") protected long state;
     @org.influxdb.annotation.Column(name = "id", tag = true) protected String id;
-    public java.time.Instant getTime() { return time; }
-    public void setTime(java.time.Instant time) { this.time = time; }
+    public Instant getTime() { return time; }
+    public void setTime(Instant time) { this.time = time; }
     public String getId() { return id; }
     public void setId(String id) { this.id = id; }
-    public void setState(Date state) { this.state = state.getTime(); }
-    public Date getState() { return new Date(state); }
+    public void setState(Instant state) { this.state = state.toEpochMilli(); }
+    public Instant getState() { return Instant.ofEpochMilli(state); }
 
     public static final String NAME = "ItemT";
-    public static DateTimeStatePoint of(java.time.Instant time, Date state, String id) {
+    public static DateTimeStatePoint of(Instant time, Instant state, String id) {
       DateTimeStatePoint result = new DateTimeStatePoint();
       result.setTime(time); result.setState(state); result.setId(id);
       return result;
diff --git a/eraser-base/src/main/jastadd/Location.jrag b/eraser-base/src/main/jastadd/Location.jrag
new file mode 100644
index 0000000000000000000000000000000000000000..b61b260c998e15a89712e00f3e2c2a44eb84759b
--- /dev/null
+++ b/eraser-base/src/main/jastadd/Location.jrag
@@ -0,0 +1,10 @@
+aspect Location {
+  syn Optional<Location> Item.myLocation() {
+    if (this.hasLocation()) {
+      return Optional.of(this.getLocation());
+    } else {
+      return JavaUtils.ifPresentOrElseReturn(linkedThing(),
+          thing -> thing.hasLocation() ? Optional.of(thing.getLocation()) : Optional.empty(), () -> Optional.empty());
+    }
+  }
+}
diff --git a/eraser-base/src/main/jastadd/Location.relast b/eraser-base/src/main/jastadd/Location.relast
new file mode 100644
index 0000000000000000000000000000000000000000..3cdf199776adc6eaa575b31f867a16bae891e8d5
--- /dev/null
+++ b/eraser-base/src/main/jastadd/Location.relast
@@ -0,0 +1,3 @@
+Location ::= <Label:String> SubLocation:Location ;
+rel Location.Thing* <-> Thing.Location? ;
+rel Location.Item* <-> Item.Location? ;
diff --git a/eraser-base/src/main/jastadd/Logging.jadd b/eraser-base/src/main/jastadd/Logging.jadd
index 3fbe1ab40fdd10105f03ba1730e6c13e3b0ac399..710bd79a7bce048213ce0d52bb42cae5176b832a 100644
--- a/eraser-base/src/main/jastadd/Logging.jadd
+++ b/eraser-base/src/main/jastadd/Logging.jadd
@@ -1,6 +1,21 @@
 aspect Logging {
-  private org.apache.logging.log4j.Logger MqttRoot.logger = org.apache.logging.log4j.LogManager.getLogger(MqttRoot.class);
-  private org.apache.logging.log4j.Logger Event.logger = org.apache.logging.log4j.LogManager.getLogger(Event.class);
+  // Base
   protected org.apache.logging.log4j.Logger Item.logger = org.apache.logging.log4j.LogManager.getLogger(Item.class);
+  protected org.apache.logging.log4j.Logger ItemPreference.logger = org.apache.logging.log4j.LogManager.getLogger(ItemPreference.class);
+
+  // MachineLearning
+  private org.apache.logging.log4j.Logger DummyMachineLearningModel.logger = org.apache.logging.log4j.LogManager.getLogger(DummyMachineLearningModel.class);
+  private org.apache.logging.log4j.Logger InternalMachineLearningModel.logger = org.apache.logging.log4j.LogManager.getLogger(InternalMachineLearningModel.class);
+  private org.apache.logging.log4j.Logger ExternalMachineLearningModel.logger = org.apache.logging.log4j.LogManager.getLogger(ExternalMachineLearningModel.class);
+
+  // NeuralNetwork
+  private org.apache.logging.log4j.Logger NeuralNetworkRoot.logger = org.apache.logging.log4j.LogManager.getLogger(NeuralNetworkRoot.class);
+  private org.apache.logging.log4j.Logger OutputLayer.logger = org.apache.logging.log4j.LogManager.getLogger(OutputLayer.class);
   protected org.apache.logging.log4j.Logger Neuron.logger = org.apache.logging.log4j.LogManager.getLogger(Neuron.class);
+
+  // Rules
+  private org.apache.logging.log4j.Logger Rule.logger = org.apache.logging.log4j.LogManager.getLogger(Rule.class);
+
+  // MQTT
+  private org.apache.logging.log4j.Logger MqttRoot.logger = org.apache.logging.log4j.LogManager.getLogger(MqttRoot.class);
 }
diff --git a/eraser-base/src/main/jastadd/MachineLearning.jrag b/eraser-base/src/main/jastadd/MachineLearning.jrag
index 6a69f5c89a60695d10ae9ec0554ca37f827abf17..e584d8f2f6b9f1b2b0ce140a46c005b32887e1dd 100644
--- a/eraser-base/src/main/jastadd/MachineLearning.jrag
+++ b/eraser-base/src/main/jastadd/MachineLearning.jrag
@@ -1,41 +1,193 @@
 aspect MachineLearning {
 
+  public static final MachineLearningRoot MachineLearningRoot.createDefault() {
+    MachineLearningRoot result = new MachineLearningRoot();
+    return result;
+  }
+
   public interface Leaf {
     String getLabel();
     int getActivityIdentifier();
     List<ItemPreference> computePreferences();
   }
 
-  syn Leaf MachineLearningModel.classify();
+  syn Leaf InternalMachineLearningModel.classify();
 
   //--- currentActivityName ---
-  syn String Root.currentActivityName() = getMachineLearningRoot().hasActivityRecognition() ? getMachineLearningRoot().getActivityRecognition().classify().getLabel() : "no activity";
+  syn String Root.currentActivityName() = JavaUtils.ifPresentOrElseReturn(
+      currentActivity(),
+      Activity::getLabel,
+      () -> "no activity"
+    );
 
   //--- currentActivity ---
-  syn java.util.Optional<Activity> Root.currentActivity() = resolveActivity(getMachineLearningRoot().hasActivityRecognition() ? getMachineLearningRoot().getActivityRecognition().classify().getActivityIdentifier() : -1);
+  syn java.util.Optional<Activity> Root.currentActivity() {
+    return resolveActivity((int) getOpenHAB2Model().getActivityItem().getState());
+  }
+  private int Root.extractActivityIdentifier(List<ItemPreference> preferences) {
+    if (preferences.isEmpty()) {
+      return -1;
+    }
+    return (int) ((ItemPreferenceDouble) preferences.get(0)).getPreferredValue();
+  }
 
   //--- currentPreferences ---
-  syn List<ItemPreference> Root.currentPreferences() = getMachineLearningRoot().getPreferenceLearning().classify().computePreferences();
-
-  //--- computePreferences ---
-  syn List<ItemPreference> DecisionTreeLeaf.computePreferences() {
-    // iterate over preference of this leaf, and all its parents and ancestors
-    List<ItemPreference> result = new ArrayList<>();
-    Set<Item> seenItems = new HashSet<>();
-    List<DecisionTreeElement> ancestors = ancestors();
-    for (ItemPreference pref : getPreferenceList()) {
-      result.add(pref);
-      seenItems.add(pref.getItem());
+  syn List<ItemPreference> Root.currentPreferences() = getMachineLearningRoot().getPreferenceLearning().getDecoder().classify().getPreferences();
+
+  //--- canSetActivity ---
+  syn boolean MachineLearningModel.canSetActivity() = false;
+  eq DummyMachineLearningModel.canSetActivity() = true;
+
+  //--- setActivity ---
+  public void MachineLearningModel.setActivity(int activityIdentifier) { /* empty by default */ }
+  public void DummyMachineLearningModel.setActivity(int activityIdentifier) {
+    getCurrent().setActivityIdentifier(activityIdentifier);
+  }
+
+  //--- DummyMachineLearningModel.classify ---
+  eq DummyMachineLearningModel.classify() {
+    if (logger.isInfoEnabled() && getItemList().size() > 0) {
+      logger.info("Dummy classification of {}, values of connected items: {}",
+          mlKind(),
+          getItemList().stream()
+              .map(item -> item.getID() + ":" + item.getStateAsString())
+              .collect(java.util.stream.Collectors.toList()));
     }
-    for (DecisionTreeElement ancestor : ancestors) {
-      for (ItemPreference pref : ancestor.getPreferenceList()) {
-        if (!seenItems.contains(pref.getItem())) {
-          result.add(pref);
-          seenItems.add(pref.getItem());
-        }
+    return getCurrent();
+  }
+
+  //--- DummyMachineLearningModel.createDefault() ---
+  public static DummyMachineLearningModel DummyMachineLearningModel.createDefault() {
+    DummyMachineLearningModel dummy = new DummyMachineLearningModel();
+    DecisionTreeLeaf current = new DecisionTreeLeaf();
+    current.setActivityIdentifier(0);
+    current.setLabel("Dummy");
+    // no item preference set
+    dummy.setCurrent(current);
+    return dummy;
+  }
+
+  //--- connectItems ---
+  public abstract void MachineLearningModel.connectItems(List<String> itemNames);
+  public void DummyMachineLearningModel.connectItems(List<String> itemNames) {
+    logger.info("Storing items to connect");
+    for (String itemName : itemNames) {
+      JavaUtils.ifPresentOrElse(getRoot().getOpenHAB2Model().resolveItem(itemName),
+          this::addItem,
+          () -> logger.warn("Could not resolve item '{}'", itemName));
+    }
+  }
+
+  //--- check ---
+  /**
+   * Checks the ML model for all necessary children.
+   * @return true, if everything is alright. false otherwise
+   */
+  public abstract boolean MachineLearningModel.check();
+
+  @Override
+  public boolean InternalMachineLearningModel.check() {
+    boolean good = true;
+    if (getOutputApplication() == null) {
+      logger.warn("{}: OutputApplication function is null!", mlKind());
+      good = false;
+    }
+    return good;
+  }
+
+  @Override
+  public boolean DummyMachineLearningModel.check() {
+    // ignore definition of MachineLearningModel, thus ignore output application
+    boolean good = true;
+    if (getCurrent() == null) {
+      logger.error("{}: Current is null!", mlKind());
+      good = false;
+    } else {
+      if (getCurrent().getNumPreference() == 0) {
+        logger.warn("{}: No item preferences set.", mlKind());
       }
     }
+    return good;
+  }
+
+  @Override
+  public boolean ExternalMachineLearningModel.check() {
+    logger.warn("check not available for external ML models (yet)!");
+    return true;
+  }
+
+  //--- mlKind ---
+  inh String MachineLearningModel.mlKind();
+  eq MachineLearningRoot.getActivityRecognition().mlKind() = "ActivityRecognition";
+  eq MachineLearningRoot.getPreferenceLearning().mlKind() = "PreferenceLearning";
+
+  //... ExternalMachineLearningModel ...
+  private MachineLearningEncoder ExternalMachineLearningModel.encoder;
+  public void ExternalMachineLearningModel.setEncoder(MachineLearningEncoder encoder) {
+    this.encoder = encoder;
+  }
+  private MachineLearningDecoder ExternalMachineLearningModel.decoder;
+  public void ExternalMachineLearningModel.setDecoder(MachineLearningDecoder decoder) {
+    this.decoder = decoder;
+  }
+//  eq ExternalMachineLearningModel.classify() = null;
+  public void ExternalMachineLearningModel.connectItems(List<String> itemNames) { }
+
+  //... InternalMachineLearningModel ...
+  syn InternalMachineLearningHandler InternalMachineLearningModel.handler() {
+    return new InternalMachineLearningHandler().setModel(this);
+  }
+  cache InternalMachineLearningModel.handler();
+
+  //--- getEncoder ---
+  public abstract MachineLearningEncoder MachineLearningModel.getEncoder();
+  @Override
+  public MachineLearningEncoder InternalMachineLearningModel.getEncoder() {
+    return handler();
+  }
+  @Override
+  public MachineLearningEncoder ExternalMachineLearningModel.getEncoder() {
+    return this.encoder;
+  }
+
+  //--- getDecoder ---
+  public abstract MachineLearningDecoder MachineLearningModel.getDecoder();
+  @Override
+  public MachineLearningDecoder InternalMachineLearningModel.getDecoder() {
+    return handler();
+  }
+  @Override
+  public MachineLearningDecoder ExternalMachineLearningModel.getDecoder() {
+    return this.decoder;
+  }
+
+}
+
+aspect ChangeEvents {
+  private static final java.util.concurrent.atomic.AtomicInteger ChangeEvent.idCounter = new java.util.concurrent.atomic.AtomicInteger(0);
+
+  //--- createRecognitionEvent ---
+  public static RecognitionEvent RecognitionEvent.createRecognitionEvent(MachineLearningModel modelOfRecognition) {
+    RecognitionEvent result = new RecognitionEvent();
+    result.initChangeEvent();
+    for (Item relevantItem : modelOfRecognition.getRelevantItems()) {
+      result.addChangedItem(ChangedItem.newFromItem(relevantItem));
+    }
     return result;
   }
 
+  //--- newFromItem ---
+  public static ChangedItem ChangedItem.newFromItem(Item source) {
+    ChangedItem result = new ChangedItem();
+    result.setItem(source);
+    result.setNewStateAsString(source.getStateAsString());
+    return result;
+  }
+
+  //--- initChangeEvent ---
+  protected void ChangeEvent.initChangeEvent() {
+    this.setCreated(Instant.now());
+    this.setIdentifier(idCounter.incrementAndGet());
+  }
+
 }
diff --git a/eraser-base/src/main/jastadd/MachineLearning.relast b/eraser-base/src/main/jastadd/MachineLearning.relast
new file mode 100644
index 0000000000000000000000000000000000000000..906d344b8f123fca5d617b174078dcfa56f7f4c8
--- /dev/null
+++ b/eraser-base/src/main/jastadd/MachineLearning.relast
@@ -0,0 +1,28 @@
+// ----------------    Machine Learning Model    ------------------------------
+MachineLearningRoot ::= [ActivityRecognition:MachineLearningModel] [PreferenceLearning:MachineLearningModel] Activity* ChangeEvent* ;
+
+Activity ::= <Identifier:int> <Label:String> ;
+
+abstract ChangeEvent ::= <Identifier:int> <Created:Instant> ChangedItem* ;
+
+ChangedItem ::= <NewStateAsString:String> ;
+rel ChangedItem.Item -> Item ;
+
+RecognitionEvent : ChangeEvent ;
+rel RecognitionEvent.Activity -> Activity ;
+
+ManualChangeEvent : ChangeEvent ;
+
+abstract MachineLearningModel ::= ;
+rel MachineLearningModel.RelevantItem* <-> Item.RelevantInMachineLearningModel* ;
+rel MachineLearningModel.TargetItem* <-> Item.TargetInMachineLearningModel* ;
+
+ExternalMachineLearningModel : MachineLearningModel ;
+
+abstract InternalMachineLearningModel : MachineLearningModel ::= <OutputApplication:DoubleDoubleFunction> ;
+
+abstract ItemPreference ::= ;
+rel ItemPreference.Item -> Item ;
+
+ItemPreferenceColor : ItemPreference ::= <PreferredHSB:TupleHSB> ;
+ItemPreferenceDouble : ItemPreference ::= <PreferredValue:double> ;
diff --git a/eraser-base/src/main/jastadd/ModelStatistics.jrag b/eraser-base/src/main/jastadd/ModelStatistics.jrag
index 293fcd920a69bbdf3393add6fd62c451d0abe87a..443635f1e2f3ceb71785e5977d43ba27f55c2b6c 100644
--- a/eraser-base/src/main/jastadd/ModelStatistics.jrag
+++ b/eraser-base/src/main/jastadd/ModelStatistics.jrag
@@ -1,7 +1,7 @@
 aspect ModelStatistics {
 
   //--- numChannels ---
-  syn int Root.numChannels() {
+  syn int OpenHAB2Model.numChannels() {
     int sum = 0;
     for (Thing thing : getThingList()) {
       sum += thing.getNumChannel();
@@ -10,7 +10,7 @@ aspect ModelStatistics {
   }
 
   //--- description ---
-  syn String Root.description() = "["
+  syn String OpenHAB2Model.description() = "["
     + this.getNumThingType() + " thing type(s), "
     + this.getNumChannelType() + " channel type(s), "
     + this.numChannels() + " channel(s), "
diff --git a/eraser-base/src/main/jastadd/Navigation.jrag b/eraser-base/src/main/jastadd/Navigation.jrag
index b1025c8092e3a0989dca556f1dcbd855c30f64f9..69592c1bf0b980b6287cb28d10b7b131bd4383ef 100644
--- a/eraser-base/src/main/jastadd/Navigation.jrag
+++ b/eraser-base/src/main/jastadd/Navigation.jrag
@@ -1,36 +1,36 @@
 aspect Navigation {
 
-  syn Comparator<ModelElement> Root.modelElementComparator() {
+  syn Comparator<ModelElement> OpenHAB2Model.modelElementComparator() {
     return (e1, e2) -> (e1.getID().compareTo(e2.getID()));
   }
 
   //--- items ---
-  syn java.util.List<Item> Root.items() {
+  syn java.util.List<Item> OpenHAB2Model.items() {
     java.util.List<Item> result = new java.util.ArrayList<>();
     addItems(result, getGroupList());
     return result;
   }
 
-  private void Root.addItems(java.util.List<Item> result, JastAddList<Group> groups) {
+  private void OpenHAB2Model.addItems(java.util.List<Item> result, JastAddList<Group> groups) {
     groups.forEach(group -> group.getItemList().forEach(item -> result.add(item)));
   }
 
   //--- parameters ---
-  syn java.util.Set<Parameter> Root.parameters() {
+  syn java.util.Set<Parameter> OpenHAB2Model.parameters() {
     java.util.Set<Parameter> result = new java.util.TreeSet<>(modelElementComparator());
     getThingTypeList().forEach(tt -> tt.getParameterList().forEach(parameter -> result.add(parameter)));
     return result;
   }
 
   //--- channels ---
-  syn java.util.Set<Channel> Root.channels() {
+  syn java.util.Set<Channel> OpenHAB2Model.channels() {
     java.util.Set<Channel> result = new java.util.TreeSet<>(modelElementComparator());
     getThingList().forEach(thing -> thing.getChannelList().forEach(channel -> result.add(channel)));
     return result;
   }
 
   //--- resolveThingType ---
-  syn java.util.Optional<ThingType> Root.resolveThingType(String thingTypeId) {
+  syn java.util.Optional<ThingType> OpenHAB2Model.resolveThingType(String thingTypeId) {
     for (ThingType thingType : this.getThingTypeList()) {
       if (thingType.getID().equals(thingTypeId)) {
         return java.util.Optional.of(thingType);
@@ -40,7 +40,7 @@ aspect Navigation {
   }
 
   //--- resolveChannel ---
-  syn java.util.Optional<Channel> Root.resolveChannel(String channelId) {
+  syn java.util.Optional<Channel> OpenHAB2Model.resolveChannel(String channelId) {
     for (Thing thing : this.getThingList()) {
       for (Channel channel : thing.getChannelList()) {
         if (channel.getID().equals(channelId)) {
@@ -52,7 +52,7 @@ aspect Navigation {
   }
 
   //--- resolveChannelType ---
-  syn java.util.Optional<ChannelType> Root.resolveChannelType(String channelTypeId) {
+  syn java.util.Optional<ChannelType> OpenHAB2Model.resolveChannelType(String channelTypeId) {
     for (ChannelType channelType : this.getChannelTypeList()) {
       if (channelType.getID().equals(channelTypeId)) {
         return java.util.Optional.of(channelType);
@@ -62,7 +62,10 @@ aspect Navigation {
   }
 
   //--- resolveItem ---
-  syn java.util.Optional<Item> Root.resolveItem(String itemId) {
+  syn java.util.Optional<Item> OpenHAB2Model.resolveItem(String itemId) {
+    if ("activity".equals(itemId)) {
+      return Optional.of(getActivityItem());
+    }
     for (Item item : items()) {
       if (item.getID().equals(itemId)) {
         return java.util.Optional.of(item);
@@ -72,7 +75,7 @@ aspect Navigation {
   }
 
   //--- resolveGroup ---
-  syn java.util.Optional<Group> Root.resolveGroup(String groupId) {
+  syn java.util.Optional<Group> OpenHAB2Model.resolveGroup(String groupId) {
     for (Group group : this.getGroupList()) {
       if (group.getID().equals(groupId)) {
         return java.util.Optional.of(group);
@@ -87,7 +90,7 @@ aspect Navigation {
   }
 
   //--- resolveItemCategory ---
-  syn java.util.Optional<ItemCategory> Root.resolveItemCategory(String categoryName) {
+  syn java.util.Optional<ItemCategory> OpenHAB2Model.resolveItemCategory(String categoryName) {
     for (ItemCategory category : getItemCategoryList()) {
       if (category.getName().equals(categoryName)) {
         return java.util.Optional.of(category);
@@ -116,26 +119,31 @@ aspect Navigation {
     return java.util.Optional.empty();
   }
 
-  //--- containingChannel ---
-  inh Channel Link.containingChannel();
-  eq Channel.getLink().containingChannel() = this;
-
   //--- containingThing ---
   inh Thing Channel.containingThing();
   eq Thing.getChannel().containingThing() = this;
 
+  //--- containingNeuralNetwork ---
+  inh NeuralNetworkRoot OutputLayer.containingNeuralNetwork();
+  eq NeuralNetworkRoot.getOutputLayer().containingNeuralNetwork() = this;
+
+  //--- linkedThing ---
+  syn Optional<Thing> Item.linkedThing() {
+    if (!this.hasChannel()) {
+      return Optional.empty();
+    }
+    Channel channel = this.getChannel();
+    Thing thing = channel.containingThing();
+    return Optional.of(thing);
+  }
+
   //--- getRoot ---
   inh Root ASTNode.getRoot();
-  eq Root.getChannelCategory().getRoot() = this;
+  eq Root.getOpenHAB2Model().getRoot() = this;
   eq Root.getMqttRoot().getRoot() = this;
   eq Root.getInfluxRoot().getRoot() = this;
   eq Root.getMachineLearningRoot().getRoot() = this;
-  eq Root.getItemObserver().getRoot() = this;
-  eq Root.getThing().getRoot() = this;
-  eq Root.getGroup().getRoot() = this;
-  eq Root.getThingType().getRoot() = this;
-  eq Root.getChannelType().getRoot() = this;
   eq Root.getRule().getRoot() = this;
-  eq Root.getItemCategory().getRoot() = this;
   eq Root.getUser().getRoot() = this;
+  eq Root.getLocation().getRoot() = this;
 }
diff --git a/eraser-base/src/main/jastadd/NeuralNetwork.jrag b/eraser-base/src/main/jastadd/NeuralNetwork.jrag
index c075af0c8d50be1842ad340cc2063dd8e49b3ab5..e673b69a1ea95c386320630fae1c8152cfe9cf78 100644
--- a/eraser-base/src/main/jastadd/NeuralNetwork.jrag
+++ b/eraser-base/src/main/jastadd/NeuralNetwork.jrag
@@ -4,11 +4,13 @@ aspect NeuralNetwork {
   // let any number be a Leaf
   public class DoubleNumber implements Leaf {
     public final double number;
-    DoubleNumber(double number) {
+    public final Item affectedItem;
+    DoubleNumber(Item affectedItem, double number) {
+      this.affectedItem = affectedItem;
       this.number = number;
     }
-    public static DoubleNumber of(double number) {
-      return new DoubleNumber(number);
+    public static DoubleNumber of(Item affectedItem, double number) {
+      return new DoubleNumber(affectedItem, number);
     }
     public String getLabel() {
       return Double.toString(this.number);
@@ -17,29 +19,33 @@ aspect NeuralNetwork {
       return (int) number;
     }
     public List<ItemPreference> computePreferences() {
-      return new ArrayList<>();
+      return Collections.singletonList(new ItemPreferenceDouble(affectedItem, number));
     }
   }
 
+  //--- classify ---
   syn DoubleNumber NeuralNetworkRoot.classify() {
     return getOutputLayer().classify();
   }
 
-//  class FormulaEvaluator {
-//    public static double evaluate(String formula, double[] inputs) {
-//
-//    }
-//  }
-
   syn DoubleNumber OutputLayer.classify() {
     double[] inputs = new double[getNumOutputNeuron()];
     for (int i = 0; i < getNumOutputNeuron(); ++i) {
       OutputNeuron n = getOutputNeuron(i);
       inputs[i] = n.value();
     }
-    return DoubleNumber.of(getCombinator().apply(inputs));
+    return DoubleNumber.of(getAffectedItem(), containingNeuralNetwork().getOutputApplication().apply(getCombinator().apply(inputs)));
   }
 
+  //--- createEmpty ---
+  public static NeuralNetworkRoot NeuralNetworkRoot.createEmpty() {
+    NeuralNetworkRoot result = new NeuralNetworkRoot();
+    // use identity function
+    result.setOutputApplication(d -> d);
+    return result;
+  }
+
+  //--- getLabel (not used) ---
   syn String OutputLayer.getLabel() {
     StringBuilder sb = new StringBuilder();
     for (OutputNeuron n : getOutputNeuronList()) {
@@ -48,26 +54,32 @@ aspect NeuralNetwork {
     return sb.toString();
   }
 
+  //--- value ---
   syn double Neuron.value();
 
-  syn double HiddenNeuron.value() {
+  eq HiddenNeuron.value() {
     double[] inputs = new double[getInputs().size()];
     for (int i=0; i<inputs.length; ++i) {
       NeuronConnection connection = getInputList().get(i);
       inputs[i] = connection.containingNeuron().value() * connection.getWeight();
+//      logger.debug("{}[{}]: {} * {}", this, i, connection.containingNeuron().value(), connection.getWeight());
     }
     double result = getActivationFormula().apply(inputs);
-    logger.debug("{}: {} -> {}", this, java.util.Arrays.toString(inputs), result);
+//    logger.debug("{}: {} -> {}", this, java.util.Arrays.toString(inputs), result);
     return result;
-    }
+  }
+
+  eq BiasNeuron.value() = 1;
 
-  syn double InputNeuron.value() {
+  eq InputNeuron.value() {
     return getItem().getStateAsDouble();
   }
 
+  //--- containingNeuron ---
   inh Neuron NeuronConnection.containingNeuron();
   eq Neuron.getOutput().containingNeuron() = this;
 
+  //--- connectTo ---
   public void Neuron.connectTo(Neuron otherNeuron, double weight) {
     NeuronConnection connection = new NeuronConnection();
     connection.setWeight(weight);
@@ -75,4 +87,94 @@ aspect NeuralNetwork {
     this.addOutput(connection);
   }
 
+  //--- connectItems ---
+  public void NeuralNetworkRoot.connectItems(List<String> itemNames) {
+    int numItems = itemNames.size();
+    if (numItems > getNumInputNeuron()) {
+      logger.warn("Excessive item names (total: {}) specfied to connect {} input neurons.", numItems, getNumInputNeuron());
+    }
+    for (int i = 0; i < getNumInputNeuron(); ++i) {
+      if (i >= numItems) {
+        logger.error("Not enough item names specified to connect all input neurons.");
+        return;
+      }
+      String itemName = itemNames.get(i);
+      InputNeuron neuron = getInputNeuron(i);
+      JavaUtils.ifPresentOrElse(getRoot().getOpenHAB2Model().resolveItem(itemName),
+          neuron::setItem,
+          () -> logger.warn("Could not resolve item '{}'", itemName));
+    }
+  }
+
+  //--- check ---
+  @Override
+  public boolean NeuralNetworkRoot.check() {
+    boolean good = super.check();
+    if (getNumInputNeuron() == 0) {
+      logger.warn("{}: There are no input neurons.", mlKind());
+    }
+    if (getNumHiddenNeuron() == 0) {
+      logger.warn("{}: There are no hidden neurons.", mlKind());
+    }
+    for (InputNeuron inputNeuron : getInputNeuronList()) {
+      good &= inputNeuron.check();
+    }
+    for (HiddenNeuron hiddenNeuron : getHiddenNeuronList()) {
+      good &= hiddenNeuron.check();
+    }
+    good &= getOutputLayer().check();
+    return good;
+  }
+
+  public boolean OutputLayer.check() {
+    boolean good = true;
+    if (getCombinator() == null) {
+      logger.error("{}: There is no combinator function.", mlKind());
+      good = false;
+    }
+    for (OutputNeuron outputNeuron : getOutputNeuronList()) {
+      good &= outputNeuron.check();
+    }
+    return good;
+  }
+
+  public boolean Neuron.check() {
+    for (NeuronConnection outputConnection : getOutputList()) {
+      if (outputConnection.getWeight() == 0) {
+        logger.warn("{}: Connection from {} to {} has weight zero.", mlKind(), outputConnection.getNeuron(), this);
+      }
+    }
+    return true;
+  }
+
+  @Override
+  public boolean InputNeuron.check() {
+    boolean good = super.check();
+    if (getItem() == null) {
+      logger.error("{}: No item connected to {}.", mlKind(), this);
+      return false;
+    }
+    return good;
+  }
+
+  @Override
+  public boolean HiddenNeuron.check() {
+    boolean good = super.check();
+    if (getActivationFormula() == null) {
+      logger.error("{}: Neuron {} has no activation function.", mlKind(), this);
+      good = false;
+    }
+    return good;
+  }
+
+  @Override
+  public boolean BiasNeuron.check() {
+    setActivationFormula(inputs -> 1.0);
+    return super.check();
+  }
+
+  //--- mlKind ---
+  inh String OutputLayer.mlKind();
+  inh String Neuron.mlKind();
+
 }
diff --git a/eraser-base/src/main/jastadd/NeuralNetwork.relast b/eraser-base/src/main/jastadd/NeuralNetwork.relast
new file mode 100644
index 0000000000000000000000000000000000000000..1214eb199e7ae39e9bae4754eabe6ee0fcd14ae3
--- /dev/null
+++ b/eraser-base/src/main/jastadd/NeuralNetwork.relast
@@ -0,0 +1,20 @@
+// ----------------    Neural Network    ------------------------------
+NeuralNetworkRoot : InternalMachineLearningModel ::= InputNeuron* HiddenNeuron* OutputLayer ;
+
+OutputLayer ::= OutputNeuron* <Combinator:DoubleArrayDoubleFunction> ;
+rel OutputLayer.AffectedItem -> Item ;
+
+abstract Neuron ::= Output:NeuronConnection* ;
+
+NeuronConnection ::= <Weight:double> ;
+rel NeuronConnection.Neuron <-> Neuron.Input* ;
+
+InputNeuron : Neuron ;
+rel InputNeuron.Item -> Item ;
+
+HiddenNeuron : Neuron ::= <ActivationFormula:DoubleArrayDoubleFunction> ;
+BiasNeuron : HiddenNeuron ;
+OutputNeuron : HiddenNeuron ::= <Label:String> ;
+
+DummyMachineLearningModel : InternalMachineLearningModel ::= Current:DecisionTreeLeaf ;
+rel DummyMachineLearningModel.Item* -> Item ;
diff --git a/eraser-base/src/main/jastadd/Printing.jrag b/eraser-base/src/main/jastadd/Printing.jrag
index bda979a24f7aff3a2b3e6bfecf6003754acdc7a4..7402973f563eb21924f1f54a54ae8dc6d2bea848 100644
--- a/eraser-base/src/main/jastadd/Printing.jrag
+++ b/eraser-base/src/main/jastadd/Printing.jrag
@@ -3,8 +3,17 @@ aspect Printing {
 
   String ASTNode.safeID(ModelElement elem) { return elem == null ? "NULL" : elem.getID(); }
 
-//Root ::= Thing* Item* Group* ThingType* ChannelType* MqttRoot ;
   syn String Root.prettyPrint() {
+    StringBuilder sb = new StringBuilder();
+    sb.append(getOpenHAB2Model().prettyPrint());
+    sb.append(getMqttRoot().prettyPrint());
+    sb.append(getInfluxRoot().prettyPrint());
+    sb.append(getMachineLearningRoot().prettyPrint());
+    return sb.toString();
+  }
+
+//--- OpenHAB2Model.prettyPrint() ---
+  syn String OpenHAB2Model.prettyPrint() {
     StringBuilder sb = new StringBuilder();
     for (Thing t : getThingList()) {
       sb.append(t.prettyPrint());
@@ -27,71 +36,74 @@ aspect Printing {
     for (Channel c : channels()) {
       sb.append(c.prettyPrint());
     }
-    sb.append(getMqttRoot().prettyPrint());
-    sb.append(getInfluxRoot().prettyPrint());
     return sb.toString();
   }
 
-//Thing: id="id" label="label" type="type" channels=[];
+//Thing: id="" label="" type="" channels=["CHANNEL_ID", "CHANNEL_ID"] ;
   syn String Thing.prettyPrint() {
-    StringBuilder sb = new StringBuilder("Thing: id=\"");
-    sb.append(getID())
-      .append("\" label=\"").append(getLabel())
-      .append("\" type=\"").append(safeID(getType()));
-    PrinterUtils.concatIds(sb.append("\" channels=["), getChannelList());
-    return sb.append("] ;\n").toString();
+    return new MemberPrinter("Thing")
+        .addRequired("id", getID())
+        .addNonDefault("label", getLabel())
+        .addRequired("type", getType(), ThingType::getID)
+        .addIds("channels", getNumChannel(), getChannelList())
+        .build();
   }
 
-//Item: id="id" label="label" state="state" topic="topic";
+//ITEM_TYPE Item: id="" label="" state="" category="" topic="" controls=["ITEM_ID", "ITEM_ID"];
   syn String Item.prettyPrint() {
-    StringBuilder sb = new StringBuilder(prettyPrintType());
-    sb.append("Item: id=\"").append(getID());
-    if (getLabel() != null && !getLabel().isEmpty()) {
-      sb.append("\" label=\"").append(getLabel());
-    }
-    sb.append("\" state=\"").append(getStateAsString());
-    if (hasCategory()) {
-      sb.append("\" category=\"").append(getCategory().getName());
-    }
-    if (getTopic() != null) {
-      sb.append("\" topic=\"").append(getTopic().allParts());
-    }
-    return sb.append("\" ;\n").toString();
+    return new MemberPrinter(prettyPrintType())
+        .addRequired("id", getID())
+        .addNonDefault("label", getLabel())
+        .addRequired("state", getStateAsString())
+        .addOptional("category", hasCategory(), () -> getCategory().getName())
+        .addOptional("topic", hasTopic(), () -> getTopic().getTopicString())
+        .addIds("controls", getControllingList())
+        .addNodes("metaData", getNumMetaData(), getMetaDataList(),
+                  md -> "\"" + md.getKey() + "\":\"" + md.getValue() + "\"",
+                  MemberPrinter.ListBracketType.CURLY)
+        .build();
   }
 
   syn String Item.prettyPrintType();
-  eq ColorItem.prettyPrintType() = "Color " ;
-  eq ContactItem.prettyPrintType() = "Contact " ;
-  eq DateTimeItem.prettyPrintType() = "DateTime " ;
-  eq DimmerItem.prettyPrintType() = "Dimmer " ;
-  eq ImageItem.prettyPrintType() = "Image " ;
-  eq LocationItem.prettyPrintType() = "Location " ;
-  eq NumberItem.prettyPrintType() = "Number " ;
-  eq PlayerItem.prettyPrintType() = "Player " ;
-  eq RollerShutterItem.prettyPrintType() = "RollerShutter " ;
-  eq StringItem.prettyPrintType() = "String " ;
-  eq SwitchItem.prettyPrintType() = "Switch " ;
-  eq DefaultItem.prettyPrintType() = "" ;
-
-//Group: id="id" [label="label"] [aggregation="name"] groups=[] items=[];
-//Group: id="id" [label="label"] [aggregation="name" ("p1", "p2")] groups=[] items=[];
+  eq ColorItem.prettyPrintType() = "Color Item" ;
+  eq ContactItem.prettyPrintType() = "Contact Item" ;
+  eq DateTimeItem.prettyPrintType() = "DateTime Item" ;
+  eq DimmerItem.prettyPrintType() = "Dimmer Item" ;
+  eq ImageItem.prettyPrintType() = "Image Item" ;
+  eq LocationItem.prettyPrintType() = "Location Item" ;
+  eq NumberItem.prettyPrintType() = "Number Item" ;
+  eq PlayerItem.prettyPrintType() = "Player Item" ;
+  eq RollerShutterItem.prettyPrintType() = "RollerShutter Item" ;
+  eq StringItem.prettyPrintType() = "String Item" ;
+  eq SwitchItem.prettyPrintType() = "Switch Item" ;
+  eq ActivityItem.prettyPrintType() = "Activity Item" ;
+  eq DefaultItem.prettyPrintType() = "Item" ;
+
+  // special ActivityItem printing. Always omit state.
+  syn String ActivityItem.prettyPrint() {
+    return new MemberPrinter(prettyPrintType())
+        .addRequired("id", getID())
+        .addNonDefault("label", getLabel())
+        .addOptional("category", hasCategory(), () -> getCategory().getName())
+        .addOptional("topic", hasTopic(), () -> getTopic().getTopicString())
+        .addIds("controls", getControllingList())
+        .addNodes("metaData", getNumMetaData(), getMetaDataList(),
+                  md -> "\"" + md.getKey() + "\":\"" + md.getValue() + "\"",
+                  MemberPrinter.ListBracketType.CURLY)
+        .build();
+  }
+
+
+//Group: id="" groups=["GROUP_ID", "GROUP_ID"] items=["ITEM_ID", "ITEM_ID"] aggregation=AGG;
+//       AGG either '"agg-name"', or '"agg-name" ("param1", "param2")'
   syn String Group.prettyPrint() {
-    StringBuilder sb = new StringBuilder("Group: id=\"").append(getID()).append("\"");
-    if (getLabel() != null && !getLabel().isEmpty()) {
-      sb.append(" label=\"").append(getLabel()).append("\"");
-    }
-    if (getAggregationFunction() != null) {
-      sb.append(getAggregationFunction().prettyPrint());
-    }
-    if (getNumGroup() > 0) {
-      PrinterUtils.concatIds(sb.append(" groups=["), getGroups());
-      sb.append("]");
-    }
-    if (getNumItem() > 0) {
-      PrinterUtils.concatIds(sb.append(" items=["), getItems());
-      sb.append("]");
-    }
-    return sb.append(" ;\n").toString();
+    return new MemberPrinter("Group")
+        .addRequired("id", getID())
+        .addNonDefault("label", getLabel())
+        .addIds("groups", getNumGroup(), getGroups())
+        .addIds("items", getNumItem(), getItems())
+        .addOptionalPrettyPrint(getAggregationFunction())
+        .build();
   }
 
   syn String GroupAggregationFunction.prettyPrint();
@@ -109,66 +121,53 @@ aspect Printing {
     return sb.toString();
   }
 
-//ThingType: id="id" label="label" description="desc" parameters=[] channelTypes=[];
+//ThingType: id="" label="" description="" parameters=["PARAM_ID", "PARAM_ID"] channelTypes=["CHANNEL_TYPE_ID", "CHANNEL_TYPE_ID"];
   syn String ThingType.prettyPrint() {
-    StringBuilder sb = new StringBuilder("ThingType: id=\"");
-    sb.append(getID())
-      .append("\" label=\"").append(getLabel())
-      .append("\" description=\"").append(getDescription());
-    PrinterUtils.concatIds(sb.append("\" parameters=["), getParameters());
-    PrinterUtils.concatIds(sb.append("] channelTypes=["), getChannelTypes());
-    return sb.append("] ;\n").toString();
+    return new MemberPrinter("ThingType")
+        .addRequired("id", getID())
+        .addNonDefault("label", getLabel())
+        .addNonDefault("description", getDescription())
+        .addIds("parameters", getNumParameter(), getParameters())
+        .addIds("channelTypes", getChannelTypes())
+        .build();
   }
 
-//Parameter: id="id" label="" description="" type="" context="" default="" required;
-//Parameter: id="id" label="" description="" type="" context="" ;
+//Parameter: id="" label="" description="" type="" default="" required;
   syn String Parameter.prettyPrint() {
-    StringBuilder sb = new StringBuilder("Parameter: id=\"");
-    sb.append(getID())
-      .append("\" label=\"").append(getLabel())
-      .append("\" description=\"").append(getDescription())
-      .append("\" type=\"").append(getType())
-      .append("\" context=\"").append(getContext()).append("\"");
-    if (hasDefaultValue()) {
-      sb.append(" default=\"").append(getDefaultValue().getValue()).append("\"");
-    }
-    if (getRequired()) {
-      sb.append(" required");
-    }
-    return sb.append(" ;\n").toString();
+    return new MemberPrinter("Parameter")
+        .addRequired("id", getID())
+        .addNonDefault("label", getLabel())
+        .addNonDefault("description", getDescription())
+        .addOptional("type", getType() != null, () -> getType().toString())
+        .addNonDefault("context", getContext())
+        .addOptional("default", hasDefaultValue(), () -> getDefaultValue().getValue())
+        .addFlag("required", getRequired())
+        .build();
   }
 
-//ChannelType: id="id" label="label" description="desc" itemType="type" category=(default TEXT) readyOnly;
-//ChannelType: id="id" label="label" description="desc" itemType="type" category=(simple "") readyOnly;
+//ChannelType: id="" label="" description="" itemType="" category="" readyOnly;
   syn String ChannelType.prettyPrint() {
-    StringBuilder sb = new StringBuilder("ChannelType: id=\"");
-    sb.append(getID())
-      .append("\" label=\"").append(getLabel())
-      .append("\" description=\"").append(getDescription()).append("\"");
-    if (getItemType() != null){
-      sb.append(" itemType=\"").append(getItemType().name()).append("\"");
-    }
-    if (getChannelCategory() != null) {
-      sb.append(" category=\"").append(getChannelCategory().prettyPrint()).append("\"");
-    }
-    if (getReadOnly()) {
-      sb.append(" readOnly");
-    }
-    return sb.append(" ;\n").toString();
+    return new MemberPrinter("ChannelType")
+        .addRequired("id", getID())
+        .addNonDefault("label", getLabel())
+        .addNonDefault("description", getDescription())
+        .addOptional("itemType", getItemType() != null, () -> getItemType().name())
+        .addOptional("category", getChannelCategory() != null, () -> getChannelCategory().prettyPrint())
+        .addFlag("readOnly", getReadOnly())
+        .build();
   }
 
   syn String DefaultChannelCategory.prettyPrint() = getValue().name();
 
   syn String SimpleChannelCategory.prettyPrint() = getValue();
 
-//Channel: id="id" type="" links=["ITEM_ID", "ITEM_ID"];
+//Channel: id="" type="" links=["ITEM_ID", "ITEM_ID"];
   syn String Channel.prettyPrint() {
-    StringBuilder sb = new StringBuilder("Channel: id=\"");
-    sb.append(getID())
-      .append("\" type=\"").append(safeID(getType()))
-      .append("\" links=[");
-    return PrinterUtils.concatIds(sb, getLinkList(), Link::getItem)
-      .append("] ;\n").toString();
+    return new MemberPrinter("Channel")
+        .addRequired("id", getID())
+        .addRequired("type", getType(), ChannelType::getID)
+        .addIds("links", getLinkedItems())
+        .build();
   }
 
 //ExternalHost: "hostName:port"
@@ -180,37 +179,57 @@ aspect Printing {
     return getHostName() + ":" + getPort();
   }
 
-//Mqtt: incoming="oh2/out" outgoing="oh2/in" host="";
+//Mqtt: incoming="" outgoing="" host="";
   syn String MqttRoot.prettyPrint() {
-    StringBuilder sb = new StringBuilder("Mqtt: incoming=\"");
-    sb.append(getIncomingPrefix())
-      .append("\" outgoing=\"").append(getOutgoingPrefix());
-    if (this.hasHost()) {
-      sb.append("\" host=\"").append(getHost().prettyPrint(DEFAULT_PORT));
-    }
-    return sb.append("\" ;\n").toString();
+    return new MemberPrinter("Mqtt")
+        .addNonDefault("incoming", getIncomingPrefix())
+        .addNonDefault("outgoing", getOutgoingPrefix())
+        .addOptional("host", hasHost(), () -> getHost().prettyPrint(DEFAULT_PORT))
+        .build();
   }
 
-//Influx: host="";
+//Influx: user="" password="" dbName="" host="" ;
   syn String InfluxRoot.prettyPrint() {
-    StringBuilder sb = new StringBuilder("Influx:");
-    if (nonDefault(this.getUser(), DEFAULT_USER)) {
-      sb.append(" user=\"").append(getUser()).append("\"");
-    }
-    if (nonDefault(this.getPassword(), DEFAULT_PASSWORD)) {
-      sb.append(" password=\"").append(getPassword()).append("\"");
-    }
-    if (nonDefault(this.getDbName(), DEFAULT_DB_NAME)) {
-      sb.append(" dbName=\"").append(getDbName()).append("\"");
-    }
-    if (this.hasHost()) {
-      sb.append(" host=\"").append(getHost().prettyPrint(DEFAULT_PORT)).append("\"");
-    }
-    return sb.append(" ;\n").toString();
+    return new MemberPrinter("Influx")
+        .addNonDefault("user", getUser(), DEFAULT_USER)
+        .addNonDefault("password", getPassword(), DEFAULT_PASSWORD)
+        .addNonDefault("dbName", getDbName(), DEFAULT_DB_NAME)
+        .addOptional("host", hasHost(), () -> getHost().prettyPrint(DEFAULT_PORT))
+        .build();
   }
 
-  private boolean InfluxRoot.nonDefault(String actual, String expected) {
-    return actual != null && !actual.equals(expected);
+// Activities: { index: "name" }
+  syn String MachineLearningRoot.prettyPrint() {
+    return new MemberPrinter("ML")
+        .addNodes("activities", getNumActivity(), getActivityList(),
+                  activity -> activity.getIdentifier() + ":\"" + activity.getLabel() + "\"",
+                  MemberPrinter.ListBracketType.CURLY)
+        .build();
+  }
+
+// Expressions
+  syn String ParenthesizedNumberExpression.prettyPrint() = "(" + getOperand().prettyPrint() + ")";
+  syn String NumberLiteralExpression.prettyPrint() = Double.toString(getValue());
+  syn String AddExpression.prettyPrint() = "(" + getLeftOperand().prettyPrint() + " + " + getRightOperand().prettyPrint() + ")";
+  syn String SubExpression.prettyPrint() = "(" + getLeftOperand().prettyPrint() + " - " + getRightOperand().prettyPrint() + ")";
+  syn String MultExpression.prettyPrint() = "(" + getLeftOperand().prettyPrint() + " * " + getRightOperand().prettyPrint() + ")";
+  syn String DivExpression.prettyPrint() = "(" + getLeftOperand().prettyPrint() + " / " + getRightOperand().prettyPrint() + ")";
+  syn String PowerExpression.prettyPrint() = "(" + getLeftOperand().prettyPrint() + " ^ " + getRightOperand().prettyPrint() + ")";
+  syn String ParenthesizedLogicalExpression.prettyPrint()  = "(" + getOperand().prettyPrint() + ")";
+  syn String NotExpression.prettyPrint()  = "!" + getOperand().prettyPrint();
+  syn String ComparingExpression.prettyPrint() {
+    switch (getComparator()) {
+      case NotEquals: return "(" + getLeftOperand().prettyPrint() + " != " + getRightOperand().prettyPrint() + ")";
+      case Equals: return "(" + getLeftOperand().prettyPrint() + " == " + getRightOperand().prettyPrint() + ")";
+      case LessThan: return "(" + getLeftOperand().prettyPrint() + " < " + getRightOperand().prettyPrint() + ")";
+      case GreaterThan: return "(" + getLeftOperand().prettyPrint() + " > " + getRightOperand().prettyPrint() + ")";
+      case LessOrEqualThan: return "(" + getLeftOperand().prettyPrint() + " <= " + getRightOperand().prettyPrint() + ")";
+      case GreaterOrEqualThan: return "(" + getLeftOperand().prettyPrint() + " >= " + getRightOperand().prettyPrint() + ")";
+      default: throw new IllegalArgumentException("Unknown compartor type: " + getComparator());
+    }
   }
+  syn String AndExpression.prettyPrint() = "(" + getLeftOperand().prettyPrint() + " & " + getRightOperand().prettyPrint() + ")";
+  syn String OrExpression.prettyPrint() = "(" + getLeftOperand().prettyPrint() + " | " + getRightOperand().prettyPrint() + ")";
+  syn String Designator.prettyPrint() = getItem().getID();
 
 }
diff --git a/eraser-base/src/main/jastadd/Rules.jrag b/eraser-base/src/main/jastadd/Rules.jrag
index 96ec84feceef82622f37e495a2bd7e06959169ff..e1e0db7c7248016dd6db7bea0d6579994548cee9 100644
--- a/eraser-base/src/main/jastadd/Rules.jrag
+++ b/eraser-base/src/main/jastadd/Rules.jrag
@@ -1,102 +1,116 @@
 aspect Rules {
 
-  Object ItemObserver.last_known_state = null;
-
-  // idea: abuse dependency tracking to trigger rule only iff item state has changed
-  syn boolean ItemObserver.apply() {
-//    if (getObservedItem().getStateAsString() != last_known_state) {
-    if (!getObservedItem().stateEquals(last_known_state)) {
-      // state has changed
-      last_known_state = getObservedItem().copyState();
-      for (Event event : getTriggeredEventList()) {
-        // TODO maybe trigger those events asynchronously?
-        event.triggerRule();
-      }
-      return true;
+  public void ItemObserver.apply() {
+    // state has changed, so trigger rules
+    for (Rule rule : getTriggeredRuleList()) {
+      rule.trigger(observedItem());
     }
-    return false;
   }
 
-  // TODO this should maybe happen automatically, but setState has to be changed for this
-  syn boolean Root.applyRules() {
-    boolean atLeastOneRuleApplied = false;
-    for (Rule rule : getRuleList()) {
-      rule.apply();
-      atLeastOneRuleApplied = true;
+  public void Rule.trigger(Item triggeringItem) {
+    // 1) check conditions
+    for (Condition condition : getConditionList()) {
+      if (!condition.holdsFor(triggeringItem)) {
+        return;
+      }
+    }
+    // 2) execute actions
+    for (Action action : getActionList()) {
+      action.applyFor(triggeringItem);
     }
-    return atLeastOneRuleApplied;
   }
 
-  syn boolean Rule.apply() {
-    // iterate over events, and "try" to apply them.
-    // if one succeeds, then stop, since we want to avoid firing the rule multiple times
-    for (Event event : getEventList()) {
-      if (event.apply()) {
-        return true;
+  inh Item ItemObserver.observedItem();
+  eq Item.getItemObserver().observedItem() = this;
+
+  public void Rule.activateFor(Item item) {
+    // 1) Get or create new ItemObserver, and add it to Root
+    ItemObserver itemObserver;
+    if (item.hasItemObserver()) {
+      itemObserver = item.getItemObserver();
+      // 1.a) Check if observer already triggers this rule
+      for (Rule rule : itemObserver.getTriggeredRules()) {
+        if (rule.equals(this)) {
+        logger.warn("Rule already activated for item {}. Ignoring.", item);
+        return;
+        }
       }
+    } else {
+      itemObserver = new ItemObserver();
+      item.setItemObserver(itemObserver);
     }
-    return false;
-  }
-
-  syn boolean Event.apply() {
-    return getObserver().apply();
+    // 2) Link event and itemObserver
+    itemObserver.addTriggeredRule(this);
   }
 
-  inh Rule Event.containingRule();
-  eq Rule.getEvent().containingRule() = this;
+  private static java.util.concurrent.ScheduledExecutorService Rule.executor = java.util.concurrent.Executors.newScheduledThreadPool(4);
 
-  public void Event.triggerRule(){
-    containingRule().trigger();
+  /**
+   * Trigger this rule in the given period with no initial delay.
+   * @param period the period between successive executions
+   * @param unit   the time unit of the period parameter
+   * @return a ScheduledFuture that can be used to cancel
+   */
+  public java.util.concurrent.ScheduledFuture Rule.activateEvery(long period, java.util.concurrent.TimeUnit unit) {
+    return activateEvery(0, period, unit);
   }
 
-  public void Rule.trigger() {
-    // 1) check condition
-    Root model = getRoot();
-    if (getCondition().apply(model)) {
-      // 2) execute action
-      getAction().accept(model);
-    }
+  /**
+   * Trigger this rule in the given period with the given initial delay.
+   * @param initialDelay the time to delay first execution
+   * @param period       the period between successive executions
+   * @param unit         the time unit of the initialDelay and period parameters
+   * @return a ScheduledFuture that can be used to cancel
+   */
+  public java.util.concurrent.ScheduledFuture Rule.activateEvery(long initialDelay, long period, java.util.concurrent.TimeUnit unit) {
+    return executor.scheduleAtFixedRate(() -> trigger(null), initialDelay, period, unit);
   }
 
-  public void Event.activateFor(Item item) {
-    // 0) Check if this event has already an observer (which will be lost)
-    if (getObserver() != null) {
-      logger.warn("Overriding observer in {}", this.getLabel());
-    }
-    // 1) Get or create new ItemObserver, and add it to Root
-    ItemObserver itemObserver;
-    if (item.hasObserver()) {
-      itemObserver = item.getObserver();
+  public void Rule.removeActivationOf(Item item) {
+    if (item.hasItemObserver()) {
+      item.getItemObserver().removeTriggeredRule(this);
     } else {
-      itemObserver = new ItemObserver();
-      getRoot().addItemObserver(itemObserver);
-      item.setObserver(itemObserver);
-      itemObserver.last_known_state = item.copyState();
+      // there is no observer yet
+      logger.warn("Item {} was never activated before.", item);
     }
-    // 2) Link event and itemObserver
-    itemObserver.addTriggeredEvent(this);
   }
 
-  public void Rule.addEventFor(Item item, String label) {
-    Event e = new Event(label);
-    this.addEvent(e);
-    e.activateFor(item);
-  }
+  // --- Condition.holdsFor ---
+  syn boolean Condition.holdsFor(Item item);
+  eq ItemStateCheckCondition.holdsFor(Item item) = getItemStateCheck().holdsFor(item);
+  eq ExpressionCondition.holdsFor(Item item) = getLogicalExpression().eval();
 
-  public boolean Event.removeSelf() {
-    // 1) Remove from ItemObserver
-    getObserver().removeTriggeredEvent(this);
-    // 2) Remove from Root
-    if (getParent() == null) {
-      return false;
-    }
-    for (int i = 0; i < getParent().numChildren(); ++i) {
-      if (getParent().getChild(i) == this) {
-        getParent().removeChild(i);
-        return true;
-      }
-    }
-    return false;
+  // --- Action.applyFor ---
+  public abstract void Action.applyFor(Item item);
+  public void NoopAction.applyFor(Item item) {
+    // empty by design
+  }
+  public void LambdaAction.applyFor(Item item) {
+    getLambda().accept(item);
+  }
+  public void TriggerRuleAction.applyFor(Item item) {
+    getRule().trigger(item);
+  }
+  public void SetStateFromExpression.applyFor(Item item) {
+    getAffectedItem().setStateFromDouble(getNumberExpression().eval());
+  }
+  public void SetStateFromConstantStringAction.applyFor(Item item) {
+    getAffectedItem().setStateFromString(getNewState());
+  }
+  public void SetStateFromLambdaAction.applyFor(Item item) {
+    getAffectedItem().setStateFromString(getNewStateProvider().get());
+  }
+  public void SetStateFromTriggeringItemAction.applyFor(Item item) {
+    item.doUpdateFor(getAffectedItem());
+  }
+  public void SetStateFromItemsAction.applyFor(Item item) {
+    getAffectedItem().setStateFromString(getCombinator().apply(getSourceItems()));
+  }
+  public void AddDoubleToStateAction.applyFor(Item item) {
+    getAffectedItem().setStateFromDouble(getAffectedItem().getStateAsDouble() + getIncrement());
+  }
+  public void MultiplyDoubleToStateAction.applyFor(Item item) {
+    getAffectedItem().setStateFromDouble(getAffectedItem().getStateAsDouble() * getMultiplier());
   }
 
 }
diff --git a/eraser-base/src/main/jastadd/Rules.relast b/eraser-base/src/main/jastadd/Rules.relast
new file mode 100644
index 0000000000000000000000000000000000000000..3385858454a006cda3c052dbc41028684cb17d23
--- /dev/null
+++ b/eraser-base/src/main/jastadd/Rules.relast
@@ -0,0 +1,29 @@
+// --- New ECA rules ---
+Rule ::= Condition* Action* ;
+abstract Condition ;
+ItemStateCheckCondition : Condition ::= ItemStateCheck ;
+ExpressionCondition : Condition ::= LogicalExpression ;
+abstract Action ;
+NoopAction : Action ;
+LambdaAction : Action ::= <Lambda:Action2EditConsumer> ;
+
+TriggerRuleAction : Action ;
+rel TriggerRuleAction.Rule -> Rule ;
+
+abstract SetStateAction : Action ;
+rel SetStateAction.AffectedItem -> Item ;
+
+SetStateFromExpression : SetStateAction ::= NumberExpression ;
+
+SetStateFromConstantStringAction : SetStateAction ::= <NewState:String> ;
+SetStateFromLambdaAction : SetStateAction ::= <NewStateProvider:NewStateProvider> ;
+SetStateFromTriggeringItemAction : SetStateAction ::= ;
+
+SetStateFromItemsAction : SetStateAction ::= <Combinator:ItemsToStringFunction> ;
+rel SetStateFromItemsAction.SourceItem* -> Item ;
+
+AddDoubleToStateAction : SetStateAction ::= <Increment:double> ;
+MultiplyDoubleToStateAction : SetStateAction ::= <Multiplier:double> ;
+
+ItemObserver ::= ;
+rel ItemObserver.TriggeredRule* <-> Rule.Observer* ;
diff --git a/eraser-base/src/main/jastadd/Util.jrag b/eraser-base/src/main/jastadd/Util.jrag
index b7e252afd51fda93273eba63e745483f7c78df59..b038ca0dfae51d7bbea748086965b78e40f0c3e6 100644
--- a/eraser-base/src/main/jastadd/Util.jrag
+++ b/eraser-base/src/main/jastadd/Util.jrag
@@ -5,19 +5,32 @@ aspect Util {
 //    return new ExternalHost(hostName, 1883);
 //  }
   public void MqttRoot.setHostByName(String hostName) {
-    setHost(new ExternalHost(hostName, 1883));
+    setHost(ExternalHost.of(hostName, DEFAULT_PORT));
+    flushCache();
   }
 
   public void InfluxRoot.setHostByName(String hostName) {
-    setHost(new ExternalHost(hostName, 8086));
+    setHost(ExternalHost.of(hostName, DEFAULT_PORT));
+  }
+
+  public static ExternalHost ExternalHost.of(String hostName, int defaultPort) {
+    String host = hostName;
+    int port = defaultPort;
+    if (hostName.contains(":")) {
+      String[] parts = hostName.split(":");
+      host = parts[0];
+      port = Integer.parseInt(parts[1]);
+    }
+    return new ExternalHost(host, port);
   }
 
   syn String ExternalHost.urlAsString() = String.format("http://%s:%s", getHostName(), getPort());
 
   public static Root Root.createEmptyRoot() {
     Root model = new Root();
+    model.setOpenHAB2Model(new OpenHAB2Model());
     model.setMqttRoot(new MqttRoot());
-    model.setInfluxRoot(new InfluxRoot());
+    model.setInfluxRoot(InfluxRoot.createDefault());
     model.setMachineLearningRoot(new MachineLearningRoot());
     return model;
   }
diff --git a/eraser-base/src/main/jastadd/eraser.flex b/eraser-base/src/main/jastadd/eraser.flex
index 8ffefb73b6c6dd219dd4a59839fed77e8302ea47..bc9b3e8d9a41c9bd4cc51d1879f081b32538f103 100644
--- a/eraser-base/src/main/jastadd/eraser.flex
+++ b/eraser-base/src/main/jastadd/eraser.flex
@@ -30,11 +30,11 @@ import de.tudresden.inf.st.eraser.jastadd.parser.EraserParser.Terminals;
 %}
 
 WhiteSpace = [ ] | \t | \f | \n | \r | \r\n
-//Identifier = [:jletter:][:jletterdigit:]*
+Identifier = [:jletter:][:jletterdigit:]*
 Text = \" ([^\"]*) \"
 
-//Integer = [:digit:]+ // | "+" [:digit:]+ | "-" [:digit:]+
-//Real    = [:digit:]+ "." [:digit:]* | "." [:digit:]+
+Integer = [:digit:]+ // | "+" [:digit:]+ | "-" [:digit:]+
+Real    = [:digit:]+ "." [:digit:]* | "." [:digit:]+
 
 Comment = "//" [^\n\r]+
 
@@ -55,7 +55,10 @@ Comment = "//" [^\n\r]+
 "Channel"      { return sym(Terminals.CHANNEL); }
 "Mqtt"         { return sym(Terminals.MQTT); }
 "Influx"       { return sym(Terminals.INFLUX); }
+"ML"           { return sym(Terminals.ML); }
+"Rule"         { return sym(Terminals.RULE); }
 // special items (group already has a token definition)
+"Activity"     { return sym(Terminals.ACTIVITY); }
 "Color"        { return sym(Terminals.COLOR); }
 "Contact"      { return sym(Terminals.CONTACT); }
 "DateTime"     { return sym(Terminals.DATE_TIME); }
@@ -68,11 +71,13 @@ Comment = "//" [^\n\r]+
 "String"       { return sym(Terminals.STRING); }
 "Switch"       { return sym(Terminals.SWITCH); }
 // within specification
+"activities"   { return sym(Terminals.ACTIVITIES); }
 "aggregation"  { return sym(Terminals.AGGREGATION); }
 "category"     { return sym(Terminals.CATEGORY); }
 "channels"     { return sym(Terminals.CHANNELS); }
 "channelTypes" { return sym(Terminals.CHANNEL_TYPES); }
 "context"      { return sym(Terminals.CONTEXT); }
+"controls"     { return sym(Terminals.CONTROLS); }
 "dbName"       { return sym(Terminals.DB_NAME); }
 "default"      { return sym(Terminals.DEFAULT); }
 "description"  { return sym(Terminals.DESCRIPTION); }
@@ -84,6 +89,7 @@ Comment = "//" [^\n\r]+
 "itemType"     { return sym(Terminals.ITEM_TYPE); }
 "label"        { return sym(Terminals.LABEL); }
 "links"        { return sym(Terminals.LINKS); }
+"metaData"     { return sym(Terminals.META_DATA); }
 "outgoing"     { return sym(Terminals.OUTGOING); }
 "parameters"   { return sym(Terminals.PARAMETERS); }
 "password"     { return sym(Terminals.PASSWORD); }
@@ -96,6 +102,20 @@ Comment = "//" [^\n\r]+
 // special characters
 "="            { return sym(Terminals.EQUALS); }
 //"\""           { return sym(Terminals.QUOTE); }
+"<"            { return sym(Terminals.LT); }
+"<="           { return sym(Terminals.LE); }
+"=="           { return sym(Terminals.EQ); }
+"!="           { return sym(Terminals.NE); }
+">="           { return sym(Terminals.GE); }
+">"            { return sym(Terminals.GT); }
+"+"            { return sym(Terminals.PLUS); }
+"*"            { return sym(Terminals.MULT); }
+"-"            { return sym(Terminals.MINUS); }
+"/"            { return sym(Terminals.DIV); }
+"^"            { return sym(Terminals.POW); }
+"!"            { return sym(Terminals.EXCLAMATION); }
+"|"            { return sym(Terminals.OR); }
+"&"            { return sym(Terminals.AND); }
 ":"            { return sym(Terminals.COLON); }
 ","            { return sym(Terminals.COMMA); }
 ";"            { return sym(Terminals.SEMICOLON); }
@@ -103,8 +123,12 @@ Comment = "//" [^\n\r]+
 "]"            { return sym(Terminals.RB_SQUARE); }
 "("            { return sym(Terminals.LB_ROUND); }
 ")"            { return sym(Terminals.RB_ROUND); }
-//{Identifier}   { return sym(Terminals.NAME); }
+"{"            { return sym(Terminals.LB_CURLY); }
+"}"            { return sym(Terminals.RB_CURLY); }
+{Identifier}   { return sym(Terminals.NAME); }
 {Text}         { return symText(Terminals.TEXT); }
-//{Real}         { return sym(Terminals.REAL); }
-//{Integer}      { return sym(Terminals.INTEGER); }
+{Integer}      { return sym(Terminals.INTEGER); }
+{Real}         { return sym(Terminals.REAL); }
 <<EOF>>        { return sym(Terminals.EOF); }
+/* error fallback */
+[^]            { throw new Error("Illegal character '"+ yytext() +"' at line " + (yyline+1) + " column " + (yycolumn+1)); }
diff --git a/eraser-base/src/main/jastadd/eraser.parser b/eraser-base/src/main/jastadd/eraser.parser
index bc065f7338ee33d4e641b3b494b6e57a539c4660..9934302f129072e5165966d34a4300281312fdbd 100644
--- a/eraser-base/src/main/jastadd/eraser.parser
+++ b/eraser-base/src/main/jastadd/eraser.parser
@@ -1,6 +1,7 @@
 %header {:
 package de.tudresden.inf.st.eraser.jastadd.parser;
 import de.tudresden.inf.st.eraser.jastadd.model.*;
+import de.tudresden.inf.st.eraser.jastadd.model.Action;
 import de.tudresden.inf.st.eraser.parser.EraserParserHelper;
 import java.util.Map;
 import java.util.HashMap;
@@ -23,31 +24,80 @@ import java.util.HashMap;
 :} ;
 
 %goal goal;
+%goal number_expression;
+%goal logical_expression;
 
 Root goal =
-     thing.t goal.r           {: insertZero(r.getThingList(), t); return r; :}
-  |  item.i goal.r            {: return r; :}
-  |  group.g goal.r           {: insertZero(r.getGroupList(), g); return r; :}
-  |  thing_type.tt goal.r     {: insertZero(r.getThingTypeList(), tt); return r; :}
-  |  parameter goal.r         {: return r; :}
-  |  channel_type.ct goal.r   {: insertZero(r.getChannelTypeList(), ct); return r; :}
-  |  channel.c goal.r         {: return r; :}
-  |  mqtt_root.mr goal.r      {: r.setMqttRoot(mr); return r; :}
-  |  thing.t           {: return eph.createRoot(t); :}
-  |  item.i            {: return eph.createRoot(); :}
-  |  group.g           {: return eph.createRoot(g); :}
-  |  thing_type.tt     {: return eph.createRoot(tt); :}
-  |  parameter         {: return eph.createRoot(); :}
-  |  channel_type.ct   {: return eph.createRoot(ct); :}
-  |  mqtt_root.mr      {: return eph.createRoot(mr); :}
-  |  influx_root.ir    {: return eph.createRoot(ir); :}
-  |  channel.c         {: return eph.createRoot(); :}
+     thing.t goal.r                     {: insertZero(r.getOpenHAB2Model().getThingList(), t); return r; :}
+  |  item.i goal.r                      {: return r; :}
+  |  group.g goal.r                     {: insertZero(r.getOpenHAB2Model().getGroupList(), g); return r; :}
+  |  thing_type.tt goal.r               {: insertZero(r.getOpenHAB2Model().getThingTypeList(), tt); return r; :}
+  |  parameter goal.r                   {: return r; :}
+  |  channel_type.ct goal.r             {: insertZero(r.getOpenHAB2Model().getChannelTypeList(), ct); return r; :}
+  |  channel.c goal.r                   {: return r; :}
+  |  mqtt_root.mr goal.r                {: r.setMqttRoot(mr); return r; :}
+  |  influx_root.ir goal.r              {: r.setInfluxRoot(ir); return r; :}
+  |  machine_learning_root.ml goal.r    {: r.setMachineLearningRoot(ml); return r; :}
+  |  rule.rule goal.r                   {: r.addRule(rule); return r; :}
+  |  thing.t                            {: return eph.createRoot(t); :}
+  |  item.i                             {: return eph.createRoot(); :}
+  |  group.g                            {: return eph.createRoot(g); :}
+  |  thing_type.tt                      {: return eph.createRoot(tt); :}
+  |  parameter                          {: return eph.createRoot(); :}
+  |  channel_type.ct                    {: return eph.createRoot(ct); :}
+  |  channel.c                          {: return eph.createRoot(); :}
+  |  mqtt_root.mr                       {: return eph.createRoot(mr); :}
+  |  influx_root.ir                     {: return eph.createRoot(ir); :}
+  |  machine_learning_root.ml           {: return eph.createRoot(ml); :}
+  |  rule.rule                          {: return eph.createRoot(rule); :}
+  ;
+
+%left RB_ROUND;
+%left MULT, DIV;
+%left PLUS, MINUS;
+%left POW;
+%left LT, LE, EQ, GE, GT;
+%left OR;
+%left AND;
+
+NumberExpression number_expression =
+    LB_ROUND number_expression.a MULT  number_expression.b RB_ROUND     {: return new MultExpression(a, b); :}
+  | LB_ROUND number_expression.a DIV   number_expression.b RB_ROUND     {: return new DivExpression(a, b); :}
+  | LB_ROUND number_expression.a PLUS  number_expression.b RB_ROUND     {: return new AddExpression(a, b); :}
+  | LB_ROUND number_expression.a MINUS number_expression.b RB_ROUND     {: return new SubExpression(a, b); :}
+  | LB_ROUND number_expression.a POW number_expression.b RB_ROUND       {: return new PowerExpression(a, b); :}
+  | literal_expression.l                                                {: return l; :}
+  | designator.d                                                        {: return d; :}
+  | LB_ROUND number_expression.e RB_ROUND                               {: return new ParenthesizedNumberExpression(e); :}
+  ;
+
+LogicalExpression logical_expression =
+    LB_ROUND logical_expression.a AND   logical_expression.b RB_ROUND     {: return new AndExpression(a, b); :}
+  | LB_ROUND logical_expression.a OR    logical_expression.b RB_ROUND     {: return new OrExpression(a, b); :}
+  | EXCLAMATION logical_expression.e                                      {: return new NotExpression(e); :}
+  | LB_ROUND logical_expression.e RB_ROUND                                {: return new ParenthesizedLogicalExpression(e); :}
+  | LB_ROUND number_expression.a LT    number_expression.b RB_ROUND       {: return new ComparingExpression(a, b, ComparatorType.LessThan); :}
+  | LB_ROUND number_expression.a LE    number_expression.b RB_ROUND       {: return new ComparingExpression(a, b, ComparatorType.LessOrEqualThan); :}
+  | LB_ROUND number_expression.a EQ    number_expression.b RB_ROUND       {: return new ComparingExpression(a, b, ComparatorType.Equals); :}
+  | LB_ROUND number_expression.a NE    number_expression.b RB_ROUND       {: return new ComparingExpression(a, b, ComparatorType.NotEquals); :}
+  | LB_ROUND number_expression.a GE    number_expression.b RB_ROUND       {: return new ComparingExpression(a, b, ComparatorType.GreaterOrEqualThan); :}
+  | LB_ROUND number_expression.a GT    number_expression.b RB_ROUND       {: return new ComparingExpression(a, b, ComparatorType.GreaterThan); :}
+  ;
+
+NumberLiteralExpression literal_expression =
+    INTEGER.n             {: return new NumberLiteralExpression(Integer.parseInt(n)); :}
+  | REAL.n                {: return new NumberLiteralExpression(Double.parseDouble(n)); :}
+  ;
+
+Designator designator =
+    NAME.n                    {: return eph.createDesignator(n); :}
   ;
 
 Thing thing =
     THING COLON thing_body.tb SEMICOLON {: return tb; :}
   ;
 
+// Thing: id="" label="" type="" channels=["CHANNEL_ID", "CHANNEL_ID"] ;
 Thing thing_body =
      ID EQUALS TEXT.n thing_body.t                        {: return eph.setID(t, n); :}
   |  LABEL EQUALS TEXT.n thing_body.t                     {: t.setLabel(n); return t; :}
@@ -59,7 +109,6 @@ Thing thing_body =
   |  CHANNELS EQUALS string_list.channels   {: return eph.setChannels(new Thing(), channels); :}
   ;
 
-//old:     ITEM COLON item_body.ib SEMICOLON {: return ib; :}
 Item item =
      COLOR ITEM COLON item_body.ib SEMICOLON          {: return eph.retype(new ColorItem(), ib); :}
   |  CONTACT ITEM COLON item_body.ib SEMICOLON        {: return eph.retype(new ContactItem(), ib); :}
@@ -72,16 +121,21 @@ Item item =
   |  ROLLER_SHUTTER ITEM COLON item_body.ib SEMICOLON {: return eph.retype(new RollerShutterItem(), ib); :}
   |  STRING ITEM COLON item_body.ib SEMICOLON         {: return eph.retype(new StringItem(), ib); :}
   |  SWITCH ITEM COLON item_body.ib SEMICOLON         {: return eph.retype(new SwitchItem(), ib); :}
+  |  ACTIVITY ITEM COLON item_body.ib SEMICOLON       {: return eph.retype(new ActivityItem(), ib); :}
   |  ITEM COLON item_body.ib SEMICOLON                {: return eph.retype(new DefaultItem(), ib); :}
   ;
 
-// ITEM_TYPE Item: id="" label="" state="" topic="";
+// ITEM_TYPE Item: id="" label="" state="" category="" topic="" controls=["ITEM_ID"] metaData={"key":"value"} ;
 Item item_body =
      ID EQUALS TEXT.n item_body.i       {: return eph.setID(i, n); :}
   |  LABEL EQUALS TEXT.n item_body.i    {: i.setLabel(n); return i; :}
   |  STATE EQUALS TEXT.n item_body.i    {: i.setStateFromString(n); return i; :}
   |  TOPIC EQUALS TEXT.n item_body.i    {: return eph.setTopic(i, n); :}
   |  CATEGORY EQUALS TEXT.n item_body.i {: return eph.setCategory(i, n); :}
+  |  CONTROLS EQUALS string_list.controlling item_body.i
+    {: return eph.setControlling(i, controlling); :}
+  |  META_DATA EQUALS string_map.md item_body.i
+    {: return eph.setMetaData(i, md); :}
   |                                     {: return eph.createItem(); :}
   ;
 
@@ -89,7 +143,8 @@ Group group =
      GROUP COLON group_body.gb SEMICOLON    {: return gb; :}
   ;
 
-// Group: id="" groups=[] items=[];
+// Group: id="" groups=["GROUP_ID", "GROUP_ID"] items=["ITEM_ID", "ITEM_ID"] aggregation="";
+// Group: id="" groups=["GROUP_ID", "GROUP_ID"] items=["ITEM_ID", "ITEM_ID"] aggregation="" ("","");
 Group group_body =
      ID EQUALS TEXT.n group_body.g                    {: return eph.setID(g, n); :}
   |  LABEL EQUALS TEXT.n group_body.g                 {: g.setLabel(n); return g; :}
@@ -188,32 +243,88 @@ InfluxRoot influx_root_body =
   |                                                  {: return InfluxRoot.createDefault(); :}
   ;
 
+// Machine Learning
+MachineLearningRoot machine_learning_root =
+     ML COLON machine_learning_root_body.b SEMICOLON     {: return b; :}
+  ;
+
+// ML: activities={index:"name"} ;
+MachineLearningRoot machine_learning_root_body =
+    ACTIVITIES EQUALS integer_map.map machine_learning_root_body.b    {: return eph.setActivities(b, map); :}
+  |                                                                   {: return MachineLearningRoot.createDefault(); :}
+  ;
+
+// Rule: condition=condition action=a1 action=a2;
+// (only allow one condition and action for now)
+Rule rule =
+    RULE COLON CONDITION EQUALS condition.c action.a SEMICOLON          {: return eph.createRule(c, a); :}
+  ;
+
+Condition condition =
+    logical_expression.be                            {: return new ExpressionCondition(be); :}
+  ;
+
+// TODO implement action cases
+Action action = {: return new NoopAction(); :}
+  ;
+
+// Util: StringList, StringKeyMap, IntegerKeyMap
 StringList string_list =
-     LB_SQUARE string_list_body.slb RB_SQUARE   {: return slb; :}
+     LB_SQUARE string_list_body.slb RB_SQUARE         {: return slb; :}
+  |  LB_SQUARE RB_SQUARE                              {: return new StringList(); :}
   ;
 
 StringList string_list_body =
-     TEXT.n COMMA string_list_body.slb     {: slb.add(n); return slb; :}
+     TEXT.n COMMA string_list_body.slb                {: slb.add(n); return slb; :}
   |  TEXT.n
     {:
        StringList result = new StringList();
        result.add(n);
        return result;
     :}
-  |                                         {: return new StringList(); :}
   ;
 
 StringList round_string_list =
-     LB_ROUND round_string_list_body.slb RB_ROUND   {: return slb; :}
+     LB_ROUND round_string_list_body.slb RB_ROUND     {: return slb; :}
+  |  LB_ROUND RB_ROUND                                {: return new StringList(); :}
   ;
 
 StringList round_string_list_body =
-     TEXT.n COMMA round_string_list_body.slb     {: slb.add(n); return slb; :}
+     TEXT.n COMMA round_string_list_body.slb          {: slb.add(n); return slb; :}
   |  TEXT.n
     {:
        StringList result = new StringList();
        result.add(n);
        return result;
     :}
-  |                                         {: return new StringList(); :}
+  ;
+
+StringKeyMap string_map =
+     LB_CURLY string_map_body.smb RB_CURLY             {: return smb; :}
+  |  LB_CURLY RB_CURLY                                 {: return new StringKeyMap(); :}
+  ;
+
+StringKeyMap string_map_body =
+     TEXT.key COLON TEXT.value COMMA string_map_body.smb {: smb.put(key, value); return smb; :}
+  |  TEXT.key COLON TEXT.value
+    {:
+       StringKeyMap result = new StringKeyMap();
+       result.put(key, value);
+       return result;
+    :}
+  ;
+
+IntegerKeyMap integer_map =
+     LB_CURLY integer_map_body.imb RB_CURLY             {: return imb; :}
+  |  LB_CURLY RB_CURLY                                  {: return new IntegerKeyMap(); :}
+  ;
+
+IntegerKeyMap integer_map_body =
+     INTEGER.key COLON TEXT.value COMMA integer_map_body.imb {: imb.put(Integer.parseInt(key), value); return imb; :}
+  |  INTEGER.key COLON TEXT.value
+    {:
+       IntegerKeyMap result = new IntegerKeyMap();
+       result.put(Integer.parseInt(key), value);
+       return result;
+    :}
   ;
diff --git a/eraser-base/src/main/jastadd/main.relast b/eraser-base/src/main/jastadd/main.relast
index 47bf7304f545b320da804f17135ea21509db775f..066ffbbc6f4eeb18930c645d246cb57112a2c8a9 100644
--- a/eraser-base/src/main/jastadd/main.relast
+++ b/eraser-base/src/main/jastadd/main.relast
@@ -1,114 +1,12 @@
 // ----------------    Main    ------------------------------
-Root ::= Thing* Group* ThingType* ChannelType* ChannelCategory* ItemCategory* User* MqttRoot InfluxRoot
-         MachineLearningRoot Rule* ItemObserver* ;
-
-// ----------------    openHAB    ------------------------------
-abstract ModelElement ::= <ID:String> ;
-abstract LabelledModelElement : ModelElement ::= <Label:String> ;
-abstract DescribableModelElement : LabelledModelElement ::= <Description:String> ;
-
-ThingType : DescribableModelElement ::= Parameter* ;
-
-Thing : LabelledModelElement ::= Channel* ;
-
-ChannelType : DescribableModelElement ::= <ItemType:ItemType> <ReadOnly:boolean> ;
-
-abstract ChannelCategory ;
-DefaultChannelCategory : ChannelCategory ::= <Value:DefaultChannelCategoryValue> ;
-SimpleChannelCategory : ChannelCategory ::= <Value:String> ;
-
-Channel : ModelElement ::= <Type:ChannelType> Link* ;
-
-Link ::= <Item:Item> ;
-
-Parameter : DescribableModelElement ::= <Type:ParameterValueType> [DefaultValue:ParameterDefaultValue] <Context:String> <Required:boolean> ;
-ParameterDefaultValue ::= <Value:String> ;
-
-abstract Item : LabelledModelElement ::= <DefaultShouldSendState:boolean> <_fetched_data:boolean> ;
-abstract ItemWithBooleanState : Item ::= <_state:boolean> ;
-abstract ItemWithStringState : Item ::= <_state:String> ;
-abstract ItemWithDoubleState : Item ::= <_state:double> ;
-ColorItem : Item ::= <_state:TupleHSB> ;
-DateTimeItem : Item ::= <_state:Date> ;
-ContactItem : ItemWithBooleanState ;
-DimmerItem : ItemWithDoubleState ;
-ImageItem : ItemWithStringState ;
-LocationItem : ItemWithStringState ;
-NumberItem : ItemWithDoubleState ;
-PlayerItem : ItemWithStringState ;
-RollerShutterItem : ItemWithBooleanState ;
-StringItem : ItemWithStringState ;
-SwitchItem : ItemWithBooleanState ;
-DefaultItem : ItemWithStringState ;
-
-ItemCategory ::= <Name:String> ;
-
-Group : LabelledModelElement ::= Group* Item* [AggregationFunction:GroupAggregationFunction] ;
-abstract GroupAggregationFunction ;
-SimpleGroupAggregationFunction : GroupAggregationFunction ::= <FunctionName:SimpleGroupAggregationFunctionName> ;
-ParameterizedGroupAggregationFunction : GroupAggregationFunction ::= <FunctionName:ParameterizedGroupAggregationFunctionName>
-                                                                     <Param1:String> <Param2:String> ;
+Root ::= OpenHAB2Model User* MqttRoot InfluxRoot MachineLearningRoot Rule* Location* ;
 
 // ----------------    Users   ------------------------------
 User : LabelledModelElement ;
+rel Root.CurrentUser? -> User ;
 
 // ----------------    Util    ------------------------------
 ExternalHost ::= <HostName:String> <Port:int> ;
 
-// ----------------    MQTT    ------------------------------
-MqttRoot ::= Topic:MqttTopic* <IncomingPrefix:String> <OutgoingPrefix:String> [Host:ExternalHost] ;
-MqttTopic ::= <Part:String> SubTopic:MqttTopic* ;
-
 // ----------------    InfluxDB    ------------------------------
 InfluxRoot ::= <User:String> <Password:String> <DbName:String> [Host:ExternalHost] ;
-
-// ----------------    Machine Learning Model    ------------------------------
-MachineLearningRoot ::= [ActivityRecognition:MachineLearningModel] [PreferenceLearning:MachineLearningModel] Activity* ChangeEvent* ;
-Activity ::= <Identifier:int> <Label:String> ;
-abstract ChangeEvent ::= <Identifier:int> <Timestamp:long> ChangedItem* ;
-ChangedItem ::= <NewStateAsString:String> ;
-RecognitionEvent : ChangeEvent ;
-ManualChangeEvent : ChangeEvent ;
-abstract MachineLearningModel ;
-ItemPreference ::= <PreferredHSB:TupleHSB> ;
-
-// ----------------    Decision Tree    ------------------------------
-DecisionTreeRoot : MachineLearningModel ::= RootRule:DecisionTreeRule ;
-abstract DecisionTreeElement ::= Preference:ItemPreference*;
-abstract DecisionTreeRule : DecisionTreeElement ::= Left:DecisionTreeElement Right:DecisionTreeElement <Label:String> ;
-ItemStateCheckRule : DecisionTreeRule ::= ItemStateCheck ;
-abstract ItemStateCheck ::= <Comparator:ComparatorType> ;
-ItemStateNumberCheck : ItemStateCheck ::= <Value:double> ;
-ItemStateStringCheck : ItemStateCheck ::= <Value:String> ;
-DecisionTreeLeaf : DecisionTreeElement ::= <ActivityIdentifier:int> <Label:String> ;
-
-// ----------------    Neural Network    ------------------------------
-NeuralNetworkRoot : MachineLearningModel ::= InputNeuron* HiddenNeuron* OutputLayer ;
-OutputLayer ::= OutputNeuron* <Combinator:DoubleArrayDoubleFunction> ;
-abstract Neuron ::= Output:NeuronConnection* ;
-NeuronConnection ::= <Weight:double> ;
-InputNeuron : Neuron ;
-HiddenNeuron : Neuron ::= <ActivationFormula:DoubleArrayDoubleFunction> ;
-OutputNeuron : HiddenNeuron ::= <Label:String> ;
-
-// ----------------    ECA-Rules    ------------------------------
-Rule ::= Event* <Condition:ConditionFunction> <Action:ActionEditConsumer> ;
-Event ::= <Label:String> ;
-ItemObserver ::= ;
-
-// ----------------    Relations    ------------------------------
-rel ThingType.ChannelType* -> ChannelType ;
-rel Thing.Type -> ThingType ;
-rel ChannelType.ChannelCategory -> ChannelCategory ;
-rel Item.Topic <-> MqttTopic.Item ;
-rel Item.Category? -> ItemCategory ;
-rel ItemStateCheck.Item -> Item ;
-rel NeuronConnection.Neuron <-> Neuron.Input* ;
-rel InputNeuron.Item -> Item ;
-rel Item.Observer? <-> ItemObserver.ObservedItem ;
-rel ItemObserver.TriggeredEvent* <-> Event.Observer ;
-rel ItemPreference.Item -> Item ;
-rel Item.Controlling* <-> Item.ControlledBy ;
-rel Root.CurrentUser? -> User ;
-rel ChangedItem.Item -> Item ;
-rel RecognitionEvent.Activity -> Activity ;
diff --git a/eraser-base/src/main/jastadd/mqtt.jrag b/eraser-base/src/main/jastadd/mqtt.jrag
index db748d03d62c87fce2ebce9b0d66cf37ee225344..b3f9c2a24a0a62ccc1f360fdadaad4cd7bafe3b5 100644
--- a/eraser-base/src/main/jastadd/mqtt.jrag
+++ b/eraser-base/src/main/jastadd/mqtt.jrag
@@ -3,64 +3,41 @@ aspect MQTT {
   // --- default values ---
   private static final int MqttRoot.DEFAULT_PORT = 1883;
 
-  java.util.Set<String> MqttRoot.ignoredTopics = new java.util.HashSet<>();
-
   //--- resolveTopic ---
   syn java.util.Optional<MqttTopic> MqttRoot.resolveTopic(String topic) {
     ensureCorrectPrefixes();
     if (!topic.startsWith(getIncomingPrefix())) {
-      logger.debug("Topic '{}' does not start with incoming prefix '{}'", topic, getIncomingPrefix());
+      logger.warn("Topic '{}' does not start with incoming prefix '{}'", topic, getIncomingPrefix());
       return java.util.Optional.empty();
     }
-    topic = topic.substring(getIncomingPrefix().length());
-    String[] tokens = topic.split("/");
-    int tokenIndex = 0;
-    java.util.Optional<MqttTopic> result = check(tokens, 0, getTopics());
-    if (!result.isPresent() && !ignoredTopics.contains(topic)) {
-      logger.error("Could not resolve {}, ignoring it.", topic);
-      ignoredTopics.add(topic);
-    }
-    return result;
+    String suffix = topic.substring(getIncomingPrefix().length());
+    return resolveTopicSuffix(suffix);
   }
 
-  java.util.Optional<MqttTopic> MqttRoot.check(String[] tokens, int tokenIndex, JastAddList<MqttTopic> topics) {
-    for (MqttTopic current : topics) {
-      if (tokens[tokenIndex].equals(current.getPart())) {
-        // topic part matches, move on or return if tokens are empty
-        ++tokenIndex;
-        if (tokens.length == tokenIndex) {
-          return java.util.Optional.of(current);
-        } else {
-          return check(tokens, tokenIndex, current.getSubTopics());
-        }
+  //--- resolveTopicSuffix ---
+  syn java.util.Optional<MqttTopic> MqttRoot.resolveTopicSuffix(String suffix) {
+    for (MqttTopic current : getTopics()) {
+      if (current.getTopicString().equals(suffix)) {
+        return Optional.of(current);
       }
     }
-    return java.util.Optional.empty();
+    return Optional.empty();
   }
 
   public void MqttRoot.ensureCorrectPrefixes() {
-    if (!getIncomingPrefix().endsWith("/")) {
+    if (!getIncomingPrefix().isEmpty() && !getIncomingPrefix().endsWith("/")) {
       setIncomingPrefix(getIncomingPrefix() + "/");
     }
-    if (!getOutgoingPrefix().endsWith("/")) {
+    if (!getOutgoingPrefix().isEmpty() && !getOutgoingPrefix().endsWith("/")) {
       setOutgoingPrefix(getOutgoingPrefix() + "/");
     }
   }
 
   //--- getIncomingTopic ---
-  syn String MqttTopic.getIncomingTopic() = getMqttRoot().getIncomingPrefix() + allParts();
+  syn String MqttTopic.getIncomingTopic() = getMqttRoot().getIncomingPrefix() + getTopicString();
 
   //--- getOutgoingTopic ---
-  syn String MqttTopic.getOutgoingTopic() = getMqttRoot().getOutgoingPrefix() + allParts();
-
-  //--- allParts ---
-  inh String MqttTopic.allParts();
-  eq MqttTopic.getSubTopic(int i).allParts() {
-    return allParts() + "/" + getSubTopic(i).getPart();
-  }
-  eq MqttRoot.getTopic(int i).allParts() {
-    return getTopic(i).getPart();
-  }
+  syn String MqttTopic.getOutgoingTopic() = getMqttRoot().getOutgoingPrefix() + getTopicString();
 
   //--- getMqttSender (should be cached) ---
   cache MqttRoot.getMqttSender();
@@ -78,12 +55,12 @@ aspect MQTT {
   inh MqttRoot MqttTopic.getMqttRoot();
 
   eq MqttRoot.getTopic().getMqttRoot() = this;
-  eq MqttTopic.getSubTopic().getMqttRoot() = getMqttRoot();
 
   /**
    * Sends the current state via MQTT.
    */
-  public void Item.sendState() throws Exception {
+  refine ItemHandling protected void Item.sendState() throws Exception {
+    refined();
     if (getTopic() != null) {
       getTopic().send(getStateAsString());
     }
@@ -97,4 +74,14 @@ aspect MQTT {
     getMqttRoot().getMqttSender().publish(getOutgoingTopic(), message);
   }
 
+  refine OpenHAB2 public void OpenHAB2Model.addNewItem(Item item) {
+    refined(item);
+    // update mqtt-topic to new mqtt-root
+    JavaUtils.ifPresentOrElse(
+        getRoot().getMqttRoot().resolveTopicSuffix(item.getTopic().getTopicString()),
+        topic -> item.setTopic(topic),
+        () -> de.tudresden.inf.st.eraser.util.ParserUtils.createMqttTopic(item, item.getTopic().getTopicString(), getRoot())
+    );
+  }
+
 }
diff --git a/eraser-base/src/main/jastadd/mqtt.relast b/eraser-base/src/main/jastadd/mqtt.relast
new file mode 100644
index 0000000000000000000000000000000000000000..cf477ff72e303415e47f42c939afe26cdfc4ed54
--- /dev/null
+++ b/eraser-base/src/main/jastadd/mqtt.relast
@@ -0,0 +1,4 @@
+// ----------------    MQTT    ------------------------------
+MqttRoot ::= Topic:MqttTopic* <IncomingPrefix:String> <OutgoingPrefix:String> [Host:ExternalHost] ;
+MqttTopic ::= <TopicString:String> ;
+rel Item.Topic? <-> MqttTopic.Item* ;
diff --git a/eraser-base/src/main/jastadd/openhab.jrag b/eraser-base/src/main/jastadd/openhab.jrag
new file mode 100644
index 0000000000000000000000000000000000000000..56aa89f36ac62aa817352f45a639ae7c4aa62206
--- /dev/null
+++ b/eraser-base/src/main/jastadd/openhab.jrag
@@ -0,0 +1,13 @@
+aspect OpenHAB2 {
+  syn ActivityItem OpenHAB2Model.getActivityItem() {
+    return new ActivityItem();
+  }
+
+  public void OpenHAB2Model.addNewItem(Item item) {
+    JavaUtils.ifPresentOrElse(
+        resolveGroup(de.tudresden.inf.st.eraser.util.ParserUtils.UNKNOWN_GROUP_NAME),
+        group -> group.addItem(item),
+        () -> de.tudresden.inf.st.eraser.util.ParserUtils.createUnknownGroup(this, Collections.singletonList(item)));
+  }
+
+}
diff --git a/eraser-base/src/main/jastadd/openhab.relast b/eraser-base/src/main/jastadd/openhab.relast
new file mode 100644
index 0000000000000000000000000000000000000000..575f875734994ee52ba5259e5cfe570aa488c139
--- /dev/null
+++ b/eraser-base/src/main/jastadd/openhab.relast
@@ -0,0 +1,57 @@
+// ----------------    openHAB    ------------------------------
+OpenHAB2Model ::= Thing* Group* ThingType* ChannelType* ChannelCategory* ItemCategory* /ActivityItem:Item/ ;
+
+abstract ModelElement ::= <ID:String> ;
+abstract LabelledModelElement : ModelElement ::= <Label:String> ;
+abstract DescribableModelElement : LabelledModelElement ::= <Description:String> ;
+
+ThingType : DescribableModelElement ::= Parameter* ;
+rel ThingType.ChannelType* -> ChannelType ;
+
+Thing : LabelledModelElement ::= Channel* ;
+rel Thing.Type -> ThingType ;
+
+ChannelType : DescribableModelElement ::= <ItemType:ItemType> <ReadOnly:boolean> ;
+rel ChannelType.ChannelCategory -> ChannelCategory ;
+
+abstract ChannelCategory ;
+DefaultChannelCategory : ChannelCategory ::= <Value:DefaultChannelCategoryValue> ;
+SimpleChannelCategory : ChannelCategory ::= <Value:String> ;
+
+Channel : ModelElement ::= ;
+rel Channel.Type -> ChannelType ;
+rel Channel.LinkedItem* <-> Item.Channel? ;
+
+Parameter : DescribableModelElement ::= <Type:ParameterValueType> [DefaultValue:ParameterDefaultValue] <Context:String> <Required:boolean> ;
+ParameterDefaultValue ::= <Value:String> ;
+
+abstract Item : LabelledModelElement ::= <_fetched_data:boolean> MetaData:ItemMetaData* [ItemObserver] ;
+rel Item.Category? -> ItemCategory ;
+rel Item.Controlling* <-> Item.ControlledBy* ;
+
+abstract ItemWithBooleanState : Item ::= <_state:boolean> ;
+abstract ItemWithStringState : Item ::= <_state:String> ;
+abstract ItemWithDoubleState : Item ::= <_state:double> ;
+ColorItem : Item ::= <_state:TupleHSB> ;
+DateTimeItem : Item ::= <_state:Instant> ;
+ContactItem : ItemWithBooleanState ;
+DimmerItem : ItemWithDoubleState ;
+ImageItem : ItemWithStringState ;
+LocationItem : ItemWithStringState ;
+NumberItem : ItemWithDoubleState ;
+PlayerItem : ItemWithStringState ;
+RollerShutterItem : ItemWithDoubleState ;
+StringItem : ItemWithStringState ;
+SwitchItem : ItemWithBooleanState ;
+DefaultItem : ItemWithStringState ;
+ActivityItem : ItemWithDoubleState ;
+
+ItemMetaData ::= <Key:String> <Value:String> ;
+
+ItemCategory ::= <Name:String> ;
+
+Group : LabelledModelElement ::= Group* Item* [AggregationFunction:GroupAggregationFunction] ;
+abstract GroupAggregationFunction ;
+SimpleGroupAggregationFunction : GroupAggregationFunction ::= <FunctionName:SimpleGroupAggregationFunctionName> ;
+ParameterizedGroupAggregationFunction : GroupAggregationFunction ::= <FunctionName:ParameterizedGroupAggregationFunctionName>
+                                                                     <Param1:String> <Param2:String> ;
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 9b98f688fce2bba0dadffeb6584f03ff1f28628d..9b9aaba5b25e860177545415c5ca1c088a3e7165 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
@@ -6,8 +6,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.mqtt.MQTTUpdater;
 import de.tudresden.inf.st.eraser.util.ParserUtils;
-import org.apache.commons.math3.linear.MatrixUtils;
-import org.apache.commons.math3.linear.RealMatrix;
 import org.apache.logging.log4j.LogManager;
 
 import java.io.*;
@@ -16,7 +14,7 @@ import java.io.*;
  * Main entry point for testing eraser.
  * @author rschoene - Initial contribution
  */
-@SuppressWarnings({"unused", "WeakerAccess", "RedundantThrows"})
+@SuppressWarnings({"unused", "RedundantThrows"})
 public class Main {
 
   public static void main(String[] args) throws IOException, Parser.Exception {
@@ -25,35 +23,6 @@ public class Main {
 //    Root model = importFromOpenHab();
 //    testPrinterWith(model);
 //    testUpdaterWith(model);
-    testXY_to_RGB();
-  }
-
-  private static void testXY_to_RGB() {
-    /*
-    XYZ to RGB [M]-1
-     2.0413690 -0.5649464 -0.3446944
-    -0.9692660  1.8760108  0.0415560
-     0.0134474 -0.1183897  1.0154096
-     */
-    double[][] matrixData = { { 2.0413690, -0.5649464, -0.3446944},
-                              {-0.9692660,  1.8760108,  0.0415560},
-                              { 0.0134474, -0.1183897,  1.0154096}};
-    RealMatrix mInverted = MatrixUtils.createRealMatrix(matrixData);
-    BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
-
-    while (true) {
-      try {
-        double x = readFromSystemIn(in, "x:");
-        double y = readFromSystemIn(in, "y:");
-        double z = 1;
-        RealMatrix xyz = MatrixUtils.createColumnRealMatrix(new double[] {x,y,z});
-        RealMatrix result = mInverted.multiply(xyz);
-        System.out.println(result);
-      } catch (IOException | NumberFormatException e) {
-        e.printStackTrace();
-        break;
-      }
-    }
   }
 
   private static double readFromSystemIn(BufferedReader in, String prompt) throws IOException {
@@ -70,7 +39,7 @@ public class Main {
 
   private static void testPrinterWith(Root model) {
     model.flushTreeCache();
-    System.out.println("Got model: " + model.description());
+    System.out.println("Got model: " + model.getOpenHAB2Model().description());
     System.out.println("PrettyPrinted:");
     System.out.println(model.prettyPrint());
   }
@@ -79,7 +48,7 @@ public class Main {
     Root model;
 //    model = importFromOpenHab();
     model = importFromFile();
-    System.out.println("Got model: " + model.description());
+    System.out.println("Got model: " + model.getOpenHAB2Model().description());
 //    JsonSerializer.write(model, "openhab2-data.json");
     testUpdaterWith(model);
   }
@@ -106,7 +75,7 @@ public class Main {
 
   public static Root importFromOpenHab() {
     OpenHab2Importer importer = new OpenHab2Importer();
-    return importer.importFrom("192.168.1.250", 8080);
+    return importer.importFrom("192.168.1.250", 8080).getRoot();
   }
 
   public static Root importFromFile() {
diff --git a/eraser-base/src/main/java/de/tudresden/inf/st/eraser/deserializer/ASTNodeDeserializer.java b/eraser-base/src/main/java/de/tudresden/inf/st/eraser/deserializer/ASTNodeDeserializer.java
index 95f1d470c014ce4a123b5e01e33e7b049ba8ceff..d349331de9e8e43b7513001c659f94e61cc94ae8 100644
--- a/eraser-base/src/main/java/de/tudresden/inf/st/eraser/deserializer/ASTNodeDeserializer.java
+++ b/eraser-base/src/main/java/de/tudresden/inf/st/eraser/deserializer/ASTNodeDeserializer.java
@@ -48,12 +48,17 @@ public class ASTNodeDeserializer extends StdDeserializer<ASTNode> {
     r.put(c.getName(), ((node, model) -> f.apply(model, termValue(node, terminalName))));
   }
 
+  private void addResolverForOpenHAB2Model(Map<String, ResolveAstNodeForOpenHAB2Model> r, Class<?> c, BiFunction<OpenHAB2Model, String, Optional<? extends ASTNode>> f, String terminalName) {
+    r.put(c.getName(), ((node, model) -> f.apply(model, termValue(node, terminalName))));
+  }
+
   private Map<String, ResolveAstNode> resolvers = new HashMap<>();
+  private Map<String, ResolveAstNodeForOpenHAB2Model> resolversForOpenHAB2Model = new HashMap<>();
 
   private void initResolvers() {
-    addResolver(resolvers, ThingType.class, Root::resolveThingType, "ID");
-    addResolver(resolvers, ChannelType.class, Root::resolveChannelType, "ID");
-    addResolver(resolvers, Item.class, Root::resolveItem, "ID");
+    addResolverForOpenHAB2Model(resolversForOpenHAB2Model, ThingType.class, OpenHAB2Model::resolveThingType, "ID");
+    addResolverForOpenHAB2Model(resolversForOpenHAB2Model, ChannelType.class, OpenHAB2Model::resolveChannelType, "ID");
+    addResolverForOpenHAB2Model(resolversForOpenHAB2Model, Item.class, OpenHAB2Model::resolveItem, "ID");
     addResolver(resolvers, MqttTopic.class, Root::resolveMqttTopic, "IncomingTopic");
   }
 
@@ -363,11 +368,15 @@ public class ASTNodeDeserializer extends StdDeserializer<ASTNode> {
   }
 
   }
-
 interface ResolveAstNode {
   Optional<? extends ASTNode> resolve(JsonNode node, Root model) throws IOException;
 }
 
+
+interface ResolveAstNodeForOpenHAB2Model {
+  Optional<? extends ASTNode> resolve(JsonNode node, OpenHAB2Model model) throws IOException;
+}
+
 class ResolveLater {
   JsonNode node;
 
diff --git a/eraser-base/src/main/java/de/tudresden/inf/st/eraser/jastadd/model/Action2EditConsumer.java b/eraser-base/src/main/java/de/tudresden/inf/st/eraser/jastadd/model/Action2EditConsumer.java
new file mode 100644
index 0000000000000000000000000000000000000000..e37a3238af4356278c8e1d4bb8808ab9b1948fd1
--- /dev/null
+++ b/eraser-base/src/main/java/de/tudresden/inf/st/eraser/jastadd/model/Action2EditConsumer.java
@@ -0,0 +1,11 @@
+package de.tudresden.inf.st.eraser.jastadd.model;
+
+import java.util.function.Consumer;
+
+/**
+ * Consumer used to make edits in the action part of ECA rules.
+ *
+ * @author rschoene - Initial contribution
+ */
+public interface Action2EditConsumer extends Consumer<Item> {
+}
diff --git a/eraser-base/src/main/java/de/tudresden/inf/st/eraser/jastadd/model/DoubleDoubleFunction.java b/eraser-base/src/main/java/de/tudresden/inf/st/eraser/jastadd/model/DoubleDoubleFunction.java
new file mode 100644
index 0000000000000000000000000000000000000000..6a8625d2ae9be07861d1e80312b7bf2a4fb39494
--- /dev/null
+++ b/eraser-base/src/main/java/de/tudresden/inf/st/eraser/jastadd/model/DoubleDoubleFunction.java
@@ -0,0 +1,11 @@
+package de.tudresden.inf.st.eraser.jastadd.model;
+
+import java.util.function.Function;
+
+/**
+ * Function, that takes a double as input and outputs a double.
+ *
+ * @author rschoene - Initial contribution
+ */
+public interface DoubleDoubleFunction extends Function<Double, Double> {
+}
diff --git a/eraser-base/src/main/java/de/tudresden/inf/st/eraser/jastadd/model/InfluxAdapter.java b/eraser-base/src/main/java/de/tudresden/inf/st/eraser/jastadd/model/InfluxAdapter.java
index b5e0ea7fa11960184c09f3a328e02ecee3d0608f..15704f45da1f36963a0d4c884bb1e70b9721b126 100644
--- a/eraser-base/src/main/java/de/tudresden/inf/st/eraser/jastadd/model/InfluxAdapter.java
+++ b/eraser-base/src/main/java/de/tudresden/inf/st/eraser/jastadd/model/InfluxAdapter.java
@@ -46,7 +46,7 @@ public abstract class InfluxAdapter implements AutoCloseable {
   /**
    * Forces this adapter to compute {@link #asyncQuery(String, String, Class, QueryResultCallback)} synchronously.
    */
-  public void diableAsyncQuery() {
+  public void disableAsyncQuery() {
     this.reallyAsync = false;
   }
 
diff --git a/eraser-base/src/main/java/de/tudresden/inf/st/eraser/jastadd/model/InternalMachineLearningHandler.java b/eraser-base/src/main/java/de/tudresden/inf/st/eraser/jastadd/model/InternalMachineLearningHandler.java
new file mode 100644
index 0000000000000000000000000000000000000000..75d2a1d8cfc28c0293f1d55cc4e6714a6e37818a
--- /dev/null
+++ b/eraser-base/src/main/java/de/tudresden/inf/st/eraser/jastadd/model/InternalMachineLearningHandler.java
@@ -0,0 +1,59 @@
+package de.tudresden.inf.st.eraser.jastadd.model;
+
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+import java.time.Instant;
+import java.util.List;
+
+/**
+ * Adapter for internally held machine learning models.
+ *
+ * @author rschoene - Initial contribution
+ */
+public class InternalMachineLearningHandler implements MachineLearningEncoder, MachineLearningDecoder {
+
+  private static final Logger logger = LogManager.getLogger(InternalMachineLearningHandler.class);
+  private InternalMachineLearningModel model;
+
+  public InternalMachineLearningHandler setModel(InternalMachineLearningModel model) {
+    this.model = model;
+    return this;
+  }
+
+  @Override
+  public void setKnowledgeBaseRoot(Root root) {
+    // ignored
+  }
+
+  @Override
+  public void newData(List<Item> changedItems) {
+    logger.debug("Ignored new data of {}", changedItems);
+  }
+
+  @Override
+  public List<Item> getTargets() {
+    return model.getTargetItems();
+  }
+
+  @Override
+  public List<Item> getRelevantItems() {
+    return model.getRelevantItems();
+  }
+
+  @Override
+  public void triggerTraining() {
+    logger.debug("Ignored training trigger.");
+  }
+
+  @Override
+  public MachineLearningResult classify() {
+    List<ItemPreference> preferences = model.classify().computePreferences();
+    return new InternalMachineLearningResult(preferences);
+  }
+
+  @Override
+  public Instant lastModelUpdate() {
+    return null;
+  }
+}
diff --git a/eraser-base/src/main/java/de/tudresden/inf/st/eraser/jastadd/model/InternalMachineLearningResult.java b/eraser-base/src/main/java/de/tudresden/inf/st/eraser/jastadd/model/InternalMachineLearningResult.java
new file mode 100644
index 0000000000000000000000000000000000000000..087020b383fee89ea839b2056e22072af799a9f3
--- /dev/null
+++ b/eraser-base/src/main/java/de/tudresden/inf/st/eraser/jastadd/model/InternalMachineLearningResult.java
@@ -0,0 +1,21 @@
+package de.tudresden.inf.st.eraser.jastadd.model;
+
+import java.util.List;
+
+/**
+ * Result of a classification returned by an internally held machine learning model.
+ *
+ * @author rschoene - Initial contribution
+ */
+public class InternalMachineLearningResult implements MachineLearningResult {
+  private final List<ItemPreference> preferences;
+
+  InternalMachineLearningResult(List<ItemPreference> preferences) {
+    this.preferences = preferences;
+  }
+
+  @Override
+  public List<ItemPreference> getPreferences() {
+    return this.preferences;
+  }
+}
diff --git a/eraser-base/src/main/java/de/tudresden/inf/st/eraser/jastadd/model/ItemsToStringFunction.java b/eraser-base/src/main/java/de/tudresden/inf/st/eraser/jastadd/model/ItemsToStringFunction.java
new file mode 100644
index 0000000000000000000000000000000000000000..ec5e4772263fe2588baab69759dddf375676188f
--- /dev/null
+++ b/eraser-base/src/main/java/de/tudresden/inf/st/eraser/jastadd/model/ItemsToStringFunction.java
@@ -0,0 +1,11 @@
+package de.tudresden.inf.st.eraser.jastadd.model;
+
+import java.util.function.Function;
+
+/**
+ * Merge states from given items into one (generic) state represented as a string.
+ *
+ * @author rschoene - Initial contribution
+ */
+public interface ItemsToStringFunction extends Function<Iterable<Item>, String> {
+}
diff --git a/eraser-base/src/main/java/de/tudresden/inf/st/eraser/jastadd/model/MachineLearningDecoder.java b/eraser-base/src/main/java/de/tudresden/inf/st/eraser/jastadd/model/MachineLearningDecoder.java
new file mode 100644
index 0000000000000000000000000000000000000000..045c7695ba325429a3ba82a4e1319e0926aa043b
--- /dev/null
+++ b/eraser-base/src/main/java/de/tudresden/inf/st/eraser/jastadd/model/MachineLearningDecoder.java
@@ -0,0 +1,28 @@
+package de.tudresden.inf.st.eraser.jastadd.model;
+
+import java.time.Instant;
+
+/**
+ * This interface represents the connection from a machine learning model back to the knowledge base.
+ * It decodes the output of the machine learning model and outputs the result of the classification.
+ *
+ * @author rschoene - Initial contribution
+ */
+@SuppressWarnings("unused")
+public interface MachineLearningDecoder extends MachineLearningSetRoot {
+
+  /**
+   * Execute the machine learning model and returns the classification result.
+   * @return the result of the classification
+   */
+  MachineLearningResult classify();
+
+  // less important
+
+  /**
+   * Returns the time when the model was last updated, i.e., when the last training was completed.
+   * @return the time when the model was last updated, or <code>null</code> if the model was not trained yet
+   */
+  Instant lastModelUpdate();
+
+}
diff --git a/eraser-base/src/main/java/de/tudresden/inf/st/eraser/jastadd/model/MachineLearningEncoder.java b/eraser-base/src/main/java/de/tudresden/inf/st/eraser/jastadd/model/MachineLearningEncoder.java
new file mode 100644
index 0000000000000000000000000000000000000000..23ca8959f468b8210c287b163a217a0224f7d283
--- /dev/null
+++ b/eraser-base/src/main/java/de/tudresden/inf/st/eraser/jastadd/model/MachineLearningEncoder.java
@@ -0,0 +1,44 @@
+package de.tudresden.inf.st.eraser.jastadd.model;
+
+import java.util.List;
+
+/**
+ * This interface represents the connection from knowledge base to one machine learning model.
+ * It takes information from the knowledge base, and encodes them to a representation that is readable both for
+ * the used technique and the purpose of the machine learning model.
+ *
+ * @author rschoene - Initial contribution
+ */
+@SuppressWarnings("unused")
+public interface MachineLearningEncoder extends MachineLearningSetRoot {
+
+  /**
+   * Update when new data is available.
+   * @param changedItems A list of items whose state has changed
+   */
+  void newData(List<Item> changedItems);
+
+  // to be discussed, in which form this is specified
+
+  /**
+   * Get the items that this model is supposed to change.
+   * @return the list of targeted items
+   */
+  List<Item> getTargets();
+
+  // to be discussed, in which form this is specified
+
+  /**
+   * Get the items which are relevant for the decision making of this model.
+   * @return the list of items relevant for decision making
+   */
+  List<Item> getRelevantItems();
+
+  // to be discussed, if this is necessary
+
+  /**
+   * Explicit hint for this model to start/trigger training. The model might ignore this hint.
+   */
+  void triggerTraining();
+
+}
diff --git a/eraser-base/src/main/java/de/tudresden/inf/st/eraser/jastadd/model/MachineLearningResult.java b/eraser-base/src/main/java/de/tudresden/inf/st/eraser/jastadd/model/MachineLearningResult.java
new file mode 100644
index 0000000000000000000000000000000000000000..eebd16d3679c1531cb02d1ba1abd0c1610303668
--- /dev/null
+++ b/eraser-base/src/main/java/de/tudresden/inf/st/eraser/jastadd/model/MachineLearningResult.java
@@ -0,0 +1,25 @@
+package de.tudresden.inf.st.eraser.jastadd.model;
+
+import de.tudresden.inf.st.eraser.jastadd.model.ItemPreference;
+
+import java.util.List;
+
+/**
+ * Representation of a classification result using a MachineLearningModel.
+ *
+ * @author rschoene - Initial contribution
+ */
+@SuppressWarnings("unused")
+public interface MachineLearningResult {
+
+  // Object rawClass();
+
+  // double rawConfidence();
+
+  // can be used for both activity and preferences
+  /**
+   * Get the result as a list of item preferences, i.e., new states to be set for those items.
+   * @return the classification result as item preferences
+   */
+  List<ItemPreference> getPreferences();
+}
diff --git a/eraser-base/src/main/java/de/tudresden/inf/st/eraser/jastadd/model/MachineLearningSetRoot.java b/eraser-base/src/main/java/de/tudresden/inf/st/eraser/jastadd/model/MachineLearningSetRoot.java
new file mode 100644
index 0000000000000000000000000000000000000000..13fec09c243d0d15c4f5e41a8c8851321c26d5d5
--- /dev/null
+++ b/eraser-base/src/main/java/de/tudresden/inf/st/eraser/jastadd/model/MachineLearningSetRoot.java
@@ -0,0 +1,17 @@
+package de.tudresden.inf.st.eraser.jastadd.model;
+
+/**
+ * Common interface for both {@link MachineLearningDecoder} and {@link MachineLearningEncoder}.
+ *
+ * @author rschoene - Initial contribution
+ */
+public interface MachineLearningSetRoot {
+
+  /**
+   * Informs this handler of the knowledge base.
+   * This method is called before any other of the interface methods.
+   * @param root The root node of the knowledge base
+   */
+  void setKnowledgeBaseRoot(Root root);
+
+}
diff --git a/eraser-base/src/main/java/de/tudresden/inf/st/eraser/jastadd/model/NewStateProvider.java b/eraser-base/src/main/java/de/tudresden/inf/st/eraser/jastadd/model/NewStateProvider.java
new file mode 100644
index 0000000000000000000000000000000000000000..eecbc504eda4ffd73a336032952e96ee2d56d34e
--- /dev/null
+++ b/eraser-base/src/main/java/de/tudresden/inf/st/eraser/jastadd/model/NewStateProvider.java
@@ -0,0 +1,11 @@
+package de.tudresden.inf.st.eraser.jastadd.model;
+
+import java.util.function.Supplier;
+
+/**
+ * Provide a new state in form of a string which is set to the item.
+ *
+ * @author rschoene - Initial contribution
+ */
+public interface NewStateProvider extends Supplier<String> {
+}
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
index a3520402e367ba88ab6f5f218915f3b0c5447f84..8e9ee2ad31f8d16ff415bcb7fe82bd34eb87f02a 100644
--- 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
@@ -8,9 +8,9 @@ import java.util.Objects;
  * @author rschoene - Initial contribution
  */
 public class TupleHSB implements Cloneable {
-  public int hue;
-  public int saturation;
-  public int brightness;
+  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;
@@ -19,6 +19,30 @@ public class TupleHSB implements Cloneable {
     return result;
   }
 
+  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);
   }
diff --git a/eraser-base/src/main/java/de/tudresden/inf/st/eraser/openhab2/OpenHab2Importer.java b/eraser-base/src/main/java/de/tudresden/inf/st/eraser/openhab2/OpenHab2Importer.java
index b7deb57f7fe10e46fe555c74e75eeea864c857bc..4e1742c7f48b31e040cc70866e8ecd5070622172 100644
--- a/eraser-base/src/main/java/de/tudresden/inf/st/eraser/openhab2/OpenHab2Importer.java
+++ b/eraser-base/src/main/java/de/tudresden/inf/st/eraser/openhab2/OpenHab2Importer.java
@@ -26,6 +26,7 @@ import java.util.stream.Collectors;
 public class OpenHab2Importer {
 
   protected static final String thingTypesUrl = "http://%s/rest/thing-types";
+  protected static final String thingTypeDetailUrl = "http://%s/rest/thing-types/%s";
   protected static final String channelTypeUrl = "http://%s/rest/channel-types";
   protected static final String thingsUrl = "http://%s/rest/things";
   protected static final String itemsUrl = "http://%s/rest/items";
@@ -41,7 +42,7 @@ public class OpenHab2Importer {
     nonDefaultChannelCategories = new HashSet<>();
   }
 
-  public Root importFrom(String host, int port) {
+  public OpenHAB2Model importFrom(String host, int port) {
     /*
     Plan:
     - requesting: thing-types, channel-types, things, items, links
@@ -57,7 +58,8 @@ public class OpenHab2Importer {
     mapper.registerModule(module);
 
     try {
-      Root model = Root.createEmptyRoot();
+      Root root = Root.createEmptyRoot();
+      OpenHAB2Model model = root.getOpenHAB2Model();
       ThingTypeData[] thingTypeList = mapper.readValue(makeURL(thingTypesUrl, hostAndPort), ThingTypeData[].class);
       logger.info("Read a total of {} thing type(s).", thingTypeList.length);
       update(model, thingTypeList);
@@ -78,19 +80,14 @@ public class OpenHab2Importer {
       logger.info("Read a total of {} link(s).", linkList.length);
       update(model, linkList);
 
-      // create empty MQTT root
-      MqttRoot mqttRoot = new MqttRoot();
-      mqttRoot.setHostByName(host);
-      model.setMqttRoot(mqttRoot);
-
-      // create empty Influx root
-      InfluxRoot influxRoot = InfluxRoot.createDefault();
-      influxRoot.setHostByName(host);
-      model.setInfluxRoot(influxRoot);
-
+      // get parameters of thing types
+      for (ThingType thingType : model.getThingTypeList()) {
+        ThingTypeData data = mapper.readValue(makeURL(thingTypeDetailUrl, hostAndPort, thingType.getID()), ThingTypeData.class);
+        update(thingType, data);
+      }
       return model;
     } catch (IOException e) {
-      e.printStackTrace();
+      logger.catching(e);
       return null;
     }
   }
@@ -99,7 +96,11 @@ public class OpenHab2Importer {
     return URI.create(String.format(formatUrlString, hostAndPort)).toURL();
   }
 
-  private void update(Root model, ThingTypeData[] thingTypeList) {
+  protected URL makeURL(String formatUrlString, String hostAndPort, String id) throws MalformedURLException {
+    return URI.create(String.format(formatUrlString, hostAndPort, id)).toURL();
+  }
+
+  private void update(OpenHAB2Model model, ThingTypeData[] thingTypeList) {
     for (ThingTypeData thingTypeData : thingTypeList) {
       ThingType thingType = new ThingType();
       thingType.setID(thingTypeData.UID);
@@ -109,7 +110,7 @@ public class OpenHab2Importer {
     }
   }
 
-  private void update(Root model, ChannelTypeData[] channelTypeList) {
+  private void update(OpenHAB2Model model, ChannelTypeData[] channelTypeList) {
     for (ChannelTypeData channelTypeData : channelTypeList) {
       ChannelType channelType = new ChannelType();
       channelType.setID(channelTypeData.UID);
@@ -162,7 +163,7 @@ public class OpenHab2Importer {
     }
   }
 
-  private void update(Root model, ThingData[] thingList) {
+  private void update(OpenHAB2Model model, ThingData[] thingList) {
     for (ThingData thingData : thingList) {
       Thing thing = new Thing();
       thing.setID(thingData.UID);
@@ -181,7 +182,7 @@ public class OpenHab2Importer {
     }
   }
 
-  private void update(Root model, AbstractItemData[] itemList) {
+  private void update(OpenHAB2Model model, AbstractItemData[] itemList) {
     List<Tuple<Group, GroupItemData>> groupsWithMembers = new ArrayList<>();
     List<Tuple<Group, GroupItemData>> groupsInGroups = new ArrayList<>();
     List<Tuple<Item, AbstractItemData>> itemsInGroups = new ArrayList<>();
@@ -234,11 +235,21 @@ public class OpenHab2Importer {
           }));
         }
         // if state is not set in openHAB, then use default state for the item
+        item.disableSendState();
         if (itemData.state.equals(NULL_STATE)) {
           item.setStateToDefault();
         } else {
           item.setStateFromString(itemData.state, false);
         }
+        item.enableSendState();
+        if (itemData.metadata != null) {
+          for (Map.Entry<String, MetaDataData> entry : itemData.metadata.entrySet()) {
+            logger.debug("Add metadata for namespace {}", entry.getKey());
+            for (Map.Entry<String, String> metaDataEntry : entry.getValue().config.entrySet()) {
+              item.addMetaData(new ItemMetaData(metaDataEntry.getKey(), metaDataEntry.getValue()));
+            }
+          }
+        }
         if (itemData.groupNames.size() > 0) {
           itemsInGroups.add(Tuple.of(item, itemData));
         } else {
@@ -288,15 +299,38 @@ public class OpenHab2Importer {
     }
   }
 
-  private void update(Root model, LinkData[] linkList) {
+  private void update(OpenHAB2Model model, LinkData[] linkList) {
     for (LinkData linkData : linkList) {
-      Link link = new Link();
       ifPresent(model.resolveChannel(linkData.channelUID), "Channel", linkData,
-          channel -> channel.addLink(link));
-      ifPresent(model.resolveItem(linkData.itemName), "Item", linkData, link::setItem);
+          channel -> ifPresent(model.resolveItem(linkData.itemName), "Item", linkData, channel::addLinkedItem));
     }
   }
 
+  private void update(ThingType thingType, ThingTypeData data) {
+    // just set parameters
+    if (data == null || data.configParameters == null) {
+      logger.warn("No parameters given for thing type {}", thingType.getID());
+      return;
+    }
+    for (ParameterData parameterData : data.configParameters) {
+      Parameter parameter = new Parameter();
+      parameter.setID(parameterData.name);
+      parameter.setLabel(parameterData.label);
+      parameter.setDescription(parameterData.description);
+      parameter.setContext(parameterData.context);
+      if (parameterData.defaultValue != null) {
+        parameter.setDefaultValue(new ParameterDefaultValue(parameterData.defaultValue));
+      }
+      parameter.setRequired(parameterData.required);
+      parameter.setType(ParameterValueType.valueOf(titleCase(parameterData.type)));
+      thingType.addParameter(parameter);
+    }
+  }
+
+  private String titleCase(String type) {
+    return Character.toTitleCase(type.charAt(0)) + type.substring(1).toLowerCase();
+  }
+
   @SuppressWarnings("OptionalUsedAsFieldOrParameterType")
   private <T> void ifPresent(Optional<T> opt, String type, Object data,
                              Consumer<? super T> consumer) {
@@ -307,7 +341,7 @@ public class OpenHab2Importer {
     }
   }
 
-  public Root importFrom(URL baseUrl) {
+  public OpenHAB2Model importFrom(URL baseUrl) {
     return importFrom(baseUrl.getHost(),
         baseUrl.getPort() == -1 ? baseUrl.getDefaultPort() : baseUrl.getPort());
   }
diff --git a/eraser-base/src/main/java/de/tudresden/inf/st/eraser/openhab2/data/AbstractItemData.java b/eraser-base/src/main/java/de/tudresden/inf/st/eraser/openhab2/data/AbstractItemData.java
index 519a0c776fde351d371f4deafe3abac76d0a7f6f..4ea65352764f22f9053544d7b91b3e0a1773877a 100644
--- a/eraser-base/src/main/java/de/tudresden/inf/st/eraser/openhab2/data/AbstractItemData.java
+++ b/eraser-base/src/main/java/de/tudresden/inf/st/eraser/openhab2/data/AbstractItemData.java
@@ -3,6 +3,7 @@ package de.tudresden.inf.st.eraser.openhab2.data;
 import com.fasterxml.jackson.annotation.JsonInclude;
 
 import java.util.List;
+import java.util.Map;
 
 /**
  * Abstract JSON return type for either an item or a group in openHAB 2.
@@ -23,4 +24,5 @@ public class AbstractItemData {
   public String transformedState;
   public StateDescriptionData stateDescription;
   public Boolean editable;
+  public Map<String, MetaDataData> metadata;
 }
diff --git a/eraser-base/src/main/java/de/tudresden/inf/st/eraser/openhab2/data/MetaDataData.java b/eraser-base/src/main/java/de/tudresden/inf/st/eraser/openhab2/data/MetaDataData.java
new file mode 100644
index 0000000000000000000000000000000000000000..d15cbef4177f279de3cdd318b3ca2853cab2be94
--- /dev/null
+++ b/eraser-base/src/main/java/de/tudresden/inf/st/eraser/openhab2/data/MetaDataData.java
@@ -0,0 +1,17 @@
+package de.tudresden.inf.st.eraser.openhab2.data;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+
+import java.util.Map;
+
+/**
+ * Item Meta data.
+ *
+ * @author rschoene - Initial contribution
+ */
+@SuppressWarnings("unused")
+@JsonInclude(JsonInclude.Include.NON_DEFAULT)
+public class MetaDataData {
+  public String value;
+  public Map<String, String> config;
+}
diff --git a/eraser-base/src/main/java/de/tudresden/inf/st/eraser/openhab2/data/ParameterData.java b/eraser-base/src/main/java/de/tudresden/inf/st/eraser/openhab2/data/ParameterData.java
index 12571c8caa6f1c5348ca65a94ca0e7e41e57ad00..ce6b5c350c438e4755ff0e6eaa960d192492ba01 100644
--- a/eraser-base/src/main/java/de/tudresden/inf/st/eraser/openhab2/data/ParameterData.java
+++ b/eraser-base/src/main/java/de/tudresden/inf/st/eraser/openhab2/data/ParameterData.java
@@ -9,7 +9,7 @@ import java.util.List;
  *
  * @author rschoene - Initial contribution
  */
-@SuppressWarnings("unused")
+//@SuppressWarnings("unused")
 @JsonInclude(JsonInclude.Include.NON_DEFAULT)
 public class ParameterData {
   public String context;
@@ -19,24 +19,24 @@ public class ParameterData {
   public String name;
   public Boolean required;
   public String type;
-  public Integer min;
+//  public Integer min;
   public Integer max;
-  public Integer stepsize;
+//  public Integer stepsize;
   public String pattern;
   public Boolean readOnly;
-  public Boolean multiple;
-  public Integer multipleLimit;
-  public String groupName;
-  public Boolean advanced;
-  public Boolean verify;
-  public Boolean limitToOptions;
+//  public Boolean multiple;
+//  public Integer multipleLimit;
+//  public String groupName;
+//  public Boolean advanced;
+//  public Boolean verify;
+//  public Boolean limitToOptions;
   public String unit;
-  public String unitLabel;
-  public List<OptionData> options;
-  public List<FilterCriteriaData> filterCriteria;
+//  public String unitLabel;
+//  public List<OptionData> options;
+//  public List<FilterCriteriaData> filterCriteria;
 
-  public static class FilterCriteriaData {
-    public String name;
-    public String value;
-  }
+//  public static class FilterCriteriaData {
+//    public String name;
+//    public String value;
+//  }
 }
diff --git a/eraser-base/src/main/java/de/tudresden/inf/st/eraser/openhab2/data/ThingTypeData.java b/eraser-base/src/main/java/de/tudresden/inf/st/eraser/openhab2/data/ThingTypeData.java
index 8b73dd318654a000658fa2160cb4f1f63972f11e..a4fd4cbca59cba456cd273b55aa2ed435b059e85 100644
--- a/eraser-base/src/main/java/de/tudresden/inf/st/eraser/openhab2/data/ThingTypeData.java
+++ b/eraser-base/src/main/java/de/tudresden/inf/st/eraser/openhab2/data/ThingTypeData.java
@@ -19,4 +19,5 @@ public class ThingTypeData {
   public boolean listed;
   public List<String> supportedBridgeTypeUIDs;
   public boolean bridge;
+  public List<ParameterData> configParameters;
 }
diff --git a/eraser-base/src/main/java/de/tudresden/inf/st/eraser/openhab2/mqtt/MQTTUpdater.java b/eraser-base/src/main/java/de/tudresden/inf/st/eraser/openhab2/mqtt/MQTTUpdater.java
index 2bf5880087ccc171ea4e2799c0db4fa1bda11e9d..647114843839aef335b5337cb62e744870d3a0e3 100644
--- a/eraser-base/src/main/java/de/tudresden/inf/st/eraser/openhab2/mqtt/MQTTUpdater.java
+++ b/eraser-base/src/main/java/de/tudresden/inf/st/eraser/openhab2/mqtt/MQTTUpdater.java
@@ -1,23 +1,15 @@
 package de.tudresden.inf.st.eraser.openhab2.mqtt;
 
+import de.tudresden.inf.st.eraser.jastadd.model.ExternalHost;
 import de.tudresden.inf.st.eraser.jastadd.model.Item;
 import de.tudresden.inf.st.eraser.jastadd.model.Root;
 import de.tudresden.inf.st.eraser.util.MqttReceiver;
 import org.apache.logging.log4j.LogManager;
 import org.apache.logging.log4j.Logger;
-import org.fusesource.hawtbuf.Buffer;
-import org.fusesource.hawtbuf.UTF8Buffer;
-import org.fusesource.mqtt.client.*;
+import org.fusesource.mqtt.client.QoS;
 
 import java.io.IOException;
-import java.net.URI;
-import java.util.Arrays;
-import java.util.Objects;
 import java.util.concurrent.TimeUnit;
-import java.util.concurrent.atomic.AtomicReference;
-import java.util.concurrent.locks.Condition;
-import java.util.concurrent.locks.Lock;
-import java.util.concurrent.locks.ReentrantLock;
 
 /**
  * Update an imported model by subscribing to MQTT topics.
@@ -36,28 +28,33 @@ public class MQTTUpdater implements AutoCloseable {
     this.delegatee = new MqttReceiver();
   }
 
-  public MQTTUpdater(Root model) throws IllegalArgumentException {
+  public MQTTUpdater(Root root) throws IllegalArgumentException {
     this();
-    this.setModel(model);
+    this.setRoot(root);
   }
 
   /**
-   * Sets the model to update
-   * @param model the model to update
+   * Sets the model root to update
+   * @param root the model root to update
    */
-  public void setModel(Root model) {
-    delegatee.setHost(model.getMqttRoot().getHost().getHostName());
+  public void setRoot(Root root) {
+    ExternalHost host = root.getMqttRoot().getHost();
+    delegatee.setHost(host.getHostName(), host.getPort());
     delegatee.setOnMessage((topicString, message) ->
-        model.getMqttRoot().resolveTopic(topicString).ifPresent(topic ->
-            itemUpdate(topic.getItem(), message)));
-    delegatee.setTopicsForSubscription(model.getMqttRoot().getIncomingPrefix() + "#");
+        root.getMqttRoot().resolveTopic(topicString).ifPresent(topic ->
+            topic.getItems().forEach(
+                item -> itemUpdate(item, message))));
+    delegatee.setTopicsForSubscription(root.getMqttRoot().getIncomingPrefix() + "#");
     delegatee.setQoSForSubscription(QoS.AT_LEAST_ONCE);
   }
 
   private void itemUpdate(Item item, String state) {
-    this.logger.debug("Update state of {} [{}] from '{}' to '{}'.",
-        item.getLabel(), item.getID(), item.getStateAsString(), state);
-    item.setStateFromString(state, false);
+    String oldState = item.getStateAsString();
+    if (oldState == null || !oldState.equals(state)) {
+      this.logger.debug("Update state of {} [{}] from '{}' to '{}'.",
+          item.getLabel(), item.getID(), oldState, state);
+      item.setStateFromString(state, false);
+    }
   }
 
   /**
diff --git a/eraser-base/src/main/java/de/tudresden/inf/st/eraser/parser/EraserParserHelper.java b/eraser-base/src/main/java/de/tudresden/inf/st/eraser/parser/EraserParserHelper.java
index 5b1ce8810e96c928bf5f7b722698bd76a66b42b2..5d1b1af27c4bf36e15768bd12facc02ca3a1e773 100644
--- a/eraser-base/src/main/java/de/tudresden/inf/st/eraser/parser/EraserParserHelper.java
+++ b/eraser-base/src/main/java/de/tudresden/inf/st/eraser/parser/EraserParserHelper.java
@@ -1,6 +1,7 @@
 package de.tudresden.inf.st.eraser.parser;
 
 import de.tudresden.inf.st.eraser.jastadd.model.*;
+import de.tudresden.inf.st.eraser.util.JavaUtils;
 import de.tudresden.inf.st.eraser.util.ParserUtils;
 import org.apache.logging.log4j.LogManager;
 import org.apache.logging.log4j.Logger;
@@ -28,9 +29,11 @@ public class EraserParserHelper {
   private Map<Channel, String> missingChannelTypeMap = new HashMap<>();
   private Map<Item, String> missingTopicMap = new HashMap<>();
   private Map<Item, String> missingItemCategoryMap = new HashMap<>();
+  private Map<Designator, String> missingItemForDesignator = new HashMap<>();
 
   private Map<Thing, Iterable<String>> missingChannelListMap = new HashMap<>();
   private Map<Channel, Iterable<String>> missingItemLinkListMap = new HashMap<>();
+  private Map<Item, Iterable<String>> missingControllingListMap = new HashMap<>();
   private Map<Group, Iterable<String>> missingSubGroupListMap = new HashMap<>();
   private Map<Group, Iterable<String>> missingItemListMap = new HashMap<>();
   private Map<ThingType, Iterable<String>> missingChannelTypeListMap = new HashMap<>();
@@ -44,6 +47,7 @@ public class EraserParserHelper {
   private Root root;
 
   private static boolean checkUnusedElements = true;
+  private static Root initialRoot = null;
 
   private class ItemPrototype extends DefaultItem {
 
@@ -57,25 +61,41 @@ public class EraserParserHelper {
     EraserParserHelper.checkUnusedElements = checkUnusedElements;
   }
 
+  public static void setInitialRoot(Root root) {
+    EraserParserHelper.initialRoot = root;
+  }
+
   /**
    * Post processing step after parsing a model, to resolve all references within the model.
-   * @throws java.util.NoSuchElementException if a reference can not be resolved
    */
   public void resolveReferences() {
+    if (this.root == null) {
+      // when parsing expressions
+      this.root = EraserParserHelper.initialRoot != null ? EraserParserHelper.initialRoot : createRoot();
+    }
     if (checkUnusedElements) {
       fillUnused();
     }
     resolve(thingTypeMap, missingThingTypeMap, Thing::setType);
     resolve(channelTypeMap, missingChannelTypeMap, Channel::setType);
+    if (itemMap == null || itemMap.isEmpty()) {
+      missingItemForDesignator.forEach((designator, itemName) ->
+          JavaUtils.ifPresentOrElse(root.getOpenHAB2Model().resolveItem(itemName),
+              designator::setItem,
+              () -> logger.warn("Could not resolve item {} for {}", itemName, designator)));
+    } else {
+      resolve(itemMap, missingItemForDesignator, Designator::setItem);
+    }
     missingTopicMap.forEach((topic, parts) -> ParserUtils.createMqttTopic(topic, parts, this.root));
     this.root.getMqttRoot().ensureCorrectPrefixes();
 
     resolveList(channelMap, missingChannelListMap, Thing::addChannel);
-    resolveList(itemMap, missingItemLinkListMap, (channel, item) -> channel.addLink(new Link(item)));
+    resolveList(itemMap, missingItemLinkListMap, Channel::addLinkedItem);
     resolveList(groupMap, missingSubGroupListMap, Group::addGroup);
     resolveList(itemMap, missingItemListMap, this::addItemToGroup);
     resolveList(channelTypeMap, missingChannelTypeListMap, ThingType::addChannelType);
     resolveList(parameterMap, missingParameterListMap, ThingType::addParameter);
+    resolveList(itemMap, missingControllingListMap, Item::addControlling);
 
     createUnknownGroupIfNecessary();
     createChannelCategories();
@@ -87,7 +107,7 @@ public class EraserParserHelper {
 
   private void addItemToGroup(Group group, Item item) {
     groupedItems.add(item);
-    group.getItemList().insertChild(item, 0);
+    group.addItem(item);
   }
 
   private void fillUnused() {
@@ -112,13 +132,13 @@ public class EraserParserHelper {
           sortedDanglingItems.add(item);
         }
       }
-      ParserUtils.createUnknownGroup(this.root, sortedDanglingItems);
+      ParserUtils.createUnknownGroup(this.root.getOpenHAB2Model(), sortedDanglingItems);
     }
   }
 
   private void createChannelCategories() {
     channelCategoryMap.values().stream().sorted(Comparator.comparing(this::ident)).forEach(
-        cc -> root.addChannelCategory(cc));
+        cc -> root.getOpenHAB2Model().addChannelCategory(cc));
     channelCategoryMap.clear();
   }
 
@@ -126,6 +146,7 @@ public class EraserParserHelper {
     Map<String, ItemCategory> newCategories = new HashMap<>();
     missingItemCategoryMap.forEach((item, category) ->
         item.setCategory(newCategories.computeIfAbsent(category, ItemCategory::new)));
+    newCategories.values().forEach(node -> root.getOpenHAB2Model().addItemCategory(node));
   }
 
   private void checkUnusedElements() {
@@ -136,7 +157,7 @@ public class EraserParserHelper {
     if (elem instanceof ModelElement) {
       return ((ModelElement) elem).getID();
     } else if (elem instanceof MqttTopic) {
-      return safeAllParts((MqttTopic) elem);
+      return ((MqttTopic) elem).getTopicString();
     } else if (elem instanceof DefaultChannelCategory) {
       return ((DefaultChannelCategory) elem).getValue().name();
     } else if (elem instanceof SimpleChannelCategory) {
@@ -145,73 +166,73 @@ public class EraserParserHelper {
     return elem.toString();
   }
 
-  private String safeAllParts(MqttTopic elem) {
-    StringBuilder sb = new StringBuilder(elem.getPart());
-    ASTNode parent;
-    while (true) {
-      parent = elem.getParent();
-      if (parent == null) break;
-      assert parent instanceof List;
-      parent = parent.getParent();
-      if (parent == null || parent instanceof MqttRoot) {
-        break;
-      }
-      elem = (MqttTopic) parent;
-      sb.insert(0, elem.getPart() + "/");
-    }
-    return sb.toString();
+  private <Src extends ASTNode, Target extends ASTNode> void resolveList(
+      Map<String, Target> resolved, Map<Src, Iterable<String>> missing, BiConsumer<Src, Target> adder) {
+    missing.forEach(
+        (elem, keyList) -> keyList.forEach(
+            key -> resolve0(resolved, key, elem, adder)));
+    missing.clear();
   }
 
-  private <Src extends ASTNode, Target extends ASTNode> void resolveList(Map<String, Target> resolved, Map<Src, Iterable<String>> missing, BiConsumer<Src, Target> adder) {
-    missing.forEach((elem, keyList) -> keyList.forEach(key -> {
-      Target value = resolved.get(key);
-      if (value == null) {
-        logger.warn("Reference in {} {} for '{}' cannot be resolved",
-            elem.getClass().getSimpleName(), ident(elem), key);
-        return;
-      }
-      if (checkUnusedElements) {
-        unusedElements.remove(value);
-      }
-      adder.accept(elem, value);
-    }));
+  private <Src extends ASTNode, Target extends ASTNode> void resolve(
+      Map<String, Target> resolved, Map<Src, String> missing, BiConsumer<Src, Target> setter) {
+    missing.forEach(
+        (elem, key) -> resolve0(resolved, key, elem, setter));
     missing.clear();
   }
 
-  private <Src extends ASTNode, Target extends ASTNode> void resolve(Map<String, Target> resolved, Map<Src, String> missing, BiConsumer<Src, Target> setter) {
-    missing.forEach((elem, key) -> {
-      Target value = resolved.get(key);
-      if (value == null) {
-        logger.warn("Reference in {} {} for '{}' cannot be resolved",
-            elem.getClass().getSimpleName(), ident(elem), key);
-        return;
-      }
-      if (checkUnusedElements) {
-        unusedElements.remove(value);
-      }
-      setter.accept(elem, value);
-    });
-    missing.clear();
+  private <Src extends ASTNode, Target extends ASTNode> void resolve0(
+      Map<String, Target> resolved, String key, Src elem, BiConsumer<Src, Target> action) {
+    Target value = resolved.get(key);
+    if (value == null) {
+      logger.warn("Reference in {} {} for '{}' cannot be resolved",
+          elem.getClass().getSimpleName(), ident(elem), key);
+      return;
+    }
+    if (checkUnusedElements) {
+      unusedElements.remove(value);
+    }
+    action.accept(elem, value);
   }
 
+  //--- Thing and ThingType ---
+
   public Thing addThingType(Thing t, String typeName) {
     missingThingTypeMap.put(t, typeName);
     return t;
   }
 
-  public ChannelType setItemType(ChannelType ct, String itemTypeName) {
-    ct.setItemType(ItemType.valueOf(itemTypeName));
-    return ct;
+  public ThingType setChannelTypes(ThingType tt, StringList channelTypeNames) {
+    missingChannelTypeListMap.put(tt, channelTypeNames);
+    return tt;
   }
 
-  public Item setCategory(Item item, String categoryName) {
-    missingItemCategoryMap.put(item, categoryName);
-    return item;
+  public ThingType setParameters(ThingType tt, StringList parameterNames) {
+    missingParameterListMap.put(tt, parameterNames);
+    return tt;
   }
 
-  public Item setTopic(Item item, String mqttTopicName) {
-    missingTopicMap.put(item, mqttTopicName);
-    return item;
+  public Thing setChannels(Thing t, StringList channelNames) {
+    missingChannelListMap.put(t, channelNames);
+    return t;
+  }
+
+  public Thing setID(Thing thing, String id) {
+    thing.setID(id);
+    return thing;
+  }
+
+  public ThingType setID(ThingType thingType, String id) {
+    thingType.setID(id);
+    thingTypeMap.put(id, thingType);
+    return thingType;
+  }
+
+  //--- Channel and ChannelType ---
+
+  public ChannelType setItemType(ChannelType ct, String itemTypeName) {
+    ct.setItemType(ItemType.valueOf(itemTypeName));
+    return ct;
   }
 
   public ChannelType setChannelCategory(ChannelType ct, String name) {
@@ -229,6 +250,95 @@ public class EraserParserHelper {
     return ct;
   }
 
+  public Channel setChannelType(Channel c, String channelTypeName) {
+    missingChannelTypeMap.put(c, channelTypeName);
+    return c;
+  }
+
+  public Channel setLinks(Channel c, StringList linkNames) {
+    missingItemLinkListMap.put(c, linkNames);
+    return c;
+  }
+
+  public ChannelType setID(ChannelType channelType, String id) {
+    channelType.setID(id);
+    channelTypeMap.put(id, channelType);
+    return channelType;
+  }
+
+  public Channel setID(Channel channel, String id) {
+    channel.setID(id);
+    channelMap.put(id, channel);
+    return channel;
+  }
+
+  //--- Item ---
+
+  public Item createItem() {
+    ItemPrototype result = new ItemPrototype();
+    result.disableSendState();
+    return result;
+  }
+
+  public Item setCategory(Item item, String categoryName) {
+    missingItemCategoryMap.put(item, categoryName);
+    return item;
+  }
+
+  public Item setMetaData(Item item, StringKeyMap metaData) {
+    for (AbstractMap.SimpleEntry<String, String> entry : metaData) {
+      item.addMetaData(new ItemMetaData(entry.getKey(), entry.getValue()));
+    }
+    return item;
+  }
+
+  public Item setControlling(Item item, StringList controlling) {
+    missingControllingListMap.put(item, controlling);
+    return item;
+  }
+
+  public Item retype(Item itemWithCorrectType, Item prototype) {
+    itemWithCorrectType.setID(prototype.getID());
+    itemWithCorrectType.setLabel(prototype.getLabel());
+    itemWithCorrectType.setMetaDataList(prototype.getMetaDataList());
+    if (!(itemWithCorrectType instanceof ActivityItem)) {
+      String state = prototype.getStateAsString();
+      itemWithCorrectType.disableSendState();
+      if (state.isEmpty()) {
+        itemWithCorrectType.setStateToDefault();
+      } else {
+        itemWithCorrectType.setStateFromString(state);
+      }
+      itemWithCorrectType.enableSendState();
+    }
+
+    moveMissingForRetype(itemWithCorrectType, prototype, missingTopicMap);
+    moveMissingForRetype(itemWithCorrectType, prototype, missingControllingListMap);
+    moveMissingForRetype(itemWithCorrectType, prototype, missingItemCategoryMap);
+
+    itemMap.put(prototype.getID(), itemWithCorrectType);
+
+    itemOrder.add(itemWithCorrectType);
+
+    return itemWithCorrectType;
+  }
+
+  private <T> void moveMissingForRetype(Item itemWithCorrectType, Item prototype, Map<Item, T> missingXMap) {
+    T value = missingXMap.get(prototype);
+    if (value != null) {
+      missingXMap.put(itemWithCorrectType, value);
+    }
+    missingXMap.remove(prototype);
+  }
+
+  public Item setID(Item item, String id) {
+    item.setID(id);
+    itemMap.put(id, item);
+    return item;
+  }
+
+  //--- Group ---
+
   public Group setSubGroups(Group g, StringList subGroupNames) {
     missingSubGroupListMap.put(g, subGroupNames);
     return g;
@@ -252,10 +362,8 @@ public class EraserParserHelper {
     params.iterator().forEachRemaining(paramList::add);
     String param1, param2;
     if (paramList.size() == 2) {
-      // ATTENTION: Here the order is reversed, as the parser reads from back to front
-      // So assign first parameter to second read parameter, and vice versa
-      param1 = paramList.get(1);
-      param2 = paramList.get(0);
+      param1 = paramList.get(0);
+      param2 = paramList.get(1);
     } else {
       logger.error("Got {} instead of 2 parameters in group function {}!", paramList.size(), aggFunctionName);
       param1 = "?";
@@ -265,77 +373,77 @@ public class EraserParserHelper {
     return g;
   }
 
-  public ThingType setChannelTypes(ThingType tt, StringList channelTypeNames) {
-    missingChannelTypeListMap.put(tt, channelTypeNames);
-    return tt;
+  public Group setID(Group group, String id) {
+    group.setID(id);
+    groupMap.put(id, group);
+    return group;
   }
 
+  //--- Parameter ---
+
   public Parameter setParameterValueType(Parameter p, String pvt) {
-    p.setType(ParameterValueType.valueOf(toTitleCase(pvt)));
+    p.setType(ParameterValueType.valueOf(JavaUtils.toTitleCase(pvt)));
     return p;
   }
 
-  private String toTitleCase(String s) {
-    if (s == null || s.isEmpty()) {
-      return s;
-    }
-    return s.substring(0, 1).toUpperCase() + s.substring(1);
-  }
-
   public Parameter setDefault(Parameter p, String defaultValue) {
     p.setDefaultValue(new ParameterDefaultValue(defaultValue));
     return p;
   }
 
-  public ThingType setParameters(ThingType tt, StringList parameterNames) {
-    missingParameterListMap.put(tt, parameterNames);
-    return tt;
+  public Parameter setID(Parameter parameter, String id) {
+    parameter.setID(id);
+    parameterMap.put(id, parameter);
+    return parameter;
   }
 
-  public Thing setChannels(Thing t, StringList channelNames) {
-    missingChannelListMap.put(t, channelNames);
-    return t;
-  }
+  //--- MQTT ---
 
-  public Channel setChannelType(Channel c, String channelTypeName) {
-    missingChannelTypeMap.put(c, channelTypeName);
-    return c;
+  public Item setTopic(Item item, String mqttTopicName) {
+    missingTopicMap.put(item, mqttTopicName);
+    return item;
   }
 
-  public Channel setLinks(Channel c, StringList linkNames) {
-    missingItemLinkListMap.put(c, linkNames);
-    return c;
+  //--- Activity ---
+
+  public MachineLearningRoot setActivities(MachineLearningRoot mlr, IntegerKeyMap map) {
+    for (AbstractMap.SimpleEntry<Integer, String> entry : map) {
+      Activity activity = new Activity();
+      activity.setIdentifier(entry.getKey());
+      activity.setLabel(entry.getValue());
+      mlr.addActivity(activity);
+    }
+    return mlr;
   }
 
+  //--- Root ---
+
   public Root createRoot() {
-    this.root = new Root();
-    this.root.setMqttRoot(new MqttRoot());
-    this.root.setInfluxRoot(InfluxRoot.createDefault());
-    this.root.setMachineLearningRoot(new MachineLearningRoot());
+    this.root = Root.createEmptyRoot();
     return this.root;
   }
 
   public Root createRoot(Thing t) {
     Root result = createRoot();
-    result.addThing(t);
+    result.getOpenHAB2Model().addThing(t);
     return result;
   }
 
   public Root createRoot(Group g) {
     Root result = createRoot();
-    result.addGroup(g);
+    result.getOpenHAB2Model().addGroup(g);
     return result;
   }
 
   public Root createRoot(ThingType tt) {
     Root result = createRoot();
-    result.addThingType(tt);
+    result.getOpenHAB2Model().addThingType(tt);
     return result;
   }
 
   public Root createRoot(ChannelType ct) {
     Root result = createRoot();
-    result.addChannelType(ct);
+    result.getOpenHAB2Model().addChannelType(ct);
     return result;
   }
 
@@ -351,64 +459,30 @@ public class EraserParserHelper {
     return result;
   }
 
-  public Item createItem() {
-    return new ItemPrototype();
-  }
-
-  public Item retype(Item itemWithCorrectType, Item prototype) {
-    itemWithCorrectType.setID(prototype.getID());
-    itemWithCorrectType.setLabel(prototype.getLabel());
-    itemWithCorrectType.setStateFromString(prototype.getStateAsString());
-
-    missingTopicMap.put(itemWithCorrectType, missingTopicMap.get(prototype));
-    missingTopicMap.remove(prototype);
-
-    itemMap.put(prototype.getID(), itemWithCorrectType);
-
-    itemOrder.add(itemWithCorrectType);
-
-    return itemWithCorrectType;
-  }
-
-  public Thing setID(Thing thing, String id) {
-    thing.setID(id);
-    return thing;
-  }
-
-  public Item setID(Item item, String id) {
-    item.setID(id);
-    itemMap.put(id, item);
-    return item;
-  }
-
-  public Group setID(Group group, String id) {
-    group.setID(id);
-    groupMap.put(id, group);
-    return group;
-  }
-
-  public ThingType setID(ThingType thingType, String id) {
-    thingType.setID(id);
-    thingTypeMap.put(id, thingType);
-    return thingType;
+  public Root createRoot(MachineLearningRoot ml) {
+    Root result = createRoot();
+    result.setMachineLearningRoot(ml);
+    return result;
   }
 
-  public Parameter setID(Parameter parameter, String id) {
-    parameter.setID(id);
-    parameterMap.put(id, parameter);
-    return parameter;
+  public Root createRoot(Rule rule) {
+    Root result = createRoot();
+    result.addRule(rule);
+    return result;
   }
 
-  public ChannelType setID(ChannelType channelType, String id) {
-    channelType.setID(id);
-    channelTypeMap.put(id, channelType);
-    return channelType;
+  //+++ newStuff (to be categorized) +++
+  public Designator createDesignator(String itemName) {
+    Designator result = new Designator();
+    missingItemForDesignator.put(result, itemName);
+    return result;
   }
 
-  public Channel setID(Channel channel, String id) {
-    channel.setID(id);
-    channelMap.put(id, channel);
-    return channel;
+  public Rule createRule(Condition c, Action a) {
+    Rule result = new Rule();
+    result.addCondition(c);
+    result.addAction(a);
+    return result;
   }
 
 }
diff --git a/eraser-base/src/main/java/de/tudresden/inf/st/eraser/util/JavaUtils.java b/eraser-base/src/main/java/de/tudresden/inf/st/eraser/util/JavaUtils.java
index 5a66f6a1d321480e3356c37c72f90998ad888d77..79e02fb74f53357b69b464e472801afbf311c3fd 100644
--- a/eraser-base/src/main/java/de/tudresden/inf/st/eraser/util/JavaUtils.java
+++ b/eraser-base/src/main/java/de/tudresden/inf/st/eraser/util/JavaUtils.java
@@ -45,4 +45,11 @@ public class JavaUtils {
     return StreamSupport.stream(jastAddList.spliterator(), false);
   }
 
+  public static String toTitleCase(String s) {
+    if (s == null || s.isEmpty()) {
+      return s;
+    }
+    return s.substring(0, 1).toUpperCase() + s.substring(1);
+  }
+
 }
diff --git a/eraser-base/src/main/java/de/tudresden/inf/st/eraser/util/MemberPrinter.java b/eraser-base/src/main/java/de/tudresden/inf/st/eraser/util/MemberPrinter.java
new file mode 100644
index 0000000000000000000000000000000000000000..a6c89a42bca8e978f837ad805ec4278634f3afa4
--- /dev/null
+++ b/eraser-base/src/main/java/de/tudresden/inf/st/eraser/util/MemberPrinter.java
@@ -0,0 +1,305 @@
+package de.tudresden.inf.st.eraser.util;
+
+import de.tudresden.inf.st.eraser.jastadd.model.ASTNode;
+import de.tudresden.inf.st.eraser.jastadd.model.ModelElement;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+import java.util.List;
+import java.util.function.Function;
+import java.util.function.Supplier;
+
+/**
+ * Fluent API to pretty-print ASTNodes in a line-based fashion.
+ *
+ * @author rschoene - Initial contribution
+ */
+public class MemberPrinter {
+
+  private static final Logger logger = LogManager.getLogger(MemberPrinter.class);
+  private static boolean addSemicolonNewlineWhenBuild = true;
+  private static boolean emitNothingOnEmptyBody = true;
+  private boolean empty = true;
+  private final StringBuilder sb;
+
+  public enum ListBracketType {
+    SQUARE("[", "]"),
+    ROUND("(", ")"),
+    CURLY("{", "}");
+
+    private final String begin;
+    private final String end;
+
+    ListBracketType(String begin, String end) {
+      this.begin = begin;
+      this.end = end;
+    }
+  }
+
+  public MemberPrinter(String elementName) {
+    this.sb = new StringBuilder();
+    if (elementName != null && !elementName.isEmpty()) {
+      sb.append(elementName).append(":");
+    }
+  }
+
+  public static void setAddSemicolonNewlineWhenBuild(boolean addSemicolonNewlineWhenBuild) {
+    MemberPrinter.addSemicolonNewlineWhenBuild = addSemicolonNewlineWhenBuild;
+  }
+
+  public static void setEmitNothingOnEmptyBody(boolean emitNothingOnEmptyBody) {
+    MemberPrinter.emitNothingOnEmptyBody = emitNothingOnEmptyBody;
+  }
+
+  public String build() {
+    if (empty) {
+      if (emitNothingOnEmptyBody) {
+        return "";
+      } else {
+        logger.debug("Emitting empty body {}", sb);
+      }
+    }
+    if (addSemicolonNewlineWhenBuild) {
+      sb.append(" ;\n");
+    }
+    return sb.toString();
+  }
+
+  /**
+   * Given a list of elements, concat the quoted id's of those elements, separated by commas,
+   * and appends it to the given StringBuilder.
+   * @param listOfElements the list of elements to concat
+   */
+  private void concatIds(Iterable<? extends ModelElement> listOfElements) {
+    concatNodes(listOfElements, ModelElement::getID, true);
+  }
+
+  /**
+   * Given a list of elements, concat the quoted id's of those elements, separated by commas,
+   * and appends it to the given StringBuilder.
+   * @param listOfNodes the list of nodes
+   * @param mapping a function to map a node to a ModelElement
+   */
+  private <T extends ASTNode> void concatIds(Iterable<T> listOfNodes,
+                                             Function<T, ? extends ModelElement> mapping) {
+    concatNodes(listOfNodes, t -> mapping.apply(t).getID(), true);
+  }
+
+  /**
+   * Given a list of nodes, apply a function on each of those, concat the output (separated by commas),
+   * and appends it to the given StringBuilder.
+   * @param listOfNodes the list of nodes
+   * @param mapping a function to map a node to a String
+   */
+  private <T extends ASTNode> void concatNodes(Iterable<T> listOfNodes,
+                                               Function<T, String> mapping,
+                                               boolean quote) {
+    boolean first = true;
+    for (T t : listOfNodes) {
+      if (first) {
+        first = false;
+      } else {
+        sb.append(", ");
+      }
+      if (quote) {
+        sb.append("\"");
+      }
+      sb.append(mapping.apply(t));
+      if (quote) {
+        sb.append("\"");
+      }
+    }
+  }
+
+  /**
+   * Appends model elements to the builder, if their number is positive.
+   * @param name           The name of the member
+   * @param listOfElements The list of model elements
+   * @return this
+   */
+  public MemberPrinter addIds(String name, List<? extends ModelElement> listOfElements) {
+    return addIds(name, listOfElements.size(), listOfElements);
+  }
+
+  /**
+   * Appends model elements to the builder, if count is positive.
+   * @param name           The name of the member
+   * @param count          The number of items in the listOfElements
+   * @param listOfElements The list of model elements
+   * @return this
+   */
+  public MemberPrinter addIds(String name, int count, Iterable<? extends ModelElement> listOfElements) {
+    if (count > 0) {
+      sb.append(' ').append(name).append("=[");
+      concatIds(listOfElements);
+      sb.append("]");
+      this.empty = false;
+    }
+    return this;
+  }
+
+  /**
+   * Appends nodes to the builder, if count is positive.
+   * @param name        The name of the member
+   * @param count       The number of items in the listOfNodes
+   * @param listOfNodes The list of nodes
+   * @param mapping     A function to map a node to a ModelElement
+   * @return this
+   */
+  public <T extends ASTNode> MemberPrinter addIds(
+      String name, int count, Iterable<T> listOfNodes,
+      Function<T, ? extends ModelElement> mapping) {
+    if (count > 0) {
+      sb.append(' ').append(name).append("=[");
+      concatIds(listOfNodes, mapping);
+      sb.append("]");
+      this.empty = false;
+    }
+    return this;
+  }
+
+  /**
+   * Applies the mapping to each node and appends the results to the builder, if count is positive.
+   * The list is enclosed with square brackets.
+   * @param name        The name of the member
+   * @param count       The number of items in the listOfNodes
+   * @param listOfNodes The list of nodes
+   * @param mapping     A function to map a node to a String
+   * @param <T>         The type of all nodes
+   * @return this
+   */
+  public <T extends ASTNode> MemberPrinter addNodes(String name, int count, Iterable<T> listOfNodes,
+                                Function<T, String> mapping) {
+    return addNodes(name, count, listOfNodes, mapping, ListBracketType.SQUARE);
+  }
+
+  /**
+   * Applies the mapping to each node and appends the results to the builder, if count is positive
+   * @param name        The name of the member
+   * @param count       The number of items in the listOfNodes
+   * @param listOfNodes The list of nodes
+   * @param mapping     A function to map a node to a String
+   * @param <T>         The type of all nodes
+   * @param bracketType The type of brackets to enclose the list with
+   * @return this
+   */
+  public <T extends ASTNode> MemberPrinter addNodes(String name, int count, Iterable<T> listOfNodes,
+                                                    Function<T, String> mapping, ListBracketType bracketType) {
+    if (count > 0) {
+      sb.append(' ').append(name).append("=").append(bracketType.begin);
+      concatNodes(listOfNodes, mapping, false);
+      sb.append(bracketType.end);
+      this.empty = false;
+    }
+    return this;
+  }
+
+  /**
+   * Appends a value to the builder, if the value is present.
+   * A function is used to get the value, such that it is only evaluated if the value is present
+   * @param name        The name of the member
+   * @param isPresent   Whether the optional value is present
+   * @param valueGetter A function to get the value
+   * @return this
+   */
+  public MemberPrinter addOptional(String name, boolean isPresent, Supplier<String> valueGetter) {
+    if (isPresent) {
+      return add(name, valueGetter.get());
+    }
+    return this;
+  }
+
+  /**
+   * Appends the result of calling {@link ASTNode#prettyPrint()} for the child, if non-<code>null</code>.
+   * @param child       The child to append
+   * @return this
+   */
+  public MemberPrinter addOptionalPrettyPrint(ASTNode child) {
+    if (child != null) {
+      this.empty = false;
+      sb.append(child.prettyPrint());
+    }
+    return this;
+  }
+
+  /**
+   * Appends a the name of the flag, if it is set
+   * @param name        The name of the member
+   * @param isSet       Whether the flag is set
+   * @return this
+   */
+  public MemberPrinter addFlag(String name, boolean isSet) {
+    if (isSet) {
+      this.empty = false;
+      sb.append(' ').append(name);
+    }
+    return this;
+  }
+
+  /**
+   * Appends a value to the builder, if the value is not <code>null</code> and not empty
+   * @param name        The name of the member
+   * @param actualValue The value to append
+   * @return this
+   */
+  public MemberPrinter addNonDefault(String name, String actualValue) {
+    return addNonDefault(name, actualValue, "");
+  }
+
+  /**
+   * Appends a value to the builder, if the value is not the given default
+   * @param name         The name of the member
+   * @param actualValue  The value to append
+   * @param defaultValue The default value
+   * @return this
+   */
+  public MemberPrinter addNonDefault(String name, Object actualValue, Object defaultValue) {
+    if (!actualValue.equals(defaultValue)) {
+      this.empty = false;
+      return add(name, actualValue);
+    }
+    return this;
+  }
+
+  /**
+   * Appends a value to the builder, and issue a warning if it is either <code>null</code> or empty.
+   * @param name         The name of the member
+   * @param actualValue  The value to append
+   * @return this
+   */
+  public MemberPrinter addRequired(String name, Object actualValue) {
+    if (actualValue == null || actualValue.toString().isEmpty()) {
+      logger.warn("Member \"{}\" not defined while printing", name);
+      actualValue = "";
+    }
+    return add(name, actualValue);
+  }
+
+  /**
+   * Appends a value to the builder, and issue a warning if it is either <code>null</code> or empty.
+   * @param name         The name of the member
+   * @param objectValue  An object to append
+   * @param mapping      A function to get the real value from the object
+   * @return this
+   */
+  public <T> MemberPrinter addRequired(String name, T objectValue, Function<T, String> mapping) {
+    String actualValue;
+    if (objectValue == null) {
+      logger.warn("Member \"{}\" not defined while printing", name);
+      actualValue = "";
+    } else {
+      actualValue = mapping.apply(objectValue);
+      if (actualValue.isEmpty()) {
+        logger.warn("Member \"{}\" empty while printing", name);
+      }
+    }
+    return add(name, actualValue);
+  }
+
+  private MemberPrinter add(String name, Object actualValue) {
+    this.empty = false;
+    sb.append(' ').append(name).append("=\"").append(actualValue).append("\"");
+    return this;
+  }
+
+}
diff --git a/eraser-base/src/main/java/de/tudresden/inf/st/eraser/util/MqttReceiver.java b/eraser-base/src/main/java/de/tudresden/inf/st/eraser/util/MqttReceiver.java
index e433bf34d3b2e4848291a766ca3b5e1c55a80dc3..5fa9ad6800b3964369d0a1f140db6a4c3e0d164f 100644
--- a/eraser-base/src/main/java/de/tudresden/inf/st/eraser/util/MqttReceiver.java
+++ b/eraser-base/src/main/java/de/tudresden/inf/st/eraser/util/MqttReceiver.java
@@ -1,6 +1,5 @@
 package de.tudresden.inf.st.eraser.util;
 
-import de.tudresden.inf.st.eraser.openhab2.mqtt.MQTTUpdater;
 import org.apache.logging.log4j.LogManager;
 import org.apache.logging.log4j.Logger;
 import org.fusesource.hawtbuf.Buffer;
@@ -40,7 +39,7 @@ public class MqttReceiver implements AutoCloseable {
   private QoS qos;
 
   public MqttReceiver() {
-    this.logger = LogManager.getLogger(MQTTUpdater.class);
+    this.logger = LogManager.getLogger(MqttReceiver.class);
     this.readyLock = new ReentrantLock();
     this.readyCondition = readyLock.newCondition();
     this.ready = false;
@@ -50,8 +49,8 @@ public class MqttReceiver implements AutoCloseable {
   /**
    * Sets the host to receive messages from
    */
-  public void setHost(String host) {
-    this.host = URI.create("tcp://" + host + ":1883");
+  public void setHost(String host, int port) {
+    this.host = URI.create("tcp://" + host + ":" + port);
     logger.debug("Host is {}", this.host);
   }
 
@@ -134,6 +133,17 @@ public class MqttReceiver implements AutoCloseable {
     connection.connect(new Callback<Void>() {
       @Override
       public void onSuccess(Void value) {
+        connection.publish("components", "Eraser is listening".getBytes(), QoS.AT_LEAST_ONCE, false, new Callback<Void>() {
+          @Override
+          public void onSuccess(Void value) {
+            logger.debug("success sending welcome message");
+          }
+
+          @Override
+          public void onFailure(Throwable value) {
+            logger.debug("failure sending welcome message", value);
+          }
+        });
         Topic[] topicArray = Arrays.stream(topics).map(topicName -> new Topic(topicName, qos)).toArray(Topic[]::new);
         logger.info("Connected, subscribing to {} topic(s) now.", topicArray.length);
         connection.subscribe(topicArray, new Callback<byte[]>() {
@@ -151,7 +161,7 @@ public class MqttReceiver implements AutoCloseable {
 
           @Override
           public void onFailure(Throwable cause) {
-            logger.error("Could not subscribe, because {}", cause);
+            logger.error("Could not subscribe", cause);
           }
         });
       }
diff --git a/eraser-base/src/main/java/de/tudresden/inf/st/eraser/util/ParserUtils.java b/eraser-base/src/main/java/de/tudresden/inf/st/eraser/util/ParserUtils.java
index e329f7928d587236342a04dfdd540fb1dcc2e4ea..716f63f9caa9462665b8819c9347aef466637a76 100644
--- a/eraser-base/src/main/java/de/tudresden/inf/st/eraser/util/ParserUtils.java
+++ b/eraser-base/src/main/java/de/tudresden/inf/st/eraser/util/ParserUtils.java
@@ -3,12 +3,10 @@ package de.tudresden.inf.st.eraser.util;
 import beaver.Parser;
 import beaver.Scanner;
 import beaver.Symbol;
-import de.tudresden.inf.st.eraser.jastadd.model.Group;
-import de.tudresden.inf.st.eraser.jastadd.model.Item;
-import de.tudresden.inf.st.eraser.jastadd.model.MqttTopic;
-import de.tudresden.inf.st.eraser.jastadd.model.Root;
+import de.tudresden.inf.st.eraser.jastadd.model.*;
 import de.tudresden.inf.st.eraser.jastadd.parser.EraserParser;
 import de.tudresden.inf.st.eraser.jastadd.scanner.EraserScanner;
+import de.tudresden.inf.st.eraser.parser.EraserParserHelper;
 import org.apache.logging.log4j.LogManager;
 import org.apache.logging.log4j.Logger;
 
@@ -16,8 +14,8 @@ import java.io.*;
 import java.net.URL;
 import java.nio.file.Files;
 import java.nio.file.Paths;
-import java.util.*;
-import java.util.stream.Collectors;
+import java.util.Collection;
+import java.util.Objects;
 
 /**
  * Utility methods involving scanner and parser of the models.
@@ -26,9 +24,42 @@ import java.util.stream.Collectors;
  */
 public class ParserUtils {
 
+  public static final String UNKNOWN_GROUP_NAME = "Unknown";
   private static boolean verboseLoading = false;
   private static final Logger logger = LogManager.getLogger(ParserUtils.class);
 
+  private interface ReaderProvider {
+    Reader provide() throws IOException;
+  }
+
+  private static class ReaderProviderByName implements ReaderProvider {
+    private final String filename;
+    private final Class<?> clazz;
+
+    ReaderProviderByName(String filename, Class<?> clazz) {
+      this.filename = filename;
+      this.clazz = clazz;
+    }
+
+    @Override
+    public Reader provide() throws IOException {
+      return getReader(filename, clazz);
+    }
+  }
+
+  private static class ReaderProviderByURL implements ReaderProvider {
+    private final URL url;
+
+    ReaderProviderByURL(URL url) {
+      this.url = url;
+    }
+
+    @Override
+    public Reader provide() throws IOException {
+      return new InputStreamReader(url.openStream());
+    }
+  }
+
   /**
    * Print read tokens before loading the model.
    * This will effectively parse the input two times, thus slowing the operation.
@@ -71,8 +102,23 @@ public class ParserUtils {
    */
   public static Root load(String fileName, Class<?> clazz) throws IOException, Parser.Exception {
     logger.info("Loading model DSL file '{}'", fileName);
+    return load(new ReaderProviderByName(fileName, clazz));
+  }
 
-    Reader reader = getReader(fileName, clazz);
+  /**
+   * Loads a model in a file from the given URL.
+   * @param url an URL pointing to a file
+   * @return the parsed model
+   * @throws IOException if the file could not be found, or opened
+   * @throws Parser.Exception if the file contains a malformed model
+   */
+  public static Root load(URL url) throws IOException, Parser.Exception {
+    logger.info("Loading model DSL from '{}'", url);
+    return load(new ReaderProviderByURL(url));
+  }
+
+  private static Root load(ReaderProvider readerProvider) throws IOException, Parser.Exception {
+    Reader reader = readerProvider.provide();
     if (verboseLoading) {
       EraserScanner scanner = new EraserScanner(reader);
       try {
@@ -87,7 +133,7 @@ public class ParserUtils {
         try {
           reader.reset();
         } catch (IOException resetEx) {
-          reader = getReader(fileName, clazz);
+          reader = readerProvider.provide();
         }
       }
     }
@@ -120,63 +166,91 @@ public class ParserUtils {
     }
   }
 
-  public static void createUnknownGroup(Root model, Collection<Item> danglingItems) {
+  /**
+   * Create well-known group call "Unknown" and add all dangling items to it.
+   * @param model         The model to operate on
+   * @param danglingItems A list of items to add to the new group
+   */
+  public static void createUnknownGroup(OpenHAB2Model model, Collection<Item> danglingItems) {
     Group unknownGroup = new Group();
-    unknownGroup.setID("Unknown");
+    unknownGroup.setID(UNKNOWN_GROUP_NAME);
     model.addGroup(unknownGroup);
     danglingItems.forEach(unknownGroup::addItem);
-    logger.warn("Creating group 'Unknown' for items: {}", PrinterUtils.concatIds(danglingItems));
+    logger.info("Created new {}", unknownGroup.prettyPrint().trim());
   }
 
-  public static void createMqttTopic(Item item, String topicName, Root root) {
-    String[] parts = topicName.split("/");
-    String firstPart = parts[0];
-    MqttTopic firstTopic = null;
-    for (MqttTopic topic : root.getMqttRoot().getTopicList()) {
-      if (topic.getPart().equals(firstPart)) {
-        firstTopic = topic;
-        break;
-      }
-    }
-    if (firstTopic == null) {
-      // no matching topic found for first part. create one.
-      firstTopic = createTopic(firstPart, root);
-    }
-//    MqttTopic firstTopic = firstPartTopicMap.computeIfAbsent(firstPart, part -> createTopic(part, root));
-    MqttTopic lastTopic = processRemainingTopicParts(firstTopic, parts, 1);
-    item.setTopic(lastTopic);
+  /**
+   * Create a topic for the given topic name and assign it to the given item.
+   * @param item        The item to which the topic will be assigned to
+   * @param topicSuffix The full topic name
+   * @param root        The model to operate on
+   */
+  public static void createMqttTopic(Item item, String topicSuffix, Root root) {
+    item.setTopic(root.getMqttRoot().resolveTopicSuffix(topicSuffix).orElseGet(() -> {
+      MqttTopic result = new MqttTopic();
+      result.setTopicString(topicSuffix);
+      root.getMqttRoot().addTopic(result);
+      return result;
+    }));
   }
 
-  private static MqttTopic processRemainingTopicParts(MqttTopic topic, String[] parts, int index) {
-    if (index >= parts.length) {
-      return topic;
-    }
-    for (MqttTopic subTopic : topic.getSubTopicList()) {
-      if (subTopic.getPart().equals(parts[index])) {
-        // matching part found
-        return processRemainingTopicParts(subTopic, parts, index + 1);
-      }
-    }
-    // no matching part was found. create remaining topics.
-    for (int currentIndex = index; currentIndex < parts.length; currentIndex++) {
-      MqttTopic newTopic = createSubTopic(parts[currentIndex]);
-      topic.addSubTopic(newTopic);
-      topic = newTopic;
-    }
-    return topic;
+  public static NumberExpression parseNumberExpression(String expression_string) throws IOException, Parser.Exception {
+    return parseNumberExpression(expression_string, null);
   }
 
-  private static MqttTopic createSubTopic(String part) {
-    return createTopic(part, null);
+  public static LogicalExpression parseLogicalExpression(String expression_string) throws IOException, Parser.Exception {
+    return parseLogicalExpression(expression_string, null);
   }
 
-  private static MqttTopic createTopic(String part, Root root) {
-    MqttTopic result = new MqttTopic();
-    result.setPart(part);
-    if (root != null) {
-      root.getMqttRoot().addTopic(result);
+  public static NumberExpression parseNumberExpression(String expression_string, Root root) throws IOException, Parser.Exception {
+    return (NumberExpression) parseExpression(expression_string, EraserParser.AltGoals.number_expression, root);
+  }
+
+  public static LogicalExpression parseLogicalExpression(String expression_string, Root root) throws IOException, Parser.Exception {
+    return (LogicalExpression) parseExpression(expression_string, EraserParser.AltGoals.logical_expression, root);
+  }
+
+  private static Expression parseExpression(String expression_string, short alt_goal, Root root) throws IOException, Parser.Exception {
+    EraserParserHelper.setInitialRoot(root);
+    StringReader reader = new StringReader(expression_string);
+    if (verboseLoading) {
+      EraserScanner scanner = new EraserScanner(reader);
+      try {
+        Symbol token;
+        while ((token = scanner.nextToken()).getId() != EraserParser.Terminals.EOF) {
+          logger.debug("start: {}, end: {}, id: {}, value: {}",
+              token.getStart(), token.getEnd(), EraserParser.Terminals.NAMES[token.getId()], token.value);
+        }
+      } catch (Scanner.Exception e) {
+        e.printStackTrace();
+      }
     }
+    reader = new StringReader(expression_string);
+    EraserScanner scanner = new EraserScanner(reader);
+    EraserParser parser = new EraserParser();
+    Expression result = (Expression) parser.parse(scanner, alt_goal);
+    parser.resolveReferences();
+    reader.close();
+    EraserParserHelper.setInitialRoot(null);
     return result;
   }
 
+  public static Item parseItem(String definition)
+      throws IllegalArgumentException, IOException, Parser.Exception {
+    StringReader reader = new StringReader(definition);
+    EraserScanner scanner = new EraserScanner(reader);
+    EraserParser parser = new EraserParser();
+    Root root = (Root) parser.parse(scanner);
+    parser.resolveReferences();
+    reader.close();
+    int size = root.getOpenHAB2Model().items().size();
+    if (size == 0) {
+      throw new IllegalArgumentException("Model does not contain any items!");
+    }
+    if (size > 1) {
+      logger.warn("Model does contain {} items, ignoring all but the first.", size);
+    }
+    return root.getOpenHAB2Model().items().get(0);
+  }
+
 }
diff --git a/eraser-base/src/main/java/de/tudresden/inf/st/eraser/util/PrinterUtils.java b/eraser-base/src/main/java/de/tudresden/inf/st/eraser/util/PrinterUtils.java
deleted file mode 100644
index 3f2b86613f5e627759a4c74cfa82dee58544b151..0000000000000000000000000000000000000000
--- a/eraser-base/src/main/java/de/tudresden/inf/st/eraser/util/PrinterUtils.java
+++ /dev/null
@@ -1,67 +0,0 @@
-package de.tudresden.inf.st.eraser.util;
-
-import de.tudresden.inf.st.eraser.jastadd.model.ASTNode;
-import de.tudresden.inf.st.eraser.jastadd.model.JastAddList;
-import de.tudresden.inf.st.eraser.jastadd.model.ModelElement;
-
-import java.util.function.Function;
-
-/**
- * Helper methods shared by multiple non-terminals.
- *
- * @author rschoene - Initial contribution
- */
-public class PrinterUtils {
-
-  /**
-   * Given a list of elements, concat the quoted id's of those elements, separated by commas.
-   * @param listOfElements the list of elements to concat
-   * @return a comma-separated list of the ids of the given elements
-   */
-  public static String concatIds(Iterable<? extends ModelElement> listOfElements) {
-    return concatIds(new StringBuilder(), listOfElements).toString();
-  }
-
-  /**
-   * Given a list of elements, concat the quoted id's of those elements, separated by commas,
-   * and appends it to the given StringBuilder.
-   * @param sb the StringBuilder to append to
-   * @param listOfElements the list of elements to concat
-   * @return the given StringBuilder
-   */
-  public static StringBuilder concatIds(StringBuilder sb, Iterable<? extends ModelElement> listOfElements) {
-    boolean first = true;
-    for (ModelElement c : listOfElements) {
-      if (first) {
-        first = false;
-      } else {
-        sb.append(", ");
-      }
-      sb.append("\"").append(c.getID()).append("\"");
-    }
-    return sb;
-  }
-
-  /**
-   * Given a list of elements, concat the quoted id's of those elements, separated by commas,
-   * and appends it to the given StringBuilder.
-   * @param sb the StringBuilder to append to
-   * @param listOfNodes the list of nodes
-   * @param mapping a function to map a node to a ModelElement
-   * @return the given StringBuilder
-   */
-  public static <T extends ASTNode> StringBuilder concatIds(StringBuilder sb, Iterable<T> listOfNodes,
-                                                            Function<T, ? extends ModelElement> mapping) {
-    boolean first = true;
-    for (T t : listOfNodes) {
-      if (first) {
-        first = false;
-      } else {
-        sb.append(", ");
-      }
-      sb.append("\"").append(mapping.apply(t).getID()).append("\"");
-    }
-    return sb;
-  }
-
-}
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
new file mode 100644
index 0000000000000000000000000000000000000000..5f07ec7c576829975db82121e42e142497b49586
--- /dev/null
+++ b/eraser-base/src/main/java/de/tudresden/inf/st/eraser/util/TestUtils.java
@@ -0,0 +1,64 @@
+package de.tudresden.inf.st.eraser.util;
+
+import de.tudresden.inf.st.eraser.jastadd.model.*;
+
+/**
+ * Helper class to create models used in tests.
+ *
+ * @author rschoene - Initial contribution
+ */
+public class TestUtils {
+
+  public static class ModelAndItem {
+    public OpenHAB2Model model;
+    public NumberItem item;
+    static ModelAndItem of(OpenHAB2Model model, NumberItem item) {
+      ModelAndItem result = new ModelAndItem();
+      result.model = model;
+      result.item = item;
+      return result;
+    }
+  }
+
+  public static ModelAndItem createModelAndItem(double initialValue) {
+    return createModelAndItem(initialValue, false);
+  }
+
+  public static ModelAndItem createModelAndItem(double initialValue, boolean useUpdatingItem) {
+    Root root = Root.createEmptyRoot();
+    Group g = new Group();
+    root.getOpenHAB2Model().addGroup(g);
+    g.setID("group1");
+
+    NumberItem item = addItemTo(g, initialValue, useUpdatingItem);
+
+    return ModelAndItem.of(root.getOpenHAB2Model(), item);
+  }
+
+  public static NumberItem addItemTo(OpenHAB2Model model, double initialValue) {
+    return addItemTo(model, initialValue, false);
+  }
+
+  public static NumberItem addItemTo(OpenHAB2Model model, double initialValue, boolean useUpdatingItem) {
+    return addItemTo(getDefaultGroup(model), initialValue, useUpdatingItem);
+  }
+
+  private static NumberItem addItemTo(Group group, double initialValue, boolean useUpdatingItem) {
+    NumberItem item = new NumberItem();
+    if (useUpdatingItem) {
+      item.enableSendState();
+    } else {
+      item.disableSendState();
+    }
+    item.setID("item" + group.getNumItem());
+    item.setState(initialValue, false);
+    group.addItem(item);
+    return item;
+  }
+
+  public static Group getDefaultGroup(OpenHAB2Model model) {
+    // use first found group
+    return model.getGroup(0);
+  }
+
+}
diff --git a/eraser-base/src/main/resources/log4j2.xml b/eraser-base/src/main/resources/log4j2.xml
index 89799a2f09ba34d288e610d960b3ed6348213105..18175a02521156259c8789745fb849fa893302e9 100644
--- a/eraser-base/src/main/resources/log4j2.xml
+++ b/eraser-base/src/main/resources/log4j2.xml
@@ -4,8 +4,8 @@
         <Console name="Console">
             <PatternLayout pattern="%highlight{%d{HH:mm:ss.SSS} %-5level} %c{1.} - %msg%n"/>
         </Console>
-        <RollingFile name="RollingFile" fileName="logs/jastadd-mquat.log"
-                    filePattern="logs/jastadd-mquat-%i.log">
+        <RollingFile name="RollingFile" fileName="logs/eraser.log"
+                    filePattern="logs/eraser-%i.log">
             <PatternLayout pattern="%d{HH:mm:ss.SSS} %-5level %logger{36} - %msg%n"/>
             <Policies>
                 <OnStartupTriggeringPolicy/>
diff --git a/eraser-base/src/test/java/de/tudresden/inf/st/eraser/ControllingItemTest.java b/eraser-base/src/test/java/de/tudresden/inf/st/eraser/ControllingItemTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..edc98e72719ef63ffa63f0cb19aac9b7106ab879
--- /dev/null
+++ b/eraser-base/src/test/java/de/tudresden/inf/st/eraser/ControllingItemTest.java
@@ -0,0 +1,127 @@
+package de.tudresden.inf.st.eraser;
+
+import de.tudresden.inf.st.eraser.util.TestUtils;
+import de.tudresden.inf.st.eraser.util.TestUtils.ModelAndItem;
+import de.tudresden.inf.st.eraser.jastadd.model.*;
+import org.junit.Assert;
+import org.junit.Test;
+
+import java.util.function.Consumer;
+
+/**
+ * Testing the simple rule engine.
+ *
+ * @author rschoene - Initial contribution
+ */
+public class ControllingItemTest {
+
+  private static final double DELTA = 0.001;
+
+  @Test
+  public void testOneToOneNumber() {
+    ModelAndItem mai = TestUtils.createModelAndItem(0, true);
+    NumberItem item1 = mai.item;
+    NumberItem item2 = TestUtils.addItemTo(mai.model, 4, true);
+    item1.addControlling(item2);
+
+    Assert.assertEquals(0, item1.getState(), DELTA);
+    Assert.assertEquals(4, item2.getState(), DELTA);
+
+    item1.setState(5);
+    Assert.assertEquals(5, item1.getState(), DELTA);
+    Assert.assertEquals("Item was not controlled correctly", 5, item2.getState(), DELTA);
+  }
+
+  @Test
+  public void testOneToManyNumber() {
+    ModelAndItem mai = TestUtils.createModelAndItem(0, true);
+    NumberItem item1 = mai.item;
+    NumberItem item2 = TestUtils.addItemTo(mai.model, 3, true);
+    NumberItem item3 = TestUtils.addItemTo(mai.model, 4, true);
+    NumberItem item4 = TestUtils.addItemTo(mai.model, 5, true);
+    item1.addControlling(item2);
+    item1.addControlling(item3);
+    item1.addControlling(item4);
+
+    Assert.assertEquals(0, item1.getState(), DELTA);
+    Assert.assertEquals(3, item2.getState(), DELTA);
+    Assert.assertEquals(4, item3.getState(), DELTA);
+    Assert.assertEquals(5, item4.getState(), DELTA);
+
+    item1.setState(5);
+    Assert.assertEquals(5, item1.getState(), DELTA);
+    Assert.assertEquals("Item2 was not controlled correctly", 5, item2.getState(), DELTA);
+    Assert.assertEquals("Item3 was not controlled correctly", 5, item3.getState(), DELTA);
+    Assert.assertEquals("Item4 was not controlled correctly", 5, item4.getState(), DELTA);
+  }
+
+  @Test
+  public void testManyToOneColor() {
+    ModelAndItem mai = TestUtils.createModelAndItem(0);
+    OpenHAB2Model model = mai.model;
+    NumberItem numberItem = mai.item;
+
+    Group g = TestUtils.getDefaultGroup(model);
+
+    StringItem stringItem = initAndAddItem(g, new StringItem(),
+        item -> item.setState("0"));
+
+    SwitchItem booleanItem = initAndAddItem(g, new SwitchItem(),
+        item -> item.setState(false));
+
+    ColorItem colorItem = initAndAddItem(g, new ColorItem(),
+        item -> item.setState(TupleHSB.of(0, 0, 0)));
+
+    ColorItem target = initAndAddItem(g, new ColorItem(),
+        item -> item.setState(TupleHSB.of(0, 0, 0)));
+
+    target.addControlledBy(numberItem);
+    target.addControlledBy(stringItem);
+    target.addControlledBy(booleanItem);
+    target.addControlledBy(colorItem);
+
+    Assert.assertEquals(0, numberItem.getState(), DELTA);
+    Assert.assertEquals("0", stringItem.getState());
+    Assert.assertFalse(booleanItem.getState());
+    Assert.assertEquals(TupleHSB.of(0, 0, 0), colorItem.getState());
+    Assert.assertEquals(TupleHSB.of(0, 0, 0), target.getState());
+
+    // number 5 -> set brightness to 5
+    numberItem.setState(5);
+    Assert.assertEquals(5, numberItem.getState(), DELTA);
+    Assert.assertEquals("Item was not controlled correctly", TupleHSB.of(0, 0, 5), target.getState());
+
+    // string 30 -> set brightness to 30
+    stringItem.setState("30");
+    Assert.assertEquals("30", stringItem.getState());
+    Assert.assertEquals("Item was not controlled correctly", TupleHSB.of(0, 0, 30), target.getState());
+
+    // string 30,20,10 -> set HSB to (30,20,10)
+    stringItem.setState("30,20,10");
+    Assert.assertEquals("30,20,10", stringItem.getState());
+    Assert.assertEquals("Item was not controlled correctly", TupleHSB.of(30, 20, 10), target.getState());
+
+    // boolean true -> set brightness to full (100)
+    booleanItem.setState(true);
+    Assert.assertTrue(booleanItem.getState());
+    Assert.assertEquals("Item was not controlled correctly", TupleHSB.of(30, 20, 100), target.getState());
+
+    // color (33,33,33) -> set brightness to (33,33,33)
+    colorItem.setState(TupleHSB.of(33, 33, 33));
+    Assert.assertEquals(TupleHSB.of(33, 33, 33), colorItem.getState());
+    Assert.assertEquals("Item was not controlled correctly", TupleHSB.of(33, 33, 33), target.getState());
+
+    // number 44 -> set brightness to 44
+    numberItem.setState(44);
+    Assert.assertEquals(44, numberItem.getState(), DELTA);
+    Assert.assertEquals("Item was not controlled correctly", TupleHSB.of(33, 33, 44), target.getState());
+  }
+
+  private <T extends Item> T initAndAddItem(Group group, T item, Consumer<T> setState) {
+    item.setID("item" + group.getNumItem());
+    group.addItem(item);
+    setState.accept(item);
+    item.enableSendState();
+    return item;
+  }
+}
diff --git a/eraser-base/src/test/java/de/tudresden/inf/st/eraser/DecisionTreeTest.java b/eraser-base/src/test/java/de/tudresden/inf/st/eraser/DecisionTreeTest.java
index 1cd643fdee35edc82810407e03a23e90f3c226eb..fa93e0691d782030d2e45c385f9327a57c9e1ae4 100644
--- a/eraser-base/src/test/java/de/tudresden/inf/st/eraser/DecisionTreeTest.java
+++ b/eraser-base/src/test/java/de/tudresden/inf/st/eraser/DecisionTreeTest.java
@@ -1,6 +1,7 @@
 package de.tudresden.inf.st.eraser;
 
 import de.tudresden.inf.st.eraser.jastadd.model.*;
+import de.tudresden.inf.st.eraser.util.TestUtils;
 import org.junit.Assert;
 import org.junit.Test;
 
@@ -16,7 +17,7 @@ public class DecisionTreeTest {
     TestUtils.ModelAndItem mai = TestUtils.createModelAndItem(4);
 
     DecisionTreeRoot dtroot = new DecisionTreeRoot();
-    mai.model.getMachineLearningRoot().setActivityRecognition(dtroot);
+    mai.model.getRoot().getMachineLearningRoot().setActivityRecognition(dtroot);
     DecisionTreeLeaf isLessThanFour = newLeaf("less than four");
     DecisionTreeLeaf isFourOrGreater = newLeaf("four or greater");
     ItemStateNumberCheck check = new ItemStateNumberCheck(ComparatorType.LessThan, 4f);
@@ -43,7 +44,7 @@ public class DecisionTreeTest {
     TestUtils.ModelAndItem mai = TestUtils.createModelAndItem(20);
 
     DecisionTreeRoot dtroot = new DecisionTreeRoot();
-    mai.model.getMachineLearningRoot().setActivityRecognition(dtroot);
+    mai.model.getRoot().getMachineLearningRoot().setActivityRecognition(dtroot);
 
     DecisionTreeLeaf isLessThan25 = newLeaf("less than 25");
     DecisionTreeLeaf is25OrGreater = newLeaf("25 or greater");
@@ -129,7 +130,7 @@ public class DecisionTreeTest {
     TestUtils.ModelAndItem mai = TestUtils.createModelAndItem(0);
 
     DecisionTreeRoot dtroot = new DecisionTreeRoot();
-    mai.model.getMachineLearningRoot().setActivityRecognition(dtroot);
+    mai.model.getRoot().getMachineLearningRoot().setActivityRecognition(dtroot);
     DecisionTreeLeaf left = newLeaf("left");
     DecisionTreeLeaf right = newLeaf("right");
     ItemStateNumberCheck check = new ItemStateNumberCheck(comparatorType, expected);
diff --git a/eraser-base/src/test/java/de/tudresden/inf/st/eraser/ExpressionEvalTest.java b/eraser-base/src/test/java/de/tudresden/inf/st/eraser/ExpressionEvalTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..1758931346632a63c9f3297912b1fb7d1430da46
--- /dev/null
+++ b/eraser-base/src/test/java/de/tudresden/inf/st/eraser/ExpressionEvalTest.java
@@ -0,0 +1,165 @@
+package de.tudresden.inf.st.eraser;
+
+import beaver.Parser;
+import de.tudresden.inf.st.eraser.jastadd.model.*;
+import de.tudresden.inf.st.eraser.util.ParserUtils;
+import org.junit.Test;
+
+import java.io.IOException;
+
+import static org.hamcrest.core.IsEqual.equalTo;
+import static org.hamcrest.core.IsInstanceOf.instanceOf;
+import static org.junit.Assert.assertThat;
+
+/**
+ * Test correct evaluation of NumberExpression and LogicalExpression.
+ *
+ * @author rschoene - Initial contribution
+ */
+public class ExpressionEvalTest {
+
+  @Test
+  public void plusExpression() throws IOException, Parser.Exception {
+    NumberExpression sut = ParserUtils.parseNumberExpression("(3 + 4)");
+    assertThat(sut.eval(), equalTo(7.0));
+  }
+
+  @Test
+  public void minusExpression() throws IOException, Parser.Exception {
+    NumberExpression sut = ParserUtils.parseNumberExpression("(4.1 - 12.5)");
+    assertThat(sut.eval(), equalTo(-8.4));
+  }
+
+  @Test
+  public void multExpression() throws IOException, Parser.Exception {
+    NumberExpression sut = ParserUtils.parseNumberExpression("(3 * 4)");
+    assertThat(sut.eval(), equalTo(12.0));
+  }
+
+  @Test
+  public void divExpression() throws IOException, Parser.Exception {
+    NumberExpression sut = ParserUtils.parseNumberExpression("(1.1 / 4.0)");
+    assertThat(sut.eval(), equalTo(0.275));
+  }
+
+  @Test
+  public void powerExpression() throws IOException, Parser.Exception {
+    NumberExpression sut = ParserUtils.parseNumberExpression("(16 ^ 0.5)");
+    assertThat(sut.eval(), equalTo(4.0));
+  }
+
+  @Test
+  public void parenthesizedExpression() throws IOException, Parser.Exception {
+    NumberExpression sut = ParserUtils.parseNumberExpression("(3)");
+    assertThat(sut.eval(), equalTo(3.0));
+  }
+
+  @Test
+  public void complexExpression() throws IOException, Parser.Exception {
+    NumberExpression sut = ParserUtils.parseNumberExpression("((3 + 4) * (1 / (12 - 8)))");
+    assertThat(sut.eval(), equalTo(1.75));
+    MultExpression multExpression = (MultExpression) sut;
+
+    // 3+4
+    assertThat(multExpression.getLeftOperand(), instanceOf(AddExpression.class));
+    assertThat(multExpression.getLeftOperand().eval(), equalTo(7.0));
+
+    // 1/(12-8)
+    assertThat(multExpression.getRightOperand(), instanceOf(DivExpression.class));
+    DivExpression divExpression = (DivExpression) multExpression.getRightOperand();
+    assertThat(divExpression.eval(), equalTo(0.25));
+
+    // 12-8
+    assertThat(divExpression.getRightOperand().eval(), equalTo(4.0));
+  }
+
+  @Test
+  public void expressionWithItem() throws IOException, Parser.Exception {
+    double itemValue = 5.3;
+    Item referenceItem = ParserUtils.parseItem("Number Item: id=\"myItem\" state=\"" + itemValue + "\";");
+    NumberExpression sut = ParserUtils.parseNumberExpression("(myItem * 3)", referenceItem.getRoot());
+    assertThat(sut.eval(), equalTo(itemValue * 3));
+
+    // set item state to new value
+    itemValue = 17;
+    referenceItem.setStateFromDouble(itemValue);
+    assertThat(sut.eval(), equalTo(itemValue * 3));
+  }
+
+  @Test
+  public void comparingExpressions() throws IOException, Parser.Exception {
+    comparingExpression(1, "<",  2, true);
+    comparingExpression(2, "<",  2, false);
+    comparingExpression(3, "<",  2, false);
+
+    comparingExpression(3, "<=", 4, true);
+    comparingExpression(4, "<=", 4, true);
+    comparingExpression(5, "<=", 4, false);
+
+    comparingExpression(5, "==", 6, false);
+    comparingExpression(6, "==", 6, true);
+
+    comparingExpression(7, "!=", 8, true);
+    comparingExpression(8, "!=", 8, false);
+
+    comparingExpression(9, ">=", 10, false);
+    comparingExpression(10, ">=", 10, true);
+    comparingExpression(11, ">=", 10, true);
+
+    comparingExpression(11,">",   12, false);
+    comparingExpression(12,">",   12, false);
+    comparingExpression(13,">",   12, true);
+  }
+
+  private void comparingExpression(double left, String actualComparatorString, double right, boolean expectedResult) throws IOException, Parser.Exception {
+    String expression = String.format("(%s %s %s)", Double.toString(left), actualComparatorString, Double.toString(right));
+    LogicalExpression sut = ParserUtils.parseLogicalExpression(expression);
+    assertThat(sut.eval(), equalTo(expectedResult));
+  }
+
+  @Test
+  public void notExpression() throws IOException, Parser.Exception {
+    LogicalExpression sut = ParserUtils.parseLogicalExpression("!(0==0)");
+    assertThat(sut.eval(), equalTo(false));
+    LogicalExpression sut2 = ParserUtils.parseLogicalExpression("!!(0==0)");
+    assertThat(sut2.eval(), equalTo(true));
+  }
+
+  @Test
+  public void andExpression() throws IOException, Parser.Exception {
+    LogicalExpression sut = ParserUtils.parseLogicalExpression("((0==0) & (0==0))");
+    assertThat(sut.eval(), equalTo(true));
+
+    LogicalExpression sut2 = ParserUtils.parseLogicalExpression("((0==0) & (0==1))");
+    assertThat(sut2.eval(), equalTo(false));
+
+    LogicalExpression sut3 = ParserUtils.parseLogicalExpression("((0==1) & (0==0))");
+    assertThat(sut3.eval(), equalTo(false));
+
+    LogicalExpression sut4 = ParserUtils.parseLogicalExpression("((0==1) & (0==1))");
+    assertThat(sut4.eval(), equalTo(false));
+  }
+
+  @Test
+  public void orExpression() throws IOException, Parser.Exception {
+    LogicalExpression sut = ParserUtils.parseLogicalExpression("((0==0) | (0==0))");
+    assertThat(sut.eval(), equalTo(true));
+
+    LogicalExpression sut2 = ParserUtils.parseLogicalExpression("((0==0) | (0==1))");
+    assertThat(sut2.eval(), equalTo(true));
+
+    LogicalExpression sut3 = ParserUtils.parseLogicalExpression("((0==1) | (0==0))");
+    assertThat(sut3.eval(), equalTo(true));
+
+    LogicalExpression sut4 = ParserUtils.parseLogicalExpression("((0==1) | (0==1))");
+    assertThat(sut4.eval(), equalTo(false));
+  }
+
+  @Test
+  public void parenthesizedLogicalExpression() throws IOException, Parser.Exception {
+    LogicalExpression sut = ParserUtils.parseLogicalExpression("((0==0))");
+    assertThat(sut.eval(), equalTo(true));
+    ParenthesizedLogicalExpression parenthesizedLogicalExpression = (ParenthesizedLogicalExpression) sut;
+    assertThat(parenthesizedLogicalExpression.getOperand().eval(), equalTo(true));
+  }
+}
diff --git a/eraser-base/src/test/java/de/tudresden/inf/st/eraser/ExpressionParserTest.java b/eraser-base/src/test/java/de/tudresden/inf/st/eraser/ExpressionParserTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..a034d3c8f8699046c9c89d2f139f14880a80b704
--- /dev/null
+++ b/eraser-base/src/test/java/de/tudresden/inf/st/eraser/ExpressionParserTest.java
@@ -0,0 +1,238 @@
+package de.tudresden.inf.st.eraser;
+
+import beaver.Parser;
+import de.tudresden.inf.st.eraser.jastadd.model.*;
+import de.tudresden.inf.st.eraser.util.ParserUtils;
+import org.junit.Assert;
+import org.junit.Test;
+
+import java.io.IOException;
+
+import static org.hamcrest.core.IsEqual.equalTo;
+import static org.hamcrest.core.IsInstanceOf.instanceOf;
+import static org.junit.Assert.assertThat;
+
+/**
+ * Test correct parsing of NumberExpression and LogicalExpression.
+ *
+ * @author rschoene - Initial contribution
+ */
+public class ExpressionParserTest {
+
+  private static final String TRUE_EXPRESSION = "(0.0==0.0)";
+
+  @Test
+  public void plusExpression() throws IOException, Parser.Exception {
+    NumberExpression sut = parseWithRoundTripNumberExpression("(3 + 4)");
+    assertThat(sut, instanceOf(AddExpression.class));
+    AddExpression addExpression = (AddExpression) sut;
+    assertThat(addExpression.getLeftOperand(), instanceOf(NumberLiteralExpression.class));
+    NumberLiteralExpression left = (NumberLiteralExpression) addExpression.getLeftOperand();
+    assertThat(left.getValue(), equalTo(3.0));
+    assertThat(addExpression.getRightOperand(), instanceOf(NumberLiteralExpression.class));
+    NumberLiteralExpression right = (NumberLiteralExpression) addExpression.getRightOperand();
+    assertThat(right.getValue(), equalTo(4.0));
+  }
+
+  @Test
+  public void minusExpression() throws IOException, Parser.Exception {
+    NumberExpression sut = parseWithRoundTripNumberExpression("(12.5 - 4.1)");
+    assertThat(sut, instanceOf(SubExpression.class));
+    SubExpression subExpression = (SubExpression) sut;
+    assertThat(subExpression.getLeftOperand(), instanceOf(NumberLiteralExpression.class));
+    NumberLiteralExpression left = (NumberLiteralExpression) subExpression.getLeftOperand();
+    assertThat(left.getValue(), equalTo(12.5));
+    assertThat(subExpression.getRightOperand(), instanceOf(NumberLiteralExpression.class));
+    NumberLiteralExpression right = (NumberLiteralExpression) subExpression.getRightOperand();
+    assertThat(right.getValue(), equalTo(4.1));
+  }
+
+  @Test
+  public void multExpression() throws IOException, Parser.Exception {
+    NumberExpression sut = parseWithRoundTripNumberExpression("(0 * 0)");
+    assertThat(sut, instanceOf(MultExpression.class));
+    MultExpression multExpression = (MultExpression) sut;
+    assertThat(multExpression.getLeftOperand(), instanceOf(NumberLiteralExpression.class));
+    NumberLiteralExpression left = (NumberLiteralExpression) multExpression.getLeftOperand();
+    assertThat(left.getValue(), equalTo(0.0));
+    assertThat(multExpression.getRightOperand(), instanceOf(NumberLiteralExpression.class));
+    NumberLiteralExpression right = (NumberLiteralExpression) multExpression.getRightOperand();
+    assertThat(right.getValue(), equalTo(0.0));
+  }
+
+  @Test
+  public void divExpression() throws IOException, Parser.Exception {
+    NumberExpression sut = parseWithRoundTripNumberExpression("(1.1 / 0.0)");
+    assertThat(sut, instanceOf(DivExpression.class));
+    DivExpression divExpression = (DivExpression) sut;
+    assertThat(divExpression.getLeftOperand(), instanceOf(NumberLiteralExpression.class));
+    NumberLiteralExpression left = (NumberLiteralExpression) divExpression.getLeftOperand();
+    assertThat(left.getValue(), equalTo(1.1));
+    assertThat(divExpression.getRightOperand(), instanceOf(NumberLiteralExpression.class));
+    NumberLiteralExpression right = (NumberLiteralExpression) divExpression.getRightOperand();
+    assertThat(right.getValue(), equalTo(0.0));
+  }
+
+  @Test
+  public void powerExpression() throws IOException, Parser.Exception {
+    NumberExpression sut = parseWithRoundTripNumberExpression("(3 ^ 0.5)");
+    assertThat(sut, instanceOf(PowerExpression.class));
+    PowerExpression powExpression = (PowerExpression) sut;
+    assertThat(powExpression.getLeftOperand(), instanceOf(NumberLiteralExpression.class));
+    NumberLiteralExpression left = (NumberLiteralExpression) powExpression.getLeftOperand();
+    assertThat(left.getValue(), equalTo(3.0));
+    assertThat(powExpression.getRightOperand(), instanceOf(NumberLiteralExpression.class));
+    NumberLiteralExpression right = (NumberLiteralExpression) powExpression.getRightOperand();
+    assertThat(right.getValue(), equalTo(0.5));
+  }
+
+  @Test
+  public void parenthesizedExpression() throws IOException, Parser.Exception {
+    NumberExpression sut = parseWithRoundTripNumberExpression("(3)");
+    assertThat(sut, instanceOf(ParenthesizedNumberExpression.class));
+    ParenthesizedNumberExpression parenExpression = (ParenthesizedNumberExpression) sut;
+    assertThat(parenExpression.getOperand(), instanceOf(NumberLiteralExpression.class));
+    NumberLiteralExpression left = (NumberLiteralExpression) parenExpression.getOperand();
+    assertThat(left.getValue(), equalTo(3.0));
+  }
+
+  @Test
+  public void complexExpression() throws IOException, Parser.Exception {
+    NumberExpression sut = parseWithRoundTripNumberExpression("((3 + 4) * (1 / (12 - 8)))");
+    assertThat(sut, instanceOf(MultExpression.class));
+    MultExpression multExpression = (MultExpression) sut;
+
+    // 3+4
+    assertThat(multExpression.getLeftOperand(), instanceOf(AddExpression.class));
+    AddExpression addExpression = (AddExpression) multExpression.getLeftOperand();
+    assertThat(addExpression.getLeftOperand(), instanceOf(NumberLiteralExpression.class));
+    NumberLiteralExpression left = (NumberLiteralExpression) addExpression.getLeftOperand();
+    assertThat(left.getValue(), equalTo(3.0));
+    assertThat(addExpression.getRightOperand(), instanceOf(NumberLiteralExpression.class));
+    NumberLiteralExpression right = (NumberLiteralExpression) addExpression.getRightOperand();
+    assertThat(right.getValue(), equalTo(4.0));
+
+    // 1/(12-8)
+    assertThat(multExpression.getRightOperand(), instanceOf(DivExpression.class));
+    DivExpression divExpression = (DivExpression) multExpression.getRightOperand();
+
+    assertThat(divExpression.getLeftOperand(), instanceOf(NumberLiteralExpression.class));
+    NumberLiteralExpression leftOfDiv = (NumberLiteralExpression) divExpression.getLeftOperand();
+    assertThat(leftOfDiv.getValue(), equalTo(1.0));
+
+    // 12-8
+    assertThat(divExpression.getRightOperand(), instanceOf(SubExpression.class));
+    SubExpression rightofDiv = (SubExpression) divExpression.getRightOperand();
+    assertThat(rightofDiv.getLeftOperand(), instanceOf(NumberLiteralExpression.class));
+    NumberLiteralExpression leftOfSSub = (NumberLiteralExpression) rightofDiv.getLeftOperand();
+    assertThat(leftOfSSub.getValue(), equalTo(12.0));
+    assertThat(rightofDiv.getRightOperand(), instanceOf(NumberLiteralExpression.class));
+    NumberLiteralExpression rightOfSub = (NumberLiteralExpression) rightofDiv.getRightOperand();
+    assertThat(rightOfSub.getValue(), equalTo(8.0));
+  }
+
+  @Test
+  public void expressionWithItem() throws IOException, Parser.Exception {
+    Item referenceItem = ParserUtils.parseItem("Number Item: id=\"myItem\";");
+    NumberExpression sut = parseWithRoundTripNumberExpression("(myItem * 3)", referenceItem.getRoot());
+    assertThat(sut, instanceOf(MultExpression.class));
+    MultExpression multExpression = (MultExpression) sut;
+    assertThat(multExpression.getLeftOperand(), instanceOf(Designator.class));
+    Designator left = (Designator) multExpression.getLeftOperand();
+    assertThat(left.getItem(), equalTo(referenceItem));
+    assertThat(multExpression.getRightOperand(), instanceOf(NumberLiteralExpression.class));
+    NumberLiteralExpression right = (NumberLiteralExpression) multExpression.getRightOperand();
+    assertThat(right.getValue(), equalTo(3.0));
+  }
+
+  @Test
+  public void comparingExpressions() throws IOException, Parser.Exception {
+    comparingExpression("<",  ComparatorType.LessThan, 1, 2);
+    comparingExpression("<=", ComparatorType.LessOrEqualThan, 3, 4);
+    comparingExpression("==", ComparatorType.Equals, 5, 6);
+    comparingExpression("!=", ComparatorType.NotEquals, 7, 8);
+    comparingExpression(">=", ComparatorType.GreaterOrEqualThan, 9, 10);
+    comparingExpression(">",  ComparatorType.GreaterThan, 11, 12);
+  }
+
+  private void comparingExpression(String actualComparatorString, ComparatorType expectedComparatorType, double left, double right) throws IOException, Parser.Exception {
+    String expression = String.format("(%s %s %s)", Double.toString(left), actualComparatorString, Double.toString(right));
+    LogicalExpression sut = parseWithRoundTripLogicalExpression(expression);
+    assertThat(sut, instanceOf(ComparingExpression.class));
+    ComparingExpression comparingExpression = (ComparingExpression) sut;
+    assertThat(comparingExpression.getLeftOperand(), instanceOf(NumberLiteralExpression.class));
+    NumberLiteralExpression leftExpression = (NumberLiteralExpression) comparingExpression.getLeftOperand();
+    assertThat(leftExpression.getValue(), equalTo(left));
+    assertThat(comparingExpression.getRightOperand(), instanceOf(NumberLiteralExpression.class));
+    NumberLiteralExpression rightExpression = (NumberLiteralExpression) comparingExpression.getRightOperand();
+    assertThat(rightExpression.getValue(), equalTo(right));
+    assertThat(comparingExpression.getComparator(), equalTo(expectedComparatorType));
+  }
+
+  @Test
+  public void notExpression() throws IOException, Parser.Exception {
+    LogicalExpression sut = parseWithRoundTripLogicalExpression("!" + TRUE_EXPRESSION);
+    assertThat(sut, instanceOf(NotExpression.class));
+    NotExpression notExpression = (NotExpression) sut;
+    checkZeroEqualsZero(notExpression.getOperand());
+  }
+
+  @Test
+  public void andExpression() throws IOException, Parser.Exception {
+    LogicalExpression sut = parseWithRoundTripLogicalExpression("(" + TRUE_EXPRESSION + " & " + TRUE_EXPRESSION + ")");
+    assertThat(sut, instanceOf(AndExpression.class));
+    AndExpression notExpression = (AndExpression) sut;
+    checkZeroEqualsZero(notExpression.getLeftOperand());
+    checkZeroEqualsZero(notExpression.getRightOperand());
+  }
+
+  @Test
+  public void orExpression() throws IOException, Parser.Exception {
+    LogicalExpression sut = parseWithRoundTripLogicalExpression("(" + TRUE_EXPRESSION + " | " + TRUE_EXPRESSION + ")");
+    assertThat(sut, instanceOf(OrExpression.class));
+    OrExpression notExpression = (OrExpression) sut;
+    checkZeroEqualsZero(notExpression.getLeftOperand());
+    checkZeroEqualsZero(notExpression.getRightOperand());
+  }
+
+  @Test
+  public void parenthesizedLogicalExpression() throws IOException, Parser.Exception {
+    LogicalExpression sut = parseWithRoundTripLogicalExpression("(" + TRUE_EXPRESSION + ")");
+    assertThat(sut, instanceOf(ParenthesizedLogicalExpression.class));
+    ParenthesizedLogicalExpression parenthesizedLogicalExpression = (ParenthesizedLogicalExpression) sut;
+    checkZeroEqualsZero(parenthesizedLogicalExpression.getOperand());
+  }
+
+  private NumberExpression parseWithRoundTripNumberExpression(String numberExpression) throws IOException, Parser.Exception {
+    return parseWithRoundTripNumberExpression(numberExpression, null);
+  }
+
+  private NumberExpression parseWithRoundTripNumberExpression(String numberExpression, Root root) throws IOException, Parser.Exception {
+    NumberExpression sut = ParserUtils.parseNumberExpression(numberExpression, root);
+    String first = sut.prettyPrint();
+    NumberExpression reParsed = ParserUtils.parseNumberExpression(first, root);
+    String second = reParsed.prettyPrint();
+    assertThat(first, equalTo(second));
+    return sut;
+  }
+
+  private LogicalExpression parseWithRoundTripLogicalExpression(String logicalExpression) throws IOException, Parser.Exception {
+    LogicalExpression sut = ParserUtils.parseLogicalExpression(logicalExpression);
+    String first = sut.prettyPrint();
+    LogicalExpression reParsed = ParserUtils.parseLogicalExpression(first);
+    String second = reParsed.prettyPrint();
+    assertThat(first, equalTo(second));
+    return sut;
+  }
+
+  private void checkZeroEqualsZero(LogicalExpression logicalExpression) {
+    assertThat(logicalExpression, instanceOf(ComparingExpression.class));
+    ComparingExpression comparingExpression = (ComparingExpression) logicalExpression;
+    assertThat(comparingExpression.getLeftOperand(), instanceOf(NumberLiteralExpression.class));
+    NumberLiteralExpression leftOfSSub = (NumberLiteralExpression) comparingExpression.getLeftOperand();
+    assertThat(leftOfSSub.getValue(), equalTo(0.0));
+    assertThat(comparingExpression.getRightOperand(), instanceOf(NumberLiteralExpression.class));
+    NumberLiteralExpression rightOfSub = (NumberLiteralExpression) comparingExpression.getRightOperand();
+    assertThat(rightOfSub.getValue(), equalTo(0.0));
+  }
+}
diff --git a/eraser-base/src/test/java/de/tudresden/inf/st/eraser/InfluxTest.java b/eraser-base/src/test/java/de/tudresden/inf/st/eraser/InfluxTest.java
index 5707ad2eeb2e516626f7608c159046e84c3ec6c5..9526a4db04802684261f820e807db35d6a47c24f 100644
--- a/eraser-base/src/test/java/de/tudresden/inf/st/eraser/InfluxTest.java
+++ b/eraser-base/src/test/java/de/tudresden/inf/st/eraser/InfluxTest.java
@@ -1,10 +1,12 @@
 package de.tudresden.inf.st.eraser;
 
-import de.tudresden.inf.st.eraser.TestUtils.ModelAndItem;
 import de.tudresden.inf.st.eraser.jastadd.model.*;
+import de.tudresden.inf.st.eraser.util.TestUtils;
+import de.tudresden.inf.st.eraser.util.TestUtils.ModelAndItem;
 import org.junit.*;
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
+import org.testcontainers.containers.InfluxDBContainer;
 
 import java.time.Instant;
 import java.util.ArrayList;
@@ -28,6 +30,12 @@ public class InfluxTest {
 
   private ModelAndItem mai;
 
+  @ClassRule
+  public static InfluxDBContainer influxDbContainer = new InfluxDBContainer()
+      .withDatabase(InfluxRoot.createDefault().getDbName())
+      .withAdmin(InfluxRoot.createDefault().getUser())
+      .withAdminPassword(InfluxRoot.createDefault().getPassword());
+
   @Test
   public void oneItem() {
     NumberItem item = mai.item;
@@ -94,8 +102,8 @@ public class InfluxTest {
 
   @Test
   public void justAdapter() {
-    InfluxAdapter influxAdapter = mai.model.getInfluxRoot().influxAdapter();
-    Assume.assumeTrue("Adapter not connected", influxAdapter.isConnected());
+    InfluxAdapter influxAdapter = getInfluxRoot().influxAdapter();
+    Assert.assertTrue("Adapter not connected", influxAdapter.isConnected());
     influxAdapter.deleteDatabase();
 
     // write one point
@@ -146,16 +154,21 @@ public class InfluxTest {
           point -> points.add((DoubleStatePoint) point));
     } else {
       influxRoot = InfluxRoot.createDefault();
-      // use zih vm2098 running influx
+      // use container running influx
       influxRoot.setDbName(InfluxTest.class.getSimpleName());
-      influxRoot.setHostByName("vm2098.zih.tu-dresden.de");
+      System.out.println("ports: " + influxDbContainer.getPortBindings() + " url: '" + influxDbContainer.getUrl() + "'");
+      influxRoot.setHostByName(influxDbContainer.getUrl().replaceAll("^http://", ""));
     }
-    mai.model.setInfluxRoot(influxRoot);
+    mai.model.getRoot().setInfluxRoot(influxRoot);
     Assume.assumeTrue(influxRoot.influxAdapter().isConnected());
     influxRoot.influxAdapter().deleteDatabase();
     return mai;
   }
 
+  private InfluxRoot getInfluxRoot() {
+    return mai.model.getRoot().getInfluxRoot();
+  }
+
   private List<DoubleStatePoint> query(ItemWithDoubleState... allItems) {
     if (useStub) {
       return points;
@@ -171,13 +184,13 @@ public class InfluxTest {
   @Before
   public void setNewModel() {
     mai = createModel();
-    mai.model.getInfluxRoot().influxAdapter().diableAsyncQuery();
+    getInfluxRoot().influxAdapter().disableAsyncQuery();
   }
 
   @After
   public void closeInfluxAdapter() throws Exception {
     if (mai != null && mai.model != null) {
-      mai.model.getInfluxRoot().influxAdapter().close();
+      getInfluxRoot().influxAdapter().close();
     }
   }
 
diff --git a/eraser-base/src/test/java/de/tudresden/inf/st/eraser/ItemTests.java b/eraser-base/src/test/java/de/tudresden/inf/st/eraser/ItemTests.java
index 0b02f08b9d6c12e2ac51935d2c1da8bb7660c5e6..ffda213bea9a359097db8b962f03a48e52502c57 100644
--- a/eraser-base/src/test/java/de/tudresden/inf/st/eraser/ItemTests.java
+++ b/eraser-base/src/test/java/de/tudresden/inf/st/eraser/ItemTests.java
@@ -1,9 +1,12 @@
 package de.tudresden.inf.st.eraser;
 
 import de.tudresden.inf.st.eraser.jastadd.model.*;
+import de.tudresden.inf.st.eraser.util.TestUtils;
+import de.tudresden.inf.st.eraser.util.TestUtils.ModelAndItem;
 import org.junit.Assert;
 import org.junit.Test;
 
+import java.time.Instant;
 import java.util.Date;
 
 /**
@@ -15,7 +18,7 @@ public class ItemTests {
 
   @Test
   public void testItemWithBooleanStateEquals() {
-    ItemWithBooleanState sut = new SwitchItem();
+    ItemWithBooleanState sut = createItem(SwitchItem::new);
 
     sut.setState(true);
     Assert.assertTrue("State 'true' should match 'true'", sut.stateEquals(true));
@@ -28,7 +31,7 @@ public class ItemTests {
 
   @Test
   public void testItemWithStringStateEquals() {
-    ItemWithStringState sut = new ImageItem();
+    ItemWithStringState sut = createItem(ImageItem::new);
 
     sut.setState("correct");
     Assert.assertTrue("State 'correct' should match 'correct'",
@@ -39,7 +42,7 @@ public class ItemTests {
 
   @Test
   public void testItemWithDoubleStateEquals() {
-    ItemWithDoubleState sut = new NumberItem();
+    ItemWithDoubleState sut = createItem(NumberItem::new);
 
     sut.setState(3.0);
     Assert.assertTrue("State '3.0' should match '3.0'", sut.stateEquals(3.0));
@@ -52,7 +55,7 @@ public class ItemTests {
 
   @Test
   public void testItemWithTupleHSBStateEquals() {
-    ColorItem sut = new ColorItem();
+    ColorItem sut = createItem(ColorItem::new);
 
     sut.setState(TupleHSB.of(1, 2, 3));
     Assert.assertTrue("State 'TupleHSB(1,2,3)' should match 'TupleHSB(1,2,3)'",
@@ -71,73 +74,27 @@ public class ItemTests {
 
   @Test
   public void testItemWithDateStateEquals() {
-    DateTimeItem sut = new DateTimeItem();
+    DateTimeItem sut = createItem(DateTimeItem::new);
 
-    sut.setState(new Date(1543415826));
+    sut.setState(Instant.ofEpochMilli(1543415826));
     Assert.assertTrue("State 'Date(1543415826)' should match 'Date(1543415826)'",
-        sut.stateEquals(new Date(1543415826)));
+        sut.stateEquals(Instant.ofEpochMilli(1543415826)));
     Assert.assertFalse("State 'Date(1543415826)' should not match 'Date(4)'",
-        sut.stateEquals(new Date(4)));
+        sut.stateEquals(Instant.ofEpochMilli(4)));
   }
 
-  @Test
-  public void testItemWithBooleanCopyEquals() {
-    ItemWithBooleanState sut = new SwitchItem();
-
-    sut.setState(true);
-    Object copiedState = sut.copyState();
-    Assert.assertTrue("State 'true' should match copy", sut.stateEquals(copiedState));
-
-    sut.setState(false);
-    Assert.assertFalse("State 'false' should not match copy", sut.stateEquals(copiedState));
-  }
-
-  @Test
-  public void testItemWithStringCopyEquals() {
-    ItemWithStringState sut = new ImageItem();
-
-    sut.setState("correct");
-    Object copiedState = sut.copyState();
-    Assert.assertTrue("State 'correct' should match copy", sut.stateEquals(copiedState));
-
-    sut.setState("something else");
-    Assert.assertFalse("State 'something else' should not match copy", sut.stateEquals(copiedState));
+  @FunctionalInterface
+  private interface CreateItem<T extends Item> {
+    T create();
   }
 
-  @Test
-  public void testItemWithLongCopyEquals() {
-    ItemWithDoubleState sut = new NumberItem();
-
-    sut.setState(3.0);
-    Object copiedState = sut.copyState();
-    Assert.assertTrue("State '3.0' should match copy", sut.stateEquals(copiedState));
-
-    sut.setState(4.0);
-    Assert.assertFalse("State '4.0' should not match copy", sut.stateEquals(copiedState));
-  }
-
-  @Test
-  public void testItemWithTupleHSBCopyEquals() {
-    ColorItem sut = new ColorItem();
-
-    sut.setState(TupleHSB.of(1, 2, 3));
-    Object copiedState = sut.copyState();
-    Assert.assertTrue("State 'TupleHSB(1,2,3)' should match copy", sut.stateEquals(copiedState));
-
-    sut.setState(TupleHSB.of(5,5,5));
-    Assert.assertFalse("State 'TupleHSB(5,5,5)' should not match copy", sut.stateEquals(copiedState));
-  }
-
-  @Test
-  public void testItemWithDateCopyEquals() {
-    DateTimeItem sut = new DateTimeItem();
-
-    sut.setState(new Date(1543415826));
-    Object copiedState = sut.copyState();
-    Assert.assertTrue("State 'Date(1543415826' should match copy", sut.stateEquals(copiedState));
-
-    sut.setState(new Date(4));
-    Assert.assertFalse("State 'Date(4)' should not match copy", sut.stateEquals(copiedState));
+  private <T extends  Item> T createItem(CreateItem<T> creator) {
+    // create a root with default group and one unused item
+    ModelAndItem mai = TestUtils.createModelAndItem(0);
+    // create wanted item, add it to default group, and return it
+    T result = creator.create();
+    TestUtils.getDefaultGroup(mai.model).addItem(result);
+    return result;
   }
 
 }
diff --git a/eraser-base/src/test/java/de/tudresden/inf/st/eraser/MarshallingTests.java b/eraser-base/src/test/java/de/tudresden/inf/st/eraser/MarshallingTests.java
index 53c420a3023e689cfcef2a58816ddf74b2767ce9..abcbd203be180e81b766bc48aa66cdb7778efae7 100644
--- a/eraser-base/src/test/java/de/tudresden/inf/st/eraser/MarshallingTests.java
+++ b/eraser-base/src/test/java/de/tudresden/inf/st/eraser/MarshallingTests.java
@@ -13,8 +13,8 @@ import org.junit.runners.Parameterized;
  *
  * @author rschoene - Initial contribution
  */
-@RunWith(ParallelParameterized.class)
-//@RunWith(Parameterized.class)
+//@RunWith(ParallelParameterized.class)
+@RunWith(Parameterized.class)
 public class MarshallingTests {
 
   private static final TestProperties properties = new TestProperties();
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 73520f3f406474d04ebe2d93db2a6042702a4ee0..28a8d664cd88f53495a7049b065f7ef206230e39 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
@@ -2,11 +2,17 @@ package de.tudresden.inf.st.eraser;
 
 import de.tudresden.inf.st.eraser.jastadd.model.*;
 import de.tudresden.inf.st.eraser.util.MqttReceiver;
+import de.tudresden.inf.st.eraser.util.TestUtils;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
 import org.junit.Assert;
 import org.junit.Assume;
+import org.junit.ClassRule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
+import org.testcontainers.containers.GenericContainer;
+import org.testcontainers.containers.wait.strategy.Wait;
 
 import java.io.IOException;
 import java.util.ArrayList;
@@ -34,25 +40,29 @@ public class MqttTests {
   private static final double thirdState = 3.0;
 
   private List<String> messages = new ArrayList<>();
+  private static Logger logger = LogManager.getLogger(MqttTests.class);
 
+  @ClassRule
+  public static GenericContainer mqttBroker = new GenericContainer<>("eclipse-mosquitto:1.5")
+      .withExposedPorts(1883);
 
   @Test
   public void resolve1() {
-    RootItemAndTwoTopics rootAB = createAB();
-    MqttRoot sut = rootAB.model.getMqttRoot();
+    ModelItemAndTwoTopics modelAB = createAB();
+    MqttRoot sut = modelAB.model.getRoot().getMqttRoot();
 
     // incoming mqtt topic might be "inc/a/" or "inc/a/b"
 
     Assert.assertTrue(sut.resolveTopic("inc/a").isPresent());
-    Assert.assertEquals("Could not resolve a.", rootAB.firstTopic, sut.resolveTopic("inc/a").get());
+    Assert.assertEquals("Could not resolve a.", modelAB.firstTopic, sut.resolveTopic("inc/a").get());
     Assert.assertTrue(sut.resolveTopic("inc/a/b").isPresent());
-    Assert.assertEquals("Could not resolve a/b.", rootAB.secondTopic, sut.resolveTopic("inc/a/b").get());
+    Assert.assertEquals("Could not resolve a/b.", modelAB.secondTopic, sut.resolveTopic("inc/a/b").get());
   }
 
   @Test
   public void brokerConnected() throws Exception {
-    RootItemAndTwoTopics rootAB = createAB();
-    MqttRoot sut = rootAB.model.getMqttRoot();
+    ModelItemAndTwoTopics modelAB = createAB();
+    MqttRoot sut = modelAB.model.getRoot().getMqttRoot();
 //    MqttRoot mqttRoot = new MqttRoot();
 //    mqttRoot.setHostByName("localhost");
 //    MQTTSender sender = new MQTTSenderImpl().setHost(mqttRoot.getHost());
@@ -66,59 +76,46 @@ public class MqttTests {
   public void itemUpdateSend1() throws Exception {
     String expectedTopic = outgoingPrefix + "/" + firstPart + "/" + secondPart;
 
-    RootItemAndTwoTopics rootAB = createAB();
-    assumeSenderConnected(rootAB.model);
+    ModelItemAndTwoTopics modelAB = createAB();
+    assertSenderConnected(modelAB);
 
-    NumberItem sut = rootAB.item;
-    sut.setTopic(rootAB.secondTopic);
+    NumberItem sut = modelAB.item;
+    sut.setTopic(modelAB.secondTopic);
 
     createMqttReceiver(expectedTopic);
 
     sut.setState(firstState);
-    sut.sendState();
 
     assertTimeoutEquals(2, 1, messages::size);
     Assert.assertEquals(Double.toString(firstState), messages.get(0));
 
     sut.setState(secondState);
-    sut.sendState();
 
     assertTimeoutEquals(2, 2, messages::size);
     Assert.assertEquals(Double.toString(secondState), messages.get(1));
   }
 
-  private void assumeSenderConnected(Root model) {
-    Assume.assumeTrue("Broker is not connected", model.getMqttRoot().getMqttSender().isConnected());
-  }
-
   @Test
   public void itemUpdateSend2() throws Exception {
     String expectedTopic1 = outgoingPrefix + "/" + firstPart + "/" + secondPart;
     String expectedTopic2 = outgoingPrefix + "/" + alternativeFirstPart + "/" + secondPart;
 
-    RootItemAndTwoTopics rootAB = createAB();
-    assumeSenderConnected(rootAB.model);
+    ModelItemAndTwoTopics modelAB = createAB();
+    assertSenderConnected(modelAB);
 
-    NumberItem item1 = rootAB.item;
-    item1.setTopic(rootAB.secondTopic);
+    NumberItem item1 = modelAB.item;
+    item1.setTopic(modelAB.secondTopic);
 
-    MqttTopic alternative = new MqttTopic();
-    alternative.setPart(alternativeFirstPart);
-    MqttTopic alternativeB = new MqttTopic();
-    alternativeB.setPart(secondPart);
-    alternative.addSubTopic(alternativeB);
-    rootAB.model.getMqttRoot().addTopic(alternative);
+    MqttTopic alternativeB = createAndAddMqttTopic(modelAB.model.getRoot().getMqttRoot(),alternativeFirstPart + "/" + secondPart);
 
-    NumberItem item2 = TestUtils.addItemTo(rootAB.model, 0);
+    NumberItem item2 = TestUtils.addItemTo(modelAB.model, 0, true);
     item2.setTopic(alternativeB);
 
     createMqttReceiver(expectedTopic1, expectedTopic2);
 
     item1.setState(firstState);
-    item1.sendState();
 
     item2.setState(firstState);
-    item2.sendState();
 
     assertTimeoutEquals(2, 2, messages::size);
     Assert.assertEquals(Double.toString(firstState), messages.get(0));
@@ -126,8 +123,6 @@ public class MqttTests {
 
     item1.setState(secondState);
     item2.setState(thirdState);
-    item1.sendState();
-    item2.sendState();
 
     assertTimeoutEquals(3, 4, messages::size);
     // TODO actually this does not test, whether the topic was correct for each state
@@ -135,6 +130,19 @@ public class MqttTests {
     Assert.assertThat(messages, hasItem(Double.toString(thirdState)));
   }
 
+  private void assertSenderConnected(ModelItemAndTwoTopics modelAB) {
+    MqttRoot mqttRoot = modelAB.model.getRoot().getMqttRoot();
+    mqttBroker.waitingFor(Wait.forHealthcheck());
+    if (!mqttRoot.getMqttSender().isConnected()) {
+      try {
+        Thread.sleep(1000);
+      } catch (InterruptedException e) {
+        logger.catching(e);
+      }
+    }
+    Assert.assertTrue("Broker is not connected", mqttRoot.getMqttSender().isConnected());
+  }
+
   private void createMqttReceiver(String... expectedTopics) throws IOException {
     if (useStub) {
       // do not need receiver, as messages are directly written by MqttSenderStub and callback
@@ -142,7 +150,8 @@ public class MqttTests {
     }
     MqttReceiver receiver = new MqttReceiver();
     List<String> expectedTopicList = Arrays.asList(expectedTopics);
-    receiver.setHost("localhost");
+//    receiver.setHost("localhost", 1883);
+    receiver.setHost(mqttBroker.getContainerIpAddress(), mqttBroker.getFirstMappedPort());
     receiver.setTopicsForSubscription(expectedTopics);
     receiver.setOnMessage((topic, message) -> {
       Assert.assertThat(expectedTopicList, hasItem(topic));
@@ -152,9 +161,9 @@ public class MqttTests {
     receiver.waitUntilReady(2, TimeUnit.SECONDS);
   }
 
-  private RootItemAndTwoTopics createAB() {
-    TestUtils.ModelAndItem mai = TestUtils.createModelAndItem(0);
-    Root model = mai.model;
+  private ModelItemAndTwoTopics createAB() {
+    TestUtils.ModelAndItem mai = TestUtils.createModelAndItem(0, true);
+    OpenHAB2Model model = mai.model;
     MqttRoot mqttRoot = new MqttRoot();
     mqttRoot.setIncomingPrefix("inc");
     mqttRoot.setOutgoingPrefix(outgoingPrefix);
@@ -163,26 +172,23 @@ public class MqttTests {
       // now a SenderStub is being used
       ((MQTTSenderStub) mqttRoot.getMqttSender()).setCallback(((topic, message, qos) -> messages.add(message)));
     } else {
-      mqttRoot.setHostByName("localhost");
+//      mqttRoot.setHostByName("localhost");
+      mqttRoot.setHost(ExternalHost.of(mqttBroker.getContainerIpAddress(), mqttBroker.getFirstMappedPort()));
     }
-    MqttTopic a = new MqttTopic();
-    a.setPart(firstPart);
-    MqttTopic ab = new MqttTopic();
-    ab.setPart(secondPart);
-    a.addSubTopic(ab);
-    mqttRoot.addTopic(a);
+    MqttTopic a = createAndAddMqttTopic(mqttRoot, firstPart);
+    MqttTopic ab = createAndAddMqttTopic(mqttRoot, firstPart + "/" + secondPart);
     mqttRoot.ensureCorrectPrefixes();
-    model.setMqttRoot(mqttRoot);
-    return RootItemAndTwoTopics.of(model, mai.item, a, ab);
+    model.getRoot().setMqttRoot(mqttRoot);
+    return ModelItemAndTwoTopics.of(model, mai.item, a, ab);
   }
 
-  static class RootItemAndTwoTopics {
-    Root model;
+  static class ModelItemAndTwoTopics {
+    OpenHAB2Model model;
     NumberItem item;
     MqttTopic firstTopic;
     MqttTopic secondTopic;
-    static RootItemAndTwoTopics of(Root model, NumberItem item, MqttTopic firstTopic, MqttTopic secondTopic) {
-      RootItemAndTwoTopics result = new RootItemAndTwoTopics();
+    static ModelItemAndTwoTopics of(OpenHAB2Model model, NumberItem item, MqttTopic firstTopic, MqttTopic secondTopic) {
+      ModelItemAndTwoTopics result = new ModelItemAndTwoTopics();
       result.model = model;
       result.item = item;
       result.firstTopic = firstTopic;
@@ -191,6 +197,13 @@ public class MqttTests {
     }
   }
 
+  private MqttTopic createAndAddMqttTopic(MqttRoot mqttRoot, String suffix) {
+    MqttTopic result = new MqttTopic();
+    result.setTopicString(suffix);
+    mqttRoot.addTopic(result);
+    return result;
+  }
+
   private <T> void assertTimeoutEquals(long seconds, T expected, Supplier<T> actualProvider) throws InterruptedException {
     if (expected == actualProvider.get()) {
       // already matched right now. return immediately.
diff --git a/eraser-base/src/test/java/de/tudresden/inf/st/eraser/NeuralNetworkTest.java b/eraser-base/src/test/java/de/tudresden/inf/st/eraser/NeuralNetworkTest.java
index 40295e580fcae466225c6717e2cad9fe48b248de..0eb593fd0fc6c3cb3401f315afe33d550e4b3658 100644
--- a/eraser-base/src/test/java/de/tudresden/inf/st/eraser/NeuralNetworkTest.java
+++ b/eraser-base/src/test/java/de/tudresden/inf/st/eraser/NeuralNetworkTest.java
@@ -1,6 +1,8 @@
 package de.tudresden.inf.st.eraser;
 
 import de.tudresden.inf.st.eraser.jastadd.model.*;
+import de.tudresden.inf.st.eraser.util.TestUtils;
+import de.tudresden.inf.st.eraser.util.TestUtils.ModelAndItem;
 import org.junit.Assert;
 import org.junit.Test;
 
@@ -13,7 +15,7 @@ public class NeuralNetworkTest {
 
   @Test
   public void simpleNetwork() {
-    TestUtils.ModelAndItem mai = TestUtils.createModelAndItem(1);
+    ModelAndItem mai = TestUtils.createModelAndItem(1);
 
     /* I..Input, H..Hidden, O..Output
      * (I) -> (H) -  0.4 -> (O1)
@@ -21,8 +23,8 @@ public class NeuralNetworkTest {
      *  f=    4*x           x>4?
      */
 
-    NeuralNetworkRoot neuralNetworkRoot = new NeuralNetworkRoot();
-    mai.model.getMachineLearningRoot().setPreferenceLearning(neuralNetworkRoot);
+    NeuralNetworkRoot neuralNetworkRoot = NeuralNetworkRoot.createEmpty();
+    mai.model.getRoot().getMachineLearningRoot().setPreferenceLearning(neuralNetworkRoot);
     InputNeuron inputNeuron = new InputNeuron();
     inputNeuron.setItem(mai.item);
     HiddenNeuron hiddenNeuron = new HiddenNeuron();
diff --git a/eraser-base/src/test/java/de/tudresden/inf/st/eraser/OpenHabImporterTest.java b/eraser-base/src/test/java/de/tudresden/inf/st/eraser/OpenHabImporterTest.java
index 18f51988bde6a5be42f35dd5e7d076a59069f983..8c109638fea74d074cfc72168303d68a5d2e69a7 100644
--- a/eraser-base/src/test/java/de/tudresden/inf/st/eraser/OpenHabImporterTest.java
+++ b/eraser-base/src/test/java/de/tudresden/inf/st/eraser/OpenHabImporterTest.java
@@ -1,5 +1,6 @@
 package de.tudresden.inf.st.eraser;
 
+import de.tudresden.inf.st.eraser.jastadd.model.OpenHAB2Model;
 import de.tudresden.inf.st.eraser.jastadd.model.Root;
 import de.tudresden.inf.st.eraser.jastadd_test.core.*;
 import de.tudresden.inf.st.eraser.openhab2.OpenHab2Importer;
@@ -38,19 +39,24 @@ public class OpenHabImporterTest {
     protected URL makeURL(String formatUrlString, String hostAndPort) throws MalformedURLException {
       // read from files instead of making an http request
       switch (formatUrlString) {
-        case thingTypesUrl: return makeFileUrl("thing-types.json");
-        case channelTypeUrl: return makeFileUrl("channel-types.json");
-        case thingsUrl: return makeFileUrl("things.json");
-        case itemsUrl: return makeFileUrl("items.json");
-        case linksUrl: return makeFileUrl("links.json");
+        case thingTypesUrl: return makeFileUrl("thing-types.json", "empty-list.json");
+        case channelTypeUrl: return makeFileUrl("channel-types.json", "empty-list.json");
+        case thingsUrl: return makeFileUrl("things.json", "empty-list.json");
+        case itemsUrl: return makeFileUrl("items.json", "empty-list.json");
+        case linksUrl: return makeFileUrl("links.json", "empty-list.json");
         default: throw new IllegalArgumentException("Unsupported formatURLString: " + formatUrlString);
       }
     }
 
-    private URL makeFileUrl(String fileName) throws MalformedURLException {
+    @Override
+    protected URL makeURL(String formatUrlString, String hostAndPort, String id) throws MalformedURLException {
+      return makeFileUrl(id + ".json", "empty.json");
+    }
+
+    private URL makeFileUrl(String fileName, String emptyFileName) throws MalformedURLException {
       Path path = Paths.get(directory, fileName);
       if (!path.toFile().exists() || !path.toFile().isFile()) {
-        path = Paths.get(directory, "..", "empty.json");
+        path = Paths.get(directory, "..", emptyFileName);
       }
       return path.toUri().toURL();
     }
@@ -65,7 +71,7 @@ public class OpenHabImporterTest {
 
       // call the modified importer, with the static host name, and an arbitrary chosen port
       // port will not be used during the test
-      Root model = importer.importFrom(HOST, 80);
+      OpenHAB2Model model = importer.importFrom(HOST, 80);
 
       if (model == null) {
         if (expected == Result.PARSE_FAILED) return;
@@ -83,7 +89,7 @@ public class OpenHabImporterTest {
         }
       }
 
-      printAndCompare(config, model);
+      printAndCompare(config, model.getRoot());
     }
   }
 
diff --git a/eraser-base/src/test/java/de/tudresden/inf/st/eraser/ParserTests.java b/eraser-base/src/test/java/de/tudresden/inf/st/eraser/ParserTests.java
deleted file mode 100644
index 3ce0ecaf9e7f0469be91815ad4eb2ed57ed9ef2c..0000000000000000000000000000000000000000
--- a/eraser-base/src/test/java/de/tudresden/inf/st/eraser/ParserTests.java
+++ /dev/null
@@ -1,104 +0,0 @@
-package de.tudresden.inf.st.eraser;
-
-import de.tudresden.inf.st.eraser.TestUtils.ModelAndItem;
-import de.tudresden.inf.st.eraser.jastadd.model.MqttRoot;
-import de.tudresden.inf.st.eraser.jastadd.model.NumberItem;
-import de.tudresden.inf.st.eraser.jastadd.model.Root;
-import de.tudresden.inf.st.eraser.util.ParserUtils;
-import org.junit.Assert;
-import org.junit.Test;
-
-/**
- * Testing helper methods using in parsing.
- *
- * @author rschoene - Initial contribution
- */
-public class ParserTests {
-
-  @Test
-  public void testCreateMqttTopicSimple() {
-    ModelAndItem mai = TestUtils.createModelAndItem(1);
-
-    Root model = mai.model;
-    model.setMqttRoot(new MqttRoot());
-    NumberItem item = mai.item;
-
-    Assert.assertNull(item.getTopic());
-
-    String[] parts = { "one", "two", "three" };
-    String topicName = String.join("/", parts);
-
-    ParserUtils.createMqttTopic(item, topicName, model);
-
-    Assert.assertNotNull(item.getTopic());
-
-    MqttRoot mqttRoot = model.getMqttRoot();
-    Assert.assertEquals("There must be only one topic", 1, mqttRoot.getNumTopic());
-    Assert.assertEquals("First part is wrong",
-        parts[0], mqttRoot.getTopic(0).getPart());
-    Assert.assertEquals("First topic has wrong number of sub-topics",
-        1, mqttRoot.getTopic(0).getNumSubTopic());
-    Assert.assertEquals("Second part is wrong",
-        parts[1], mqttRoot.getTopic(0).getSubTopic(0).getPart());
-    Assert.assertEquals("Second topic has wrong number of sub-topics",
-        1, mqttRoot.getTopic(0).getSubTopic(0).getNumSubTopic());
-    Assert.assertEquals("Third part is wrong",
-        parts[2], mqttRoot.getTopic(0).getSubTopic(0).getSubTopic(0).getPart());
-    Assert.assertEquals("Third part is wrong object",
-        item.getTopic(), mqttRoot.getTopic(0).getSubTopic(0).getSubTopic(0));
-
-    Assert.assertEquals("Name does not match", topicName, item.getTopic().allParts());
-  }
-
-
-  @Test
-  public void testCreateMqttTopicTwoInterleavedTopics() {
-    ModelAndItem mai = TestUtils.createModelAndItem(1);
-
-    Root model = mai.model;
-    model.setMqttRoot(new MqttRoot());
-    NumberItem item1 = mai.item;
-    NumberItem item2 = TestUtils.addItemTo(model, 3);
-
-    Assert.assertNull(item1.getTopic());
-    Assert.assertNull(item2.getTopic());
-
-    String[] parts = { "one", "two", "three" };
-    String otherPart2 = "222";
-    String topicName1 = String.join("/", parts);
-    String topicName2 = String.join("/", parts[0], otherPart2, parts[2]);
-
-    ParserUtils.createMqttTopic(item1, topicName1, model);
-    ParserUtils.createMqttTopic(item2, topicName2, model);
-
-    Assert.assertNotNull(item1.getTopic());
-    Assert.assertNotNull(item2.getTopic());
-
-    MqttRoot mqttRoot = model.getMqttRoot();
-    Assert.assertEquals("There must be only one topic", 1, mqttRoot.getNumTopic());
-    Assert.assertEquals("First part is wrong",
-        parts[0], mqttRoot.getTopic(0).getPart());
-    Assert.assertEquals("First topic has wrong number of sub-topics",
-        2, mqttRoot.getTopic(0).getNumSubTopic());
-    Assert.assertEquals("Second part for first item is wrong",
-        parts[1], mqttRoot.getTopic(0).getSubTopic(0).getPart());
-    Assert.assertEquals("Second part for first item is wrong",
-        otherPart2, mqttRoot.getTopic(0).getSubTopic(1).getPart());
-    Assert.assertEquals("Second topic for first item has wrong number of sub-topics",
-        1, mqttRoot.getTopic(0).getSubTopic(0).getNumSubTopic());
-    Assert.assertEquals("Third part for first item is wrong",
-        parts[2], mqttRoot.getTopic(0).getSubTopic(0).getSubTopic(0).getPart());
-    Assert.assertEquals("Second topic for second item has wrong number of sub-topics",
-        1, mqttRoot.getTopic(0).getSubTopic(1).getNumSubTopic());
-    Assert.assertEquals("Third part for second item is wrong",
-        parts[2], mqttRoot.getTopic(0).getSubTopic(1).getSubTopic(0).getPart());
-    Assert.assertEquals("Third part for first item is wrong object",
-        item1.getTopic(), mqttRoot.getTopic(0).getSubTopic(0).getSubTopic(0));
-    Assert.assertEquals("Third part for second item is wrong object",
-        item2.getTopic(), mqttRoot.getTopic(0).getSubTopic(1).getSubTopic(0));
-
-    Assert.assertEquals("Name for first item does not match", topicName1, item1.getTopic().allParts());
-    Assert.assertEquals("Name for second item does not match", topicName2, item2.getTopic().allParts());
-  }
-
-}
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 54b4d64e3498005d57a9c7eea53274764de77e94..13f5a493652ee42cdc70fc31ae64649de4b23987 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
@@ -1,12 +1,22 @@
 package de.tudresden.inf.st.eraser;
 
+import beaver.Parser;
 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 org.junit.Assert;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
 
+import java.io.IOException;
 import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.stream.StreamSupport;
 
 /**
  * Testing the simple rule engine.
@@ -16,202 +26,707 @@ import java.util.Arrays;
 @RunWith(Parameterized.class)
 public class RulesTest {
 
-  class Counter implements ActionEditConsumer {
-    int value = 0;
+  private static final double DELTA = 0.01d;
+
+  class CountingAction extends NoopAction {
+    final Map<Item, AtomicInteger> counters = new HashMap<>();
+
+    CountingAction() {
+      reset();
+    }
+
+    private AtomicInteger getAtomic(Item item) {
+      return counters.computeIfAbsent(item, unused -> new AtomicInteger(0));
+    }
+
     @Override
-    public void accept(Root root) {
-      value += 1;
+    public void applyFor(Item item) {
+      getAtomic(item).addAndGet(1);
+    }
+
+    int get(Item item) {
+      return getAtomic(item).get();
+    }
+
+    void reset() {
+      counters.clear();
     }
   }
 
   @Test
-  public void testUnconditionalRule() {
+  public void testUnconditionalLambdaAction() {
     TestUtils.ModelAndItem modelAndItem = createModelAndItem(3);
-    Root model = modelAndItem.model;
+    Root root = modelAndItem.model.getRoot();
     NumberItem item = modelAndItem.item;
+
     Rule rule = new Rule();
-    Event event = new Event("testEvent");
-    Counter counter = new Counter();
-    rule.addEvent(event);
-    rule.setCondition(root -> true);
-    rule.setAction(counter);
-    model.addRule(rule);
-    event.activateFor(item);
+    CountingAction counter = new CountingAction();
+    rule.addAction(counter);
+    root.addRule(rule);
+    rule.activateFor(item);
 
-    Assert.assertEquals(m("Counter not initialized correctly"), 0, counter.value);
+    Assert.assertEquals(m("Counter not initialized correctly"), 0, counter.get(item));
 
-    setState(model, item, 5);
-    Assert.assertEquals(m("Change of item state should trigger the event"), 1, counter.value);
+    setState(item, 5);
+    Assert.assertEquals(m("Change of item state should trigger the rule"), 1, counter.get(item));
 
-    setState(model, item, 5);
-    Assert.assertEquals(m("Set state to same value should not trigger the event"), 1, counter.value);
+    setState(item, 5);
+    Assert.assertEquals(m("Set state to same value should not trigger the rule"), 1, counter.get(item));
 
-    setState(model, item, 3);
-    Assert.assertEquals(m("Change of item state should trigger the event"), 2, counter.value);
+    setState(item, 3);
+    Assert.assertEquals(m("Change of item state should trigger the rule"), 2, counter.get(item));
   }
 
   @Test
-  public void testOneRuleTwoEventsForOneItem() {
+  public void testIdempotentActivation() {
     TestUtils.ModelAndItem modelAndItem = createModelAndItem(4);
-    Root model = modelAndItem.model;
+    Root root = modelAndItem.model.getRoot();
     NumberItem item = modelAndItem.item;
 
     Rule rule = new Rule();
-    Counter counter = new Counter();
-    rule.setCondition(root -> true);
-    rule.setAction(counter);
-    model.addRule(rule);
+    root.addRule(rule);
 
-    Event event1 = new Event("testEvent1");
-    rule.addEvent(event1);
+    rule.activateFor(item);
+    rule.activateFor(item);
 
-    Event event2 = new Event("testEvent2");
-    rule.addEvent(event2);
+    Assert.assertEquals(m("Rule was not activated exactly once (observer count)"), 1, rule.getObserverList().size());
+    int ruleCount = 0;
+    for (ItemObserver observer : rule.getObserverList()) {
+      ruleCount += observer.getTriggeredRuleList().size();
+    }
+    Assert.assertEquals(m("Rule was not activated exactly once (rule count)"), 1, ruleCount);
+  }
+
+  @Test
+  public void testTwoRulesForOneItem() {
+    TestUtils.ModelAndItem modelAndItem = createModelAndItem(4);
+    Root root = modelAndItem.model.getRoot();
+    NumberItem item = modelAndItem.item;
 
-    event1.activateFor(item);
-    event2.activateFor(item);
+    CountingAction counter1 = new CountingAction();
+    Rule ruleA = new Rule();
+    ruleA.addAction(counter1);
+    root.addRule(ruleA);
 
-    Assert.assertEquals(m("First counter not initialized correctly"), 0, counter.value);
+    Rule ruleB = new Rule();
+    CountingAction counter2 = new CountingAction();
+    ruleB.addAction(counter2);
+    root.addRule(ruleB);
 
-    setState(model, item, 5);
-    Assert.assertEquals(m("Change of item state should trigger the rule twice"), 2, counter.value);
+    ruleA.activateFor(item);
+    ruleB.activateFor(item);
 
-    setState(model, item, 5);
-    Assert.assertEquals(m("Set state to same value should not trigger the rule"), 2, counter.value);
+    Assert.assertEquals(m("First counter not initialized correctly"), 0, counter1.get(item));
+    Assert.assertEquals(m("Second counter not initialized correctly"), 0, counter2.get(item));
 
-    setState(model, item, 3);
-    Assert.assertEquals(m("Change of item state should trigger the rule again twice"), 4, counter.value);
+    setState(item, 5);
+    Assert.assertEquals(m("Change of item state should trigger the first rule"), 1, counter1.get(item));
+    Assert.assertEquals(m("Change of item state should trigger the second rule"), 1, counter2.get(item));
+
+    setState(item, 5);
+    Assert.assertEquals(m("Set state to same value should not trigger the first rule"), 1, counter1.get(item));
+    Assert.assertEquals(m("Set state to same value should not trigger the second rule"), 1, counter2.get(item));
+
+    setState(item, 3);
+    Assert.assertEquals(m("Change of item state should trigger the first rule"), 2, counter1.get(item));
+    Assert.assertEquals(m("Change of item state should trigger the second rule"), 2, counter2.get(item));
   }
 
   @Test
-  public void testTwoRulesForOneItem() {
+  public void testOneRuleForTwoItems() {
+    TestUtils.ModelAndItem modelAndItem = createModelAndItem(4);
+    Root root = modelAndItem.model.getRoot();
+    NumberItem item1 = modelAndItem.item;
+    NumberItem item2 = TestUtils.addItemTo(root.getOpenHAB2Model(), 4, useUpdatingItem);
+
+    Rule rule = new Rule();
+    CountingAction counter = new CountingAction();
+    rule.addAction(counter);
+    root.addRule(rule);
+
+    rule.activateFor(item1);
+    rule.activateFor(item2);
+
+    // Items: 4, 4. Expected counters: 0, 0.
+    Assert.assertEquals(m("Counter not initialized correctly for first item"), 0, counter.get(item1));
+    Assert.assertEquals(m("Counter not initialized correctly for second item"), 0, counter.get(item2));
+
+    // Items: 5, 4. Expected counters: 1, 0.
+    setState(item1, 5);
+    Assert.assertEquals(m("Change of first item state should trigger the rule once"), 1, counter.get(item1));
+    Assert.assertEquals(m("Change of first item state should not trigger the rule for second item"), 0, counter.get(item2));
+
+    // Items: 5, 4. Expected counters: 1, 0.
+    setState(item1, 5);
+    Assert.assertEquals(m("Set state of first item to same value should not trigger the rule"),
+        1, counter.get(item1));
+    Assert.assertEquals(m("Change of first item state should not trigger the rule for second item"), 0, counter.get(item2));
+
+    // Items: 5, 3. Expected counters: 1, 1.
+    setState(item2, 3);
+    Assert.assertEquals(m("Change of second item state should trigger the rule"), 1, counter.get(item2));
+    Assert.assertEquals(m("Change of second item state should not trigger the rule for first item"), 1, counter.get(item1));
+
+    // Items: 5, 3. Expected counters: 1, 1.
+    setState(item1, 5);
+    Assert.assertEquals(m("Set state of first item to same value should still not trigger the rule"),
+        1, counter.get(item1));
+    Assert.assertEquals(m("Change of first item state should still not trigger the rule for second item"), 1, counter.get(item2));
+
+    // Items: 6, 3. Expected counters: 2, 1.
+    setState(item1, 6);
+    Assert.assertEquals(m("Set state of first item should trigger the rule"),
+        2, counter.get(item1));
+    Assert.assertEquals(m("Change of first item state should still not trigger the rule for second item"), 1, counter.get(item2));
+
+    // Items: 6, 3. Expected counters: 2, 1.
+    setState(item2, 3);
+    Assert.assertEquals(m("Set state of second item to same value should not trigger the rule"),
+        1, counter.get(item2));
+    Assert.assertEquals(m("Change of second item state should not trigger the rule for first item"), 2, counter.get(item1));
+
+    // Items: 6, 0. Expected counters: 2, 2.
+    setState(item2, 0);
+    Assert.assertEquals(m("Change of second item state should trigger the rule"), 2, counter.get(item2));
+    Assert.assertEquals(m("Change of second item state should not trigger the rule for first item"), 2, counter.get(item1));
+  }
+
+  @Test
+  public void testRemoveActivation() {
     TestUtils.ModelAndItem modelAndItem = createModelAndItem(4);
-    Root model = modelAndItem.model;
+    Root root = modelAndItem.model.getRoot();
     NumberItem item = modelAndItem.item;
 
-    Counter counter1 = new Counter();
-    Rule rule1 = new Rule();
-    rule1.setCondition(root -> true);
-    rule1.setAction(counter1);
-    model.addRule(rule1);
+    Rule rule = new Rule();
+    CountingAction counter = new CountingAction();
+    rule.addAction(counter);
+    root.addRule(rule);
+
+    rule.activateFor(item);
+
+    Assert.assertEquals(m("Counter not initialized correctly"), 0, counter.get(item));
 
-    Event event1 = new Event("testEvent1");
-    rule1.addEvent(event1);
+    setState(item, 5);
+    Assert.assertEquals(m("Change of item state should trigger the rule"), 1, counter.get(item));
 
-    Rule rule2 = new Rule();
-    Counter counter2 = new Counter();
-    rule2.setCondition(root -> true);
-    rule2.setAction(counter2);
-    model.addRule(rule2);
+    rule.removeActivationOf(item);
 
-    Event event2 = new Event("testEvent2");
-    rule2.addEvent(event2);
+    setState(item, 3);
+    Assert.assertEquals(m("Change of item state should not change the counter anymore"), 1, counter.get(item));
+  }
 
-    event1.activateFor(item);
-    event2.activateFor(item);
+  @Test
+  public void testNumberConditions() {
+    TestUtils.ModelAndItem modelAndItem = createModelAndItem(2);
+    Root root = modelAndItem.model.getRoot();
+    NumberItem item = modelAndItem.item;
 
-    Assert.assertEquals(m("First counter not initialized correctly"), 0, counter1.value);
-    Assert.assertEquals(m("Second counter not initialized correctly"), 0, counter2.value);
+    Rule rule = new Rule();
+    ItemStateNumberCheck check1 = new ItemStateNumberCheck(ComparatorType.GreaterOrEqualThan, 4);
+    ItemStateNumberCheck check2 = new ItemStateNumberCheck(ComparatorType.LessThan, 6);
+    rule.addCondition(new ItemStateCheckCondition(check1));
+    rule.addCondition(new ItemStateCheckCondition(check2));
+    CountingAction counter = new CountingAction();
+    rule.addAction(counter);
+    root.addRule(rule);
+    rule.activateFor(item);
 
-    setState(model, item, 5);
-    Assert.assertEquals(m("Change of item state should trigger the first event"), 1, counter1.value);
-    Assert.assertEquals(m("Change of item state should trigger the second event"), 1, counter2.value);
+    Assert.assertEquals(m("Counter not initialized correctly"), 0, counter.get(item));
 
-    setState(model, item, 5);
-    Assert.assertEquals(m("Set state to same value should not trigger the first event"), 1, counter1.value);
-    Assert.assertEquals(m("Set state to same value should not trigger the second event"), 1, counter2.value);
+    setState(item, 3);
+    Assert.assertEquals(m("Change of item to 3 should not trigger the rule, check1 violated"), 0, counter.get(item));
 
-    setState(model, item, 3);
-    Assert.assertEquals(m("Change of item state should trigger the first event"), 2, counter1.value);
-    Assert.assertEquals(m("Change of item state should trigger the second event"), 2, counter2.value);
+    setState(item, 4);
+    Assert.assertEquals(m("Change of item to 4 should trigger the rule"), 1, counter.get(item));
+
+    setState(item, 5);
+    Assert.assertEquals(m("Change of item to 5 should trigger the rule"), 2, counter.get(item));
+
+    setState(item, 6);
+    Assert.assertEquals(m("Change of item to 6 should not trigger the rule, check2 violated"), 2, counter.get(item));
+
+    setState(item, 7);
+    Assert.assertEquals(m("Change of item to 7 should not trigger the rule, check2 violated"), 2, counter.get(item));
   }
 
   @Test
-  public void testOneRuleOneEventForTwoItems() {
-    TestUtils.ModelAndItem modelAndItem = createModelAndItem(4);
-    Root model = modelAndItem.model;
-    NumberItem item1 = modelAndItem.item;
-    NumberItem item2 = TestUtils.addItemTo(model, 4, useUpdatingItem);
+  public void testTwoActions() {
+    TestUtils.ModelAndItem modelAndItem = createModelAndItem(2);
+    Root root = modelAndItem.model.getRoot();
+    NumberItem item = modelAndItem.item;
 
     Rule rule = new Rule();
-    Counter counter = new Counter();
-    rule.setCondition(root -> true);
-    rule.setAction(counter);
-    model.addRule(rule);
+    CountingAction counter1 = new CountingAction();
+    rule.addAction(counter1);
+    CountingAction counter2 = new CountingAction();
+    rule.addAction(counter2);
+    root.addRule(rule);
+    rule.activateFor(item);
+
+    Assert.assertEquals(m("First counter not initialized correctly"), 0, counter1.get(item));
+    Assert.assertEquals(m("Second counter not initialized correctly"), 0, counter2.get(item));
+
+    setState(item, 3);
+    Assert.assertEquals(m("Change of item state should trigger the rule for first counter"), 1, counter1.get(item));
+    Assert.assertEquals(m("Change of item state should trigger the rule for second counter"), 1, counter2.get(item));
+
+    setState(item, 3);
+    Assert.assertEquals(m("Change of item to same state should not trigger the rule for first counter"), 1, counter1.get(item));
+    Assert.assertEquals(m("Change of item to same state should not trigger the rule for second counter"), 1, counter2.get(item));
+  }
 
-    Event eventItem1 = new Event("testEventItem1");
-    rule.addEvent(eventItem1);
-    eventItem1.activateFor(item1);
+  @Test
+  public void testChainedRules() {
+    TestUtils.ModelAndItem modelAndItem = createModelAndItem(2);
+    Root root = modelAndItem.model.getRoot();
+    NumberItem item = modelAndItem.item;
+    NumberItem item2 = TestUtils.addItemTo(root.getOpenHAB2Model(), 4, useUpdatingItem);
+
+    Rule ruleA = new Rule();
+    CountingAction counter1 = new CountingAction();
+    ruleA.addAction(counter1);
+
+    Rule ruleB = new Rule();
+    CountingAction counter2 = new CountingAction();
+    ruleB.addAction(counter2);
+
+    ruleA.addAction(new TriggerRuleAction(ruleB));
+
+    root.addRule(ruleA);
+    root.addRule(ruleB);
+    ruleA.activateFor(item);
+    ruleB.activateFor(item2);
+
+    Assert.assertEquals(m("First counter not initialized correctly for first item"), 0, counter1.get(item));
+    Assert.assertEquals(m("First counter not initialized correctly for second item"), 0, counter1.get(item2));
+    Assert.assertEquals(m("Second counter not initialized correctly for first item"), 0, counter2.get(item));
+    Assert.assertEquals(m("Second counter not initialized correctly for second item"), 0, counter2.get(item2));
+
+    setState(item, 3);
+    Assert.assertEquals(m("Change of first item state should trigger the first rule"), 1, counter1.get(item));
+    Assert.assertEquals(m("Change of first item state should trigger the second rule"), 1, counter2.get(item));
+    Assert.assertEquals(m("Change of first item state should not trigger the second rule for second item"), 0, counter2.get(item2));
+
+    setState(item, 3);
+    Assert.assertEquals(m("Change of item to same state should not trigger the first rule"), 1, counter1.get(item));
+    Assert.assertEquals(m("Change of item to same state should not trigger the second rule"), 1, counter2.get(item));
+    Assert.assertEquals(m("Change of first item state should not trigger the second rule for second item"), 0, counter2.get(item2));
+
+    setState(item2, 7);
+    Assert.assertEquals(m("Change of second item state should not trigger the first rule"), 1, counter1.get(item));
+    Assert.assertEquals(m("Change of second item state should not trigger the second rule for the first item"), 1, counter2.get(item));
+    Assert.assertEquals(m("Change of second item state should trigger the second rule for the second item"), 1, counter2.get(item2));
+  }
 
-    Event eventItem2 = new Event("testEventItem2");
-    rule.addEvent(eventItem2);
-    eventItem2.activateFor(item2);
+  @Test
+  public void testSetStateFromConstantStringAction() {
+    TestUtils.ModelAndItem modelAndItem = createModelAndItem(3);
+    Root root = modelAndItem.model.getRoot();
+    NumberItem item = modelAndItem.item;
+    NumberItem item2 = TestUtils.addItemTo(root.getOpenHAB2Model(), 4, useUpdatingItem);
 
-    Assert.assertEquals(m("Counter not initialized correctly"), 0, counter.value);
+    Rule rule = new Rule();
+    rule.addAction(new SetStateFromConstantStringAction(item2, "5"));
+    CountingAction counter = new CountingAction();
+    rule.addAction(counter);
+    root.addRule(rule);
+    rule.activateFor(item);
+
+    Assert.assertEquals(m("Affected item not initialized correctly"),
+        4, item2.asItemWithDoubleState().getState(), DELTA);
+
+    setState(item, 25);
+    Assert.assertEquals(m("Change of item state should trigger the rule"), 1, counter.get(item));
+    Assert.assertEquals(m("Change of item state should set the state of the affected item"),
+        5, item2.asItemWithDoubleState().getState(), DELTA);
+  }
 
-    setState(model, item1, 5);
-    Assert.assertEquals(m("Change of first item state should trigger the event once"), 1, counter.value);
+  class ValuedStateProvider implements NewStateProvider {
+    int value;
+    @Override
+    public String get() {
+      return Integer.toString(value);
+    }
+  }
 
-    setState(model, item1, 5);
-    Assert.assertEquals(m("Set state of first item to same value should not trigger the event"),
-        1, counter.value);
+  @Test
+  public void testSetStateFromLambdaAction() {
+    ValuedStateProvider provider = new ValuedStateProvider();
+    TestUtils.ModelAndItem modelAndItem = createModelAndItem(0);
+    Root root = modelAndItem.model.getRoot();
+    NumberItem item = modelAndItem.item;
+    NumberItem item2 = TestUtils.addItemTo(root.getOpenHAB2Model(), 3, useUpdatingItem);
 
-    setState(model, item2, 3);
-    Assert.assertEquals(m("Change of second item state should trigger the event"), 2, counter.value);
+    Rule rule = new Rule();
+    rule.addAction(new SetStateFromLambdaAction(item2, provider));
+    CountingAction counter = new CountingAction();
+    rule.addAction(counter);
+    root.addRule(rule);
+    rule.activateFor(item);
+
+    Assert.assertEquals(m("Affected item not initialized correctly"),
+        3, item2.asItemWithDoubleState().getState(), DELTA);
+
+    provider.value = 4;
+    setState(item, 1);
+    Assert.assertEquals(m("Change of item state should trigger the rule"), 1, counter.get(item));
+    Assert.assertEquals(m("Change of item state should set the state of the affected item"),
+        4, item2.asItemWithDoubleState().getState(), DELTA);
+
+    provider.value = 4;
+    setState(item, 2);
+    Assert.assertEquals(m("Change of item state should trigger the rule"), 2, counter.get(item));
+    Assert.assertEquals(m("Change of item state should set the state of the affected item"),
+        4, item2.asItemWithDoubleState().getState(), DELTA);
+
+    provider.value = 5;
+    setState(item, 2);
+    Assert.assertEquals(m("Change of item to same state should not trigger the rule"), 2, counter.get(item));
+    Assert.assertEquals(m("Change of item to same state should not set the state of the affected item"),
+        4, item2.asItemWithDoubleState().getState(), DELTA);
+
+    provider.value = 5;
+    setState(item, 3);
+    Assert.assertEquals(m("Change of item state should trigger the rule"), 3, counter.get(item));
+    Assert.assertEquals(m("Change of item state should set the state of the affected item"),
+        5, item2.asItemWithDoubleState().getState(), DELTA);
+  }
 
-    setState(model, item1, 5);
-    Assert.assertEquals(m("Set state of first item to same value should still not trigger the event"),
-        2, counter.value);
+  @Test
+  public void testSetStateFromTriggeringItemAction() {
+    TestUtils.ModelAndItem modelAndItem = createModelAndItem(3);
+    Root root = modelAndItem.model.getRoot();
+    NumberItem item = modelAndItem.item;
+    StringItem item2 = addStringItem(root.getOpenHAB2Model(), "0");
 
-    setState(model, item1, 6);
-    Assert.assertEquals(m("Set state of first item should trigger the event"),
-        3, counter.value);
+    Rule rule = new Rule();
+    CountingAction counter = new CountingAction();
+    rule.addAction(new SetStateFromTriggeringItemAction(item2));
+    rule.addAction(counter);
+    root.addRule(rule);
+    rule.activateFor(item);
+
+    Assert.assertEquals(m("Counter not initialized correctly"), 0, counter.get(item));
+    Assert.assertEquals(m("Affected item not initialized correctly"), "0", item2.getState());
+
+    setState(item, 5);
+    Assert.assertEquals(m("Change of item state should trigger the rule"), 1, counter.get(item));
+    Assert.assertEquals(m("Change of item state should set the state of the affected item"), "5.0", item2.getState());
+
+    setState(item, 5);
+    Assert.assertEquals(m("Change of item state should not trigger the rule"), 1, counter.get(item));
+    Assert.assertEquals(m("Change of item state should not set the state of the affected item"), "5.0", item2.getState());
+
+    setState(item, 7);
+    Assert.assertEquals(m("Change of item state should trigger the rule"), 2, counter.get(item));
+    Assert.assertEquals(m("Change of item state should set the state of the affected item"), "7.0", item2.getState());
+  }
 
-    setState(model, item2, 3);
-    Assert.assertEquals(m("Set state of second item to same value should not trigger the event"),
-        3, counter.value);
+  @Test
+  public void testSetStateFromItemsAction() {
+    TestUtils.ModelAndItem modelAndItem = createModelAndItem(3);
+    Root root = modelAndItem.model.getRoot();
+    NumberItem item = modelAndItem.item;
+    NumberItem item2 = TestUtils.addItemTo(root.getOpenHAB2Model(), 4, useUpdatingItem);
+    StringItem affectedItem = addStringItem(root.getOpenHAB2Model(), "1");
+
+    Rule rule = new Rule();
+    SetStateFromItemsAction action = new SetStateFromItemsAction(items ->
+        Long.toString(StreamSupport.stream(items.spliterator(), false)
+            .mapToLong(inner -> (long) inner.asItemWithDoubleState().getState())
+            .sum()));
+    action.addSourceItem(item);
+    action.addSourceItem(item2);
+    action.setAffectedItem(affectedItem);
+    rule.addAction(action);
+    CountingAction counter = new CountingAction();
+    rule.addAction(counter);
+    root.addRule(rule);
+    rule.activateFor(item);
+
+    Assert.assertEquals(m("Counter not initialized correctly"), 0, counter.get(item));
+    Assert.assertEquals(m("Affected item not initialized correctly"), "1",
+        affectedItem.getState());
+
+    // 5 + 4 = 9
+    setState(item, 5);
+    Assert.assertEquals(m("Change of item state should trigger the rule"), 1, counter.get(item));
+    Assert.assertEquals(m("Change of item state should set the state of the affected item"),
+        "9", affectedItem.getState());
+
+    // still 5 + 4 = 9, as rule does not trigger for item2
+    setState(item2, 5);
+    Assert.assertEquals(m("Change of item2 state should not trigger the rule"), 1, counter.get(item));
+    Assert.assertEquals(m("Change of item2 state should not set the state of the affected item"),
+        "9", affectedItem.getState());
+
+    // still 5 + 4 = 9, as rule should not trigger
+    setState(item, 5);
+    Assert.assertEquals(m("Change of item to same state should not trigger the rule"), 1, counter.get(item));
+    Assert.assertEquals(m("Change of item to same state should not set the state of the affected item"),
+        "9", affectedItem.getState());
+
+    // 7 + 5 = 12
+    setState(item, 7);
+    Assert.assertEquals(m("Change of item state should trigger the rule"), 2, counter.get(item));
+    Assert.assertEquals(m("Change of item state should set the state of the affected item"),
+        "12", affectedItem.getState());
+
+    // add new item to sum
+    NumberItem item3 = TestUtils.addItemTo(root.getOpenHAB2Model(), -4, useUpdatingItem);
+    action.addSourceItem(item3);
+
+    // still 7 + 5 = 12, as rule should not trigger
+    setState(item, 7);
+    Assert.assertEquals(m("Change of item to same state should not trigger the rule"), 2, counter.get(item));
+    Assert.assertEquals(m("Change of item to same state should not set the state of the affected item"),
+        "12", affectedItem.getState());
+
+    // 8 + 5 - 4 = 9
+    setState(item, 8);
+    Assert.assertEquals(m("Change of item state should trigger the rule"), 3, counter.get(item));
+    Assert.assertEquals(m("Change of item state should set the state of the affected item"),
+        "9", affectedItem.getState());
   }
 
   @Test
-  public void testEventRemoveSelf() {
-    TestUtils.ModelAndItem modelAndItem = createModelAndItem(4);
-    Root model = modelAndItem.model;
+  public void testAddDoubleToStateAction() {
+    TestUtils.ModelAndItem modelAndItem = createModelAndItem(3);
+    Root root = modelAndItem.model.getRoot();
     NumberItem item = modelAndItem.item;
+    NumberItem affectedItem = TestUtils.addItemTo(root.getOpenHAB2Model(), 4, useUpdatingItem);
 
     Rule rule = new Rule();
-    Counter counter = new Counter();
-    rule.setCondition(root -> true);
-    rule.setAction(counter);
-    model.addRule(rule);
+    rule.addAction(new AddDoubleToStateAction(affectedItem, 2));
+    CountingAction counter = new CountingAction();
+    rule.addAction(counter);
+    root.addRule(rule);
+    rule.activateFor(item);
+
+    Assert.assertEquals(m("Counter not initialized correctly"), 0, counter.get(item));
+    Assert.assertEquals(m("Affected item not initialized correctly"),
+        4, affectedItem.getState(), DELTA);
+
+    // 4 + 2 = 6
+    setState(item, 5);
+    Assert.assertEquals(m("Change of item state should trigger the rule"), 1, counter.get(item));
+    Assert.assertEquals(m("Change of item state should set the state of the affected item"),
+        6, affectedItem.getState(), DELTA);
+
+    // still 6
+    setState(item, 5);
+    Assert.assertEquals(m("Change of item to same state should not trigger the rule"), 1, counter.get(item));
+    Assert.assertEquals(m("Change of item to same state should not set the state of the affected item"),
+        6, affectedItem.getState(), DELTA);
+
+    // 6 + 2 = 8
+    setState(item, -2);
+    Assert.assertEquals(m("Change of item state should trigger the rule"), 2, counter.get(item));
+    Assert.assertEquals(m("Change of item state should set the state of the affected item"),
+        8, affectedItem.getState(), DELTA);
+  }
 
-    Event event = new Event("testEvent1");
-    rule.addEvent(event);
+  @Test
+  public void testMultiplyDoubleToStateAction() {
+    TestUtils.ModelAndItem modelAndItem = createModelAndItem(3);
+    Root root = modelAndItem.model.getRoot();
+    NumberItem item = modelAndItem.item;
+    NumberItem affectedItem = TestUtils.addItemTo(root.getOpenHAB2Model(), 4, useUpdatingItem);
 
-    event.activateFor(item);
+    Rule rule = new Rule();
+    rule.addAction(new MultiplyDoubleToStateAction(affectedItem, 2));
+    CountingAction counter = new CountingAction();
+    rule.addAction(counter);
+    root.addRule(rule);
+    rule.activateFor(item);
+
+    Assert.assertEquals(m("Counter not initialized correctly"), 0, counter.get(item));
+    Assert.assertEquals(m("Affected item not initialized correctly"),
+        4, affectedItem.getState(), DELTA);
+
+    // 4 * 2 = 8
+    setState(item, 5);
+    Assert.assertEquals(m("Change of item state should trigger the rule"), 1, counter.get(item));
+    Assert.assertEquals(m("Change of item state should set the state of the affected item"),
+        8, affectedItem.getState(), DELTA);
+
+    // still 8
+    setState(item, 5);
+    Assert.assertEquals(m("Change of item to same state should not trigger the rule"), 1, counter.get(item));
+    Assert.assertEquals(m("Change of item to same state should not set the state of the affected item"),
+        8, affectedItem.getState(), DELTA);
+
+    // 8 * 2 = 16
+    setState(item, 0);
+    Assert.assertEquals(m("Change of item state should trigger the rule"), 2, counter.get(item));
+    Assert.assertEquals(m("Change of item state should set the state of the affected item"),
+        16, affectedItem.getState(), DELTA);
+  }
+
+  @Test
+  public void testChainAddMultiplyActions() {
+    TestUtils.ModelAndItem modelAndItem = createModelAndItem(3);
+    Root root = modelAndItem.model.getRoot();
+    NumberItem item1 = modelAndItem.item;
+    NumberItem item2 = TestUtils.addItemTo(root.getOpenHAB2Model(), 4, useUpdatingItem);
+    NumberItem affectedItem = TestUtils.addItemTo(root.getOpenHAB2Model(), 5, useUpdatingItem);
+
+    Rule ruleA = new Rule();
+    ruleA.addAction(new AddDoubleToStateAction(affectedItem, 2));
+    CountingAction counterA = new CountingAction();
+    ruleA.addAction(counterA);
+
+    Rule ruleB = new Rule();
+    ruleB.addAction(new MultiplyDoubleToStateAction(affectedItem, 3));
+    CountingAction counterB = new CountingAction();
+    ruleB.addAction(counterB);
+
+    ruleA.addAction(new TriggerRuleAction(ruleB));
+
+    root.addRule(ruleA);
+    root.addRule(ruleB);
+    ruleA.activateFor(item1);
+    ruleB.activateFor(item2);
+
+    Assert.assertEquals(m("CounterA not initialized correctly for first item"), 0, counterA.get(item1));
+    Assert.assertEquals(m("CounterA not initialized correctly for second item"), 0, counterA.get(item2));
+    Assert.assertEquals(m("CounterB not initialized correctly for first item"), 0, counterB.get(item1));
+    Assert.assertEquals(m("CounterB not initialized correctly for second item"), 0, counterB.get(item2));
+    Assert.assertEquals(m("Affected item not initialized correctly"),
+        5, affectedItem.getState(), DELTA);
+
+    // First, 5 + 2 = 7. Then, 7 * 3 = 21
+    setState(item1, 5);
+    Assert.assertEquals(m("Change of item state should trigger the ruleA"), 1, counterA.get(item1));
+    Assert.assertEquals(m("Change of item state should also trigger the ruleB"), 1, counterB.get(item1));
+    Assert.assertEquals(m("Change of item state should set the state of the affected item"),
+        21, affectedItem.getState(), DELTA);
+
+    // still 21
+    setState(item1, 5);
+    Assert.assertEquals(m("Change of item to same state should not trigger the ruleA"), 1, counterA.get(item1));
+    Assert.assertEquals(m("Change of item to same state should not trigger the ruleB"), 1, counterB.get(item1));
+    Assert.assertEquals(m("Change of item to same state should not set the state of the affected item"),
+        21, affectedItem.getState(), DELTA);
+
+    // Only, 21 * 3 = 63
+    setState(item2, 1);
+    Assert.assertEquals(m("Change of second item state should not trigger the ruleA"), 1, counterA.get(item1));
+    Assert.assertEquals(m("Change of second item state should not trigger the ruleB for first item"), 1, counterB.get(item1));
+    Assert.assertEquals(m("Change of second item state should trigger the ruleB for second item"), 1, counterB.get(item2));
+    Assert.assertEquals(m("Change of second item state should set the state of the affected item"),
+        63, affectedItem.getState(), DELTA);
+  }
 
-    Assert.assertEquals(m("First counter not initialized correctly"), 0, counter.value);
+  @Test
+  public void testSetFromExpression() throws IOException, Parser.Exception {
+    TestUtils.ModelAndItem modelAndItem = createModelAndItem(3);
+    Root root = modelAndItem.model.getRoot();
+    NumberItem item1 = modelAndItem.item;
+    NumberItem item2 = TestUtils.addItemTo(root.getOpenHAB2Model(), 4, useUpdatingItem);
+    NumberItem affectedItem = TestUtils.addItemTo(root.getOpenHAB2Model(), 5, useUpdatingItem);
 
-    setState(model, item, 5);
-    Assert.assertEquals(m("Change of item state should trigger the event"), 1, counter.value);
+    Rule rule = new Rule();
+    SetStateFromExpression action = new SetStateFromExpression();
+    // TODO item1 should be referred to as triggering item
+    action.setNumberExpression(ParserUtils.parseNumberExpression("(" + item1.getID() + " + " + item2.getID() + ")", root));
+    action.setAffectedItem(affectedItem);
+    rule.addAction(action);
+    CountingAction counter = new CountingAction();
+    rule.addAction(counter);
+
+    root.addRule(rule);
+    rule.activateFor(item1);
+
+    Assert.assertEquals(m("Counter not initialized correctly"), 0, counter.get(item1));
+    Assert.assertEquals(m("Second item not initialized correctly"),
+        4, item2.getState(), DELTA);
+    Assert.assertEquals(m("Affected item not initialized correctly"),
+        5, affectedItem.getState(), DELTA);
+
+    // 5 + 4 = 9
+    setState(item1, 5);
+    Assert.assertEquals(m("Change of item state should trigger the rule"), 1, counter.get(item1));
+    Assert.assertEquals(m("Change of item state should set the state of the affected item"),
+        9, affectedItem.getState(), DELTA);
+
+    // still 9
+    setState(item1, 5);
+    Assert.assertEquals(m("Change of item to same state should not trigger the rule"), 1, counter.get(item1));
+    Assert.assertEquals(m("Change of item to same state should not set the state of the affected item"),
+        9, affectedItem.getState(), DELTA);
+
+    // still 9 (changes of item2 do not trigger the rule)
+    setState(item2, 1);
+    Assert.assertEquals(m("Change of second item to same state should not trigger the rule"), 1, counter.get(item1));
+    Assert.assertEquals(m("Change of second item to same state should not set the state of the affected item"),
+        9, affectedItem.getState(), DELTA);
+
+    // 0 + 1 = 1
+    setState(item1, 0);
+    Assert.assertEquals(m("Change of item state should trigger the rule"), 2, counter.get(item1));
+    Assert.assertEquals(m("Change of item state should set the state of the affected item"),
+        1, affectedItem.getState(), DELTA);
+  }
 
-    event.removeSelf();
+  @Test
+  public void testCronJobRule() {
+    Rule rule = new Rule();
+    CountingAction counter = new CountingAction();
+    rule.addAction(counter);
+
+    Assert.assertEquals(m("Counter not initialized correctly"), 0, counter.get(null));
+
+    ScheduledFuture f = rule.activateEvery(50, TimeUnit.MILLISECONDS);
+    waitMillis(160);
+    Assert.assertTrue("Rule cron job could not be cancelled", f.cancel(true));
+    // ----------------------- 160ms -----------------------
+    // + -- 50ms -- + -- 50ms -- + -- 50 ms -- + -- 10 ms --
+    // 4 times executed (+), with no initial delay
+    Assert.assertEquals(m("Rule was not executed four times"), 4, counter.get(null));
+
+    counter.reset();
+    Assert.assertEquals(m("Counter not reset correctly"), 0, counter.get(null));
+
+    f = rule.activateEvery(80, 50, TimeUnit.MILLISECONDS);
+    waitMillis(150);
+    Assert.assertTrue("Rule cron job could not be cancelled", f.cancel(true));
+    // ------------------ 150ms ------------------
+    // ----- 80ms ----- + -- 50ms -- + -- 20 ms --
+    // 2 times executed (+), with given delay
+    Assert.assertEquals(m("Rule was not executed two times"), 2, counter.get(null));
+  }
 
-    setState(model, item, 3);
-    Assert.assertEquals(m("Change of item state should not change the counter anymore"), 1, counter.value);
+  private static void waitMillis(int millis) {
+    try {
+      Thread.sleep(millis);
+    } catch (InterruptedException e) {
+      e.printStackTrace();
+      Assert.fail("Sleeping was interrupted!");
+    }
   }
 
   private String m(String message) {
     return message + " (Using " + name + ")";
   }
 
+  private StringItem addStringItem(OpenHAB2Model model, String initialValue) {
+    StringItem item = new StringItem();
+    Group group = TestUtils.getDefaultGroup(model);
+    item.setID("item" + group.getNumItem());
+    item.setState(initialValue, false);
+    group.addItem(item);
+    return item;
+  }
+
   private TestUtils.ModelAndItem createModelAndItem(long initialValue) {
     return TestUtils.createModelAndItem(initialValue, useUpdatingItem);
   }
 
-  private void setState(Root model, NumberItem item, long newState) {
+  private void setState(NumberItem item, long newState) {
     item.setState(newState);
-    if (!useUpdatingItem) {
-      model.applyRules();
+    if (!useUpdatingItem && item.hasItemObserver()) {
+      item.getItemObserver().apply();
     }
   }
 
diff --git a/eraser-base/src/test/java/de/tudresden/inf/st/eraser/TestUtils.java b/eraser-base/src/test/java/de/tudresden/inf/st/eraser/TestUtils.java
deleted file mode 100644
index a65b78cb62f2c8c8d27c4d2cc5054c572faa677c..0000000000000000000000000000000000000000
--- a/eraser-base/src/test/java/de/tudresden/inf/st/eraser/TestUtils.java
+++ /dev/null
@@ -1,62 +0,0 @@
-package de.tudresden.inf.st.eraser;
-
-import de.tudresden.inf.st.eraser.jastadd.model.*;
-
-/**
- * Helper class to create models used in tests.
- *
- * @author rschoene - Initial contribution
- */
-class TestUtils {
-
-  static class ModelAndItem {
-    Root model;
-    NumberItem item;
-    static ModelAndItem of(Root model, NumberItem item) {
-      ModelAndItem result = new ModelAndItem();
-      result.model = model;
-      result.item = item;
-      return result;
-    }
-  }
-
-  static ModelAndItem createModelAndItem(double initialValue) {
-    return createModelAndItem(initialValue, false);
-  }
-
-  static ModelAndItem createModelAndItem(double initialValue, boolean useUpdatingItem) {
-    Root model = Root.createEmptyRoot();
-    Group g = new Group();
-    model.addGroup(g);
-    g.setID("group1");
-
-    NumberItem item = addItemTo(g, initialValue, useUpdatingItem);
-
-    return ModelAndItem.of(model, item);
-  }
-
-  static NumberItem addItemTo(Root model, double initialValue) {
-    return addItemTo(model, initialValue, false);
-  }
-
-  static NumberItem addItemTo(Group group, double initialValue) {
-    return addItemTo(group, initialValue, false);
-  }
-
-  static NumberItem addItemTo(Root model, double initialValue, boolean useUpdatingItem) {
-    // use first found group
-    Group group = model.getGroup(0);
-    return addItemTo(group, initialValue, useUpdatingItem);
-  }
-
-  private static NumberItem addItemTo(Group group, double initialValue, boolean useUpdatingItem) {
-    NumberItem item = new NumberItem();
-    item.setDefaultShouldSendState(useUpdatingItem);
-    group.addItem(item);
-    item.setID("item" + group.getNumItem());
-    item.setState(initialValue, false);
-    return item;
-  }
-
-
-}
diff --git a/eraser-base/src/test/java/de/tudresden/inf/st/eraser/jastadd_test/core/TestRunner.java b/eraser-base/src/test/java/de/tudresden/inf/st/eraser/jastadd_test/core/TestRunner.java
index 984fb21d95d17d02b3ef7c07d064c410e300afb2..6978df5cafeee586a7025fc1855d88b1b240a49b 100644
--- a/eraser-base/src/test/java/de/tudresden/inf/st/eraser/jastadd_test/core/TestRunner.java
+++ b/eraser-base/src/test/java/de/tudresden/inf/st/eraser/jastadd_test/core/TestRunner.java
@@ -30,6 +30,7 @@
 package de.tudresden.inf.st.eraser.jastadd_test.core;
 
 import beaver.Parser;
+import de.tudresden.inf.st.eraser.jastadd.model.OpenHAB2Model;
 import de.tudresden.inf.st.eraser.jastadd.model.Root;
 import de.tudresden.inf.st.eraser.util.ParserUtils;
 
@@ -67,9 +68,9 @@ public class TestRunner {
     Result expected = config.expected;
 
     // Parse input model
-    Root model;
+    Root root;
     try {
-      model = parseModel(config);
+      root = parseModel(config);
     } catch (Parser.Exception e) {
       if (expected == Result.PARSE_FAILED) return;
       // otherwise rethrow error
@@ -80,7 +81,7 @@ public class TestRunner {
       fail("Parsing the model should have failed, but was successful!");
     }
 
-    if (model == null) {
+    if (root == null) {
       fail("Parsing the model should have passed, but model was null!");
     }
 
@@ -91,15 +92,15 @@ public class TestRunner {
       return;
     }
 
-    printAndCompare(config, model);
+    printAndCompare(config, root);
   }
 
-  protected static void printAndCompare(TestConfiguration config, Root model) {
+  protected static void printAndCompare(TestConfiguration config, Root root) {
     Result expected = config.expected;
     // Print model.
     String output;
     try {
-      output = printModel(model, config);
+      output = printModel(root, config);
     } catch (Exception e) {
       if (expected == Result.PRINT_FAILED) return;
         // otherwise rethrow error
@@ -149,8 +150,8 @@ public class TestRunner {
     return new File(testDir, "jastadd.err.expected");
   }
 
-  private static String printModel(Root model, TestConfiguration config) {
-    return model.prettyPrint();
+  private static String printModel(Root root, TestConfiguration config) {
+    return root.prettyPrint();
   }
 
   /**
diff --git a/eraser-base/src/test/resources/log4j2-test.xml b/eraser-base/src/test/resources/log4j2-test.xml
index 5c534092d64e9c1834c2ba20208c057e2b56be16..b20ed4ecc82af01ce5278a8d9f0c1aa77a32f00b 100644
--- a/eraser-base/src/test/resources/log4j2-test.xml
+++ b/eraser-base/src/test/resources/log4j2-test.xml
@@ -4,8 +4,8 @@
         <Console name="Console">
             <PatternLayout pattern="%highlight{%d{HH:mm:ss.SSS} %-5level} %c{1.} - %msg%n"/>
         </Console>
-        <RollingFile name="RollingFile" fileName="logs/jastadd-mquat.log"
-                    filePattern="logs/jastadd-mquat-%i.log">
+        <RollingFile name="RollingFile" fileName="logs/eraser-test.log"
+                    filePattern="logs/eraser-test-%i.log">
             <PatternLayout pattern="%d{HH:mm:ss.SSS} %-5level %logger{36} - %msg%n"/>
             <Policies>
                 <OnStartupTriggeringPolicy/>
@@ -21,5 +21,14 @@
         <Logger name="de.tudresden.inf.st.eraser.openhab2.mqtt" level="DEBUG" additivity="false">
             <Appender-ref ref="Console"/>
         </Logger>
+        <!-- Testcontainers reduce noise-->
+        <Logger name="org.testcontainers" level="INFO" additivity="false">
+            <Appender-ref ref="Console"/>
+            <AppenderRef ref="RollingFile"/>
+        </Logger>
+        <Logger name="com.github.dockerjava.core" level="INFO" additivity="false">
+            <Appender-ref ref="Console"/>
+            <AppenderRef ref="RollingFile"/>
+        </Logger>
     </Loggers>
 </Configuration>
diff --git a/eraser-base/src/test/resources/openhabtest/empty-list.json b/eraser-base/src/test/resources/openhabtest/empty-list.json
new file mode 100644
index 0000000000000000000000000000000000000000..fe51488c7066f6687ef680d6bfaa4f7768ef205c
--- /dev/null
+++ b/eraser-base/src/test/resources/openhabtest/empty-list.json
@@ -0,0 +1 @@
+[]
diff --git a/eraser-base/src/test/resources/openhabtest/empty.json b/eraser-base/src/test/resources/openhabtest/empty.json
index fe51488c7066f6687ef680d6bfaa4f7768ef205c..0967ef424bce6791893e9a57bb952f80fd536e93 100644
--- a/eraser-base/src/test/resources/openhabtest/empty.json
+++ b/eraser-base/src/test/resources/openhabtest/empty.json
@@ -1 +1 @@
-[]
+{}
diff --git a/eraser-base/src/test/resources/openhabtest/oh1/output.eraser b/eraser-base/src/test/resources/openhabtest/oh1/output.eraser
index 47453bd59614c3e358bc7996a4e2c4615bfffaf5..ac1a7993a8817a10c197fb73dd844bc2d750753f 100644
--- a/eraser-base/src/test/resources/openhabtest/oh1/output.eraser
+++ b/eraser-base/src/test/resources/openhabtest/oh1/output.eraser
@@ -1,5 +1,5 @@
 Color Item: id="Go1_item" label="Go 1" state="0,0,0" category="Lighting" ;
-Switch Item: id="Rule_Switch" state="false" ;
+Switch Item: id="Rule_Switch" state="OFF" ;
 Number Item: id="Color_Manual_Slider" state="0.0" ;
 Number Item: id="watch_acceleration_x" label="Watch Acceleration X" state="0.0" ;
 Number Item: id="watch_acceleration_y" label="Watch Acceleration Y" state="0.0" ;
@@ -16,7 +16,5 @@ Number Item: id="polar_brightness" label="Polar Brightness" state="0.0" category
 Number Item: id="moto_360_brightness" label="Moto 360 Brightness" state="0.0" category="Brightness" ;
 Color Item: id="wohnzimmer_item" label="Wohnzimmer" state="0,0,0" category="Lighting" ;
 Color Item: id="iris1_item" label="Iris 1" state="226,100,98" category="Lighting" ;
-Group: id="all_dimmable_lamps" label="All dimmable lamps" aggregation="AVG" items=["Go1_item"] ;
+Group: id="all_dimmable_lamps" label="All dimmable lamps" items=["Go1_item"] aggregation="AVG" ;
 Group: id="Unknown" items=["Rule_Switch", "Color_Manual_Slider", "watch_acceleration_x", "watch_acceleration_y", "watch_acceleration_z", "watch_rotation_x", "watch_rotation_y", "watch_rotation_z", "phone_rotation_x", "phone_rotation_y", "phone_rotation_z", "samsung_brightness", "skywriter_flick_item", "polar_brightness", "moto_360_brightness", "wohnzimmer_item", "iris1_item"] ;
-Mqtt: incoming="" outgoing="" host="localhost" ;
-Influx: host="localhost" ;
diff --git a/eraser-base/src/test/resources/openhabtest/oh2/items.json b/eraser-base/src/test/resources/openhabtest/oh2/items.json
index f57d140d070ef2caf360861b277a673d61a7b4e8..a26dfd88f1e76aeec2b34bd5e384c6b7ca59bd91 100644
--- a/eraser-base/src/test/resources/openhabtest/oh2/items.json
+++ b/eraser-base/src/test/resources/openhabtest/oh2/items.json
@@ -38,6 +38,15 @@
   },
   {
     "link": "http://localhost:48080/rest/items/iris1_item",
+    "metadata": {
+      "fuenf": {
+        "value": "value1",
+        "config": {
+          "key2": "value2",
+          "key3": "value3"
+        }
+      }
+    },
     "state": "253,0,0",
     "editable": true,
     "type": "Color",
diff --git a/eraser-base/src/test/resources/openhabtest/oh2/links.json b/eraser-base/src/test/resources/openhabtest/oh2/links.json
index 79e79442806b87513d8581eb877453873069ddbe..f881db063fd53906ab14e3fda79dbf4a5fac0d4b 100644
--- a/eraser-base/src/test/resources/openhabtest/oh2/links.json
+++ b/eraser-base/src/test/resources/openhabtest/oh2/links.json
@@ -142,7 +142,7 @@
   {
     "channelUID": "openlicht:polar-m600:342dfc32:rotation-y",
     "configuration": {},
-    "itemName": "watch_acceleration_y"
+    "itemName": "watch_rotation_y"
   },
   {
     "channelUID": "openlicht:polar-m600:342dfc32:rotation-z",
diff --git a/eraser-base/src/test/resources/openhabtest/oh2/output.eraser b/eraser-base/src/test/resources/openhabtest/oh2/output.eraser
index 155ab239c81206be07a20e0c861e3522aadd92ba..391d857ff48d13a04439dc4f5f16ba8cfc83b5a6 100644
--- a/eraser-base/src/test/resources/openhabtest/oh2/output.eraser
+++ b/eraser-base/src/test/resources/openhabtest/oh2/output.eraser
@@ -4,15 +4,15 @@ Thing: id="hue:0210:0017880adcf4:1" label="Wohnzimmer" type="hue:0210" channels=
 Thing: id="hue:0210:0017880adcf4:4" label="Hue go 1" type="hue:0210" channels=["hue:0210:0017880adcf4:4:color", "hue:0210:0017880adcf4:4:color_temperature", "hue:0210:0017880adcf4:4:alert", "hue:0210:0017880adcf4:4:effect"] ;
 Thing: id="openlicht:polar-m600:342dfc32" label="Polar M600" type="openlicht:polar-m600" channels=["openlicht:polar-m600:342dfc32:acceleration-x", "openlicht:polar-m600:342dfc32:acceleration-y", "openlicht:polar-m600:342dfc32:acceleration-z", "openlicht:polar-m600:342dfc32:rotation-x", "openlicht:polar-m600:342dfc32:rotation-y", "openlicht:polar-m600:342dfc32:rotation-z", "openlicht:polar-m600:342dfc32:activity", "openlicht:polar-m600:342dfc32:heart-rate", "openlicht:polar-m600:342dfc32:steps", "openlicht:polar-m600:342dfc32:brightness"] ;
 Thing: id="openlicht:samsung-s6:2ca84896" label="Samsung S6" type="openlicht:samsung-s6" channels=["openlicht:samsung-s6:2ca84896:brightness", "openlicht:samsung-s6:2ca84896:rotation-x", "openlicht:samsung-s6:2ca84896:rotation-y", "openlicht:samsung-s6:2ca84896:rotation-z"] ;
-Thing: id="hue:bridge:0017880adcf4" label="Philips hue (10.8.0.160)" type="hue:bridge" channels=[] ;
+Thing: id="hue:bridge:0017880adcf4" label="Philips hue (10.8.0.160)" type="hue:bridge" ;
 Color Item: id="Go1_item" label="Go 1" state="213,36,40" category="Lighting" ;
 String Item: id="skywriter_flick_item" label="Skywriter Flick Detected" state="" category="Motion" ;
 Color Item: id="wohnzimmer_item" label="Wohnzimmer" state="0,0,0" category="Lighting" ;
-Color Item: id="iris1_item" label="Iris 1" state="253,0,0" category="Lighting" ;
+Color Item: id="iris1_item" label="Iris 1" state="253,0,0" category="Lighting" metaData={"key2":"value2", "key3":"value3"} ;
 Number Item: id="moto_360_brightness" label="Moto 360 Brightness" state="0.0" category="Brightness" ;
 Number Item: id="polar_brightness" label="Polar Brightness" state="0.0" category="Brightness" ;
 String Item: id="samsung_brightness" label="Samsung Brightness" state="" category="Brightness" ;
-Switch Item: id="Rule_Switch" state="false" ;
+Switch Item: id="Rule_Switch" state="OFF" ;
 Number Item: id="Color_Manual_Slider" state="0.0" ;
 Number Item: id="watch_acceleration_x" label="Watch Acceleration X" state="6.264883611883931E-10" ;
 Number Item: id="watch_acceleration_y" label="Watch Acceleration Y" state="1.5765462769316607E-19" ;
@@ -23,21 +23,21 @@ Number Item: id="watch_rotation_z" label="Watch Rotation Z" state="0.0" ;
 Number Item: id="phone_rotation_x" label="Phone Rotation X" state="0.0" ;
 Number Item: id="phone_rotation_y" label="Phone Rotation Y" state="0.0" ;
 Number Item: id="phone_rotation_z" label="Phone Rotation Z" state="0.0" ;
-Group: id="all_dimmable_lamps" label="All dimmable lamps" aggregation="AVG" items=["Go1_item"] ;
+Group: id="all_dimmable_lamps" label="All dimmable lamps" items=["Go1_item"] aggregation="AVG" ;
 Group: id="Unknown" items=["skywriter_flick_item", "wohnzimmer_item", "iris1_item", "moto_360_brightness", "polar_brightness", "samsung_brightness", "Rule_Switch", "Color_Manual_Slider", "watch_acceleration_x", "watch_acceleration_y", "watch_acceleration_z", "watch_rotation_x", "watch_rotation_y", "watch_rotation_z", "phone_rotation_x", "phone_rotation_y", "phone_rotation_z"] ;
-ThingType: id="hue:0110" label="Dimmable Plug-in Unit" description="An outlet that can be dimmed." parameters=[] channelTypes=[] ;
-ThingType: id="hue:0100" label="Dimmable Light" description="A dimmable light." parameters=[] channelTypes=[] ;
-ThingType: id="hue:0000" label="On/Off Light" description="A light that could be switched on and off." parameters=[] channelTypes=[] ;
-ThingType: id="hue:0010" label="On/Off Plug-in Unit" description="An outlet that could be switched on and off." parameters=[] channelTypes=[] ;
-ThingType: id="hue:0210" label="Extended Color Light" description="A dimmable light with changeable colors and tunable color temperature." parameters=[] channelTypes=[] ;
-ThingType: id="hue:bridge" label="Hue Bridge" description="The hue bridge represents the Philips hue bridge." parameters=[] channelTypes=[] ;
-ThingType: id="hue:0220" label="Color Temperature Light" description="A dimmable light with tunable color temperature." parameters=[] channelTypes=[] ;
-ThingType: id="hue:0200" label="Color Light" description="A dimmable light with changeable colors." parameters=[] channelTypes=[] ;
-ThingType: id="androidsensors:android-sensor" label="AndroidSensors" description="Sensors supported by Android" parameters=[] channelTypes=[] ;
-ThingType: id="openlicht:skywriter-hat" label="SkyWriterHAT" description="SkyWriterHAT Gesture Recognition" parameters=[] channelTypes=[] ;
-ThingType: id="openlicht:polar-m600" label="Polar M600" description="Provides sensor information from a Polar M600 smart watch" parameters=[] channelTypes=[] ;
-ThingType: id="openlicht:moto-360" label="Moto 360" description="Provides sensor information from a Moto 360 smart watch" parameters=[] channelTypes=[] ;
-ThingType: id="openlicht:samsung-s6" label="Samsung S6" description="Provides sensor information from a smart phone (a Samsung S6 in our case)" parameters=[] channelTypes=[] ;
+ThingType: id="hue:0110" label="Dimmable Plug-in Unit" description="An outlet that can be dimmed." ;
+ThingType: id="hue:0100" label="Dimmable Light" description="A dimmable light." ;
+ThingType: id="hue:0000" label="On/Off Light" description="A light that could be switched on and off." ;
+ThingType: id="hue:0010" label="On/Off Plug-in Unit" description="An outlet that could be switched on and off." ;
+ThingType: id="hue:0210" label="Extended Color Light" description="A dimmable light with changeable colors and tunable color temperature." ;
+ThingType: id="hue:bridge" label="Hue Bridge" description="The hue bridge represents the Philips hue bridge." ;
+ThingType: id="hue:0220" label="Color Temperature Light" description="A dimmable light with tunable color temperature." ;
+ThingType: id="hue:0200" label="Color Light" description="A dimmable light with changeable colors." ;
+ThingType: id="androidsensors:android-sensor" label="AndroidSensors" description="Sensors supported by Android" ;
+ThingType: id="openlicht:skywriter-hat" label="SkyWriterHAT" description="SkyWriterHAT Gesture Recognition" ;
+ThingType: id="openlicht:polar-m600" label="Polar M600" description="Provides sensor information from a Polar M600 smart watch" ;
+ThingType: id="openlicht:moto-360" label="Moto 360" description="Provides sensor information from a Moto 360 smart watch" ;
+ThingType: id="openlicht:samsung-s6" label="Samsung S6" description="Provides sensor information from a smart phone (a Samsung S6 in our case)" ;
 ChannelType: id="hue:color" label="Color" description="The color channel allows to control the color of a light. It is also possible to dim values and switch the light on and off." itemType="Color" category="ColorLight" ;
 ChannelType: id="hue:brightness" label="Brightness" description="The brightness channel allows to control the brightness of a light. It is also possible to switch the light on and off." itemType="Dimmer" category="DimmableLight" ;
 ChannelType: id="hue:switch" label="Switch" description="The switch channel allows to switch the light on and off." itemType="Switch" category="Light" ;
@@ -52,38 +52,36 @@ ChannelType: id="openlicht:activity-type" label="Activity" description="Current
 ChannelType: id="openlicht:heart-rate-type" label="Heart Rate" description="Heart rate in beats per minute." itemType="Number" readOnly ;
 ChannelType: id="openlicht:steps-type" label="Steps" description="Steps run today." itemType="Number" readOnly ;
 ChannelType: id="openlicht:brightness-type" label="Brightness" description="Brightness (Lux)." itemType="Number" category="Light" readOnly ;
-ChannelType: id="system:signal-strength" label="Signal Strength" description="" itemType="Number" category="QualityOfService" readOnly ;
-ChannelType: id="system:low-battery" label="Low Battery" description="" itemType="Switch" category="Battery" readOnly ;
-ChannelType: id="system:battery-level" label="Battery Level" description="" itemType="Number" category="Battery" readOnly ;
-ChannelType: id="system:trigger" label="Trigger" description="" ;
-ChannelType: id="system:rawbutton" label="Raw button" description="" ;
-ChannelType: id="system:button" label="Button" description="" ;
-ChannelType: id="system:rawrocker" label="Raw rocker button" description="" ;
-Channel: id="hue:0200:0017880adcf4:5:alert" type="hue:alert" links=[] ;
+ChannelType: id="system:signal-strength" label="Signal Strength" itemType="Number" category="QualityOfService" readOnly ;
+ChannelType: id="system:low-battery" label="Low Battery" itemType="Switch" category="Battery" readOnly ;
+ChannelType: id="system:battery-level" label="Battery Level" itemType="Number" category="Battery" readOnly ;
+ChannelType: id="system:trigger" label="Trigger" ;
+ChannelType: id="system:rawbutton" label="Raw button" ;
+ChannelType: id="system:button" label="Button" ;
+ChannelType: id="system:rawrocker" label="Raw rocker button" ;
+Channel: id="hue:0200:0017880adcf4:5:alert" type="hue:alert" ;
 Channel: id="hue:0200:0017880adcf4:5:color" type="hue:color" links=["iris1_item"] ;
-Channel: id="hue:0200:0017880adcf4:5:effect" type="hue:effect" links=[] ;
-Channel: id="hue:0210:0017880adcf4:1:alert" type="hue:alert" links=[] ;
+Channel: id="hue:0200:0017880adcf4:5:effect" type="hue:effect" ;
+Channel: id="hue:0210:0017880adcf4:1:alert" type="hue:alert" ;
 Channel: id="hue:0210:0017880adcf4:1:color" type="hue:color" links=["wohnzimmer_item"] ;
-Channel: id="hue:0210:0017880adcf4:1:color_temperature" type="hue:color_temperature" links=[] ;
-Channel: id="hue:0210:0017880adcf4:1:effect" type="hue:effect" links=[] ;
-Channel: id="hue:0210:0017880adcf4:4:alert" type="hue:alert" links=[] ;
+Channel: id="hue:0210:0017880adcf4:1:color_temperature" type="hue:color_temperature" ;
+Channel: id="hue:0210:0017880adcf4:1:effect" type="hue:effect" ;
+Channel: id="hue:0210:0017880adcf4:4:alert" type="hue:alert" ;
 Channel: id="hue:0210:0017880adcf4:4:color" type="hue:color" links=["Go1_item"] ;
-Channel: id="hue:0210:0017880adcf4:4:color_temperature" type="hue:color_temperature" links=[] ;
-Channel: id="hue:0210:0017880adcf4:4:effect" type="hue:effect" links=[] ;
+Channel: id="hue:0210:0017880adcf4:4:color_temperature" type="hue:color_temperature" ;
+Channel: id="hue:0210:0017880adcf4:4:effect" type="hue:effect" ;
 Channel: id="openlicht:polar-m600:342dfc32:acceleration-x" type="openlicht:acceleration-type" links=["watch_acceleration_x"] ;
 Channel: id="openlicht:polar-m600:342dfc32:acceleration-y" type="openlicht:acceleration-type" links=["watch_acceleration_y"] ;
 Channel: id="openlicht:polar-m600:342dfc32:acceleration-z" type="openlicht:acceleration-type" links=["watch_acceleration_z"] ;
-Channel: id="openlicht:polar-m600:342dfc32:activity" type="openlicht:activity-type" links=[] ;
+Channel: id="openlicht:polar-m600:342dfc32:activity" type="openlicht:activity-type" ;
 Channel: id="openlicht:polar-m600:342dfc32:brightness" type="openlicht:brightness-type" links=["polar_brightness"] ;
-Channel: id="openlicht:polar-m600:342dfc32:heart-rate" type="openlicht:heart-rate-type" links=[] ;
+Channel: id="openlicht:polar-m600:342dfc32:heart-rate" type="openlicht:heart-rate-type" ;
 Channel: id="openlicht:polar-m600:342dfc32:rotation-x" type="openlicht:rotation-type" links=["watch_rotation_x"] ;
-Channel: id="openlicht:polar-m600:342dfc32:rotation-y" type="openlicht:rotation-type" links=["watch_acceleration_y"] ;
+Channel: id="openlicht:polar-m600:342dfc32:rotation-y" type="openlicht:rotation-type" links=["watch_rotation_y"] ;
 Channel: id="openlicht:polar-m600:342dfc32:rotation-z" type="openlicht:rotation-type" links=["watch_rotation_z"] ;
-Channel: id="openlicht:polar-m600:342dfc32:steps" type="openlicht:steps-type" links=[] ;
+Channel: id="openlicht:polar-m600:342dfc32:steps" type="openlicht:steps-type" ;
 Channel: id="openlicht:samsung-s6:2ca84896:brightness" type="openlicht:brightness-type" links=["samsung_brightness"] ;
 Channel: id="openlicht:samsung-s6:2ca84896:rotation-x" type="openlicht:rotation-type" links=["phone_rotation_x"] ;
 Channel: id="openlicht:samsung-s6:2ca84896:rotation-y" type="openlicht:rotation-type" links=["phone_rotation_y"] ;
 Channel: id="openlicht:samsung-s6:2ca84896:rotation-z" type="openlicht:rotation-type" links=["phone_rotation_z"] ;
 Channel: id="openlicht:skywriter-hat:e937d4f3:flick" type="openlicht:flick-type" links=["skywriter_flick_item"] ;
-Mqtt: incoming="" outgoing="" host="localhost" ;
-Influx: host="localhost" ;
diff --git a/eraser-base/src/test/resources/tests/hostNonDefaultPort/Test.properties b/eraser-base/src/test/resources/tests/hostNonDefaultPort/Test.properties
new file mode 100644
index 0000000000000000000000000000000000000000..616a3deddd4decffd5d0e6e3db7bb8ce3873562a
--- /dev/null
+++ b/eraser-base/src/test/resources/tests/hostNonDefaultPort/Test.properties
@@ -0,0 +1 @@
+result=OUTPUT_PASS
diff --git a/eraser-base/src/test/resources/tests/hostNonDefaultPort/description b/eraser-base/src/test/resources/tests/hostNonDefaultPort/description
new file mode 100644
index 0000000000000000000000000000000000000000..bdaa31556a7a02ab764f16f5a5157676ce5126bf
--- /dev/null
+++ b/eraser-base/src/test/resources/tests/hostNonDefaultPort/description
@@ -0,0 +1 @@
+Test, if non-default ports in MQTT and Influx are working.
diff --git a/eraser-base/src/test/resources/tests/hostNonDefaultPort/input.eraser b/eraser-base/src/test/resources/tests/hostNonDefaultPort/input.eraser
new file mode 100644
index 0000000000000000000000000000000000000000..427f6137aa8058df0e264bb6ca8f200eb71dbd7d
--- /dev/null
+++ b/eraser-base/src/test/resources/tests/hostNonDefaultPort/input.eraser
@@ -0,0 +1,2 @@
+Influx: host="www.example.com:1234";
+Mqtt: host="localhost:9876";
diff --git a/eraser-base/src/test/resources/tests/hostNonDefaultPort/output.eraser b/eraser-base/src/test/resources/tests/hostNonDefaultPort/output.eraser
new file mode 100644
index 0000000000000000000000000000000000000000..73d8147fea0252ec1c95364ad6ad5449a829b1e6
--- /dev/null
+++ b/eraser-base/src/test/resources/tests/hostNonDefaultPort/output.eraser
@@ -0,0 +1,2 @@
+Mqtt: host="localhost:9876" ;
+Influx: host="www.example.com:1234" ;
diff --git a/eraser-base/src/test/resources/tests/ppc1/output.eraser b/eraser-base/src/test/resources/tests/ppc1/output.eraser
index 86fdb9c19d9c7b59cb63e8ce62008fe8af115f6b..dfd7ecb410f78ad8ac5c30b1aa6bf119619b55ce 100644
--- a/eraser-base/src/test/resources/tests/ppc1/output.eraser
+++ b/eraser-base/src/test/resources/tests/ppc1/output.eraser
@@ -1,6 +1,6 @@
 Color Item: id="iris1_item" label="Iris 1" state="121,88,68" topic="iris1_item/state" ;
 Group: id="Unknown" items=["iris1_item"] ;
-ThingType: id="hue:bridge" label="Hue Bridge" description="The hue bridge represents the Philips hue bridge." parameters=[] channelTypes=[] ;
+ThingType: id="hue:bridge" label="Hue Bridge" description="The hue bridge represents the Philips hue bridge." ;
 ThingType: id="skywriter-hat" label="SkyWriterHAT" description="SkyWriterHAT Gesture Recognition" parameters=["brokername"] channelTypes=["flick-type"] ;
 ChannelType: id="flick-type" label="Last Flick" description="Last Flick detected (and its direction)" itemType="String" category="Motion" readOnly ;
 Parameter: id="brokername" label="Broker Name" description="Name of the broker as defined in the &lt;broker&gt;.url in services/mqtt.cfg. See the MQTT Binding for more information on how to configure MQTT broker connections." type="Text" context="service" default="mosquitto" required ;
diff --git a/eraser-base/src/test/resources/tests/ppc3/input.eraser b/eraser-base/src/test/resources/tests/ppc3/input.eraser
index d306f014ee1532e73ab194c3b575645830b9388a..9a8a80d0d47d21df28a233b13135c43a98be3f8a 100644
--- a/eraser-base/src/test/resources/tests/ppc3/input.eraser
+++ b/eraser-base/src/test/resources/tests/ppc3/input.eraser
@@ -14,8 +14,9 @@ Image Item : id="image1" label="an Image Item" state="def" topic="item/str/image
 Location Item : id="location1" label="a Location Item" state="ghi" topic="item/str/location1/state";
 Number Item : id="number1" label="a Number Item" state="456" topic="item/double/number1/state";
 Player Item : id="player1" label="a Player Item" state="jkl" topic="item/str/player1/state";
-RollerShutter Item : id="rollerShutter1" label="a RollerShutter Item" state="false" topic="item/str/rs1/state";
+RollerShutter Item : id="rollerShutter1" label="a RollerShutter Item" state="32" topic="item/str/rs1/state";
 String Item : id="string1" label="a String Item" state="mno" topic="item/str/string1/state";
 Switch Item : id="switch1" label="a Switch Item" state="true" topic="item/bool/switch1/state";
+Switch Item : id="switch2" label="a second Switch Item" state="OFF" topic="item/bool/switch2/state";
 Item : id="default1" label="a Default Item" state="pqr" topic="item/str/default1/state";
 Influx: host="localhost" ;
diff --git a/eraser-base/src/test/resources/tests/ppc3/output.eraser b/eraser-base/src/test/resources/tests/ppc3/output.eraser
index 686baf62230203f4458bd9297e4d017913f9f341..f480a5d4ecb8144e53e02334acf39b7e6b629868 100644
--- a/eraser-base/src/test/resources/tests/ppc3/output.eraser
+++ b/eraser-base/src/test/resources/tests/ppc3/output.eraser
@@ -1,19 +1,20 @@
 Color Item: id="color1" label="a Color Item" state="1,2,3" topic="item/hsb/color1/state" ;
-Contact Item: id="contact1" label="a Contact Item" state="true" topic="item/bool/contact1/state" ;
+Contact Item: id="contact1" label="a Contact Item" state="OPEN" topic="item/bool/contact1/state" ;
 Image Item: id="image1" label="an Image Item" state="def" topic="item/str/image1/state" ;
 Location Item: id="location1" label="a Location Item" state="ghi" topic="item/str/location1/state" ;
-DateTime Item: id="datetime1" label="a DateTime Item" state="1970-01-18T20:43:35.826" topic="item/date/datetime1/state" ;
+DateTime Item: id="datetime1" label="a DateTime Item" state="1970-01-18T20:43:35.826Z" topic="item/date/datetime1/state" ;
 Item: id="default1" label="a Default Item" state="pqr" topic="item/str/default1/state" ;
 Dimmer Item: id="dimmer1" label="a Dimmer Item" state="123.0" topic="item/double/dimmer1/state" ;
 Player Item: id="player1" label="a Player Item" state="jkl" topic="item/str/player1/state" ;
 Number Item: id="number1" label="a Number Item" state="456.0" topic="item/double/number1/state" ;
-RollerShutter Item: id="rollerShutter1" label="a RollerShutter Item" state="false" topic="item/str/rs1/state" ;
+RollerShutter Item: id="rollerShutter1" label="a RollerShutter Item" state="32.0" topic="item/str/rs1/state" ;
 String Item: id="string1" label="a String Item" state="mno" topic="item/str/string1/state" ;
-Switch Item: id="switch1" label="a Switch Item" state="true" topic="item/bool/switch1/state" ;
-Group: id="my-first-group" aggregation="AND" ("ON", "OFF") items=["color1", "contact1", "image1", "location1"] ;
+Switch Item: id="switch1" label="a Switch Item" state="ON" topic="item/bool/switch1/state" ;
+Switch Item: id="switch2" label="a second Switch Item" state="OFF" topic="item/bool/switch2/state" ;
+Group: id="my-first-group" items=["color1", "contact1", "image1", "location1"] aggregation="AND" ("ON", "OFF") ;
 Group: id="my-second-group" items=["datetime1", "default1"] ;
 Group: id="my-third-group" items=["dimmer1", "player1"] ;
 Group: id="my-empty-group" ;
-Group: id="Unknown" items=["number1", "rollerShutter1", "string1", "switch1"] ;
+Group: id="Unknown" items=["number1", "rollerShutter1", "string1", "switch1", "switch2"] ;
 Mqtt: incoming="ppc3/" outgoing="oh2/in/" host="localhost" ;
 Influx: host="localhost" ;
diff --git a/eraser-base/src/test/resources/tests/ppc4/Test.properties b/eraser-base/src/test/resources/tests/ppc4/Test.properties
new file mode 100644
index 0000000000000000000000000000000000000000..616a3deddd4decffd5d0e6e3db7bb8ce3873562a
--- /dev/null
+++ b/eraser-base/src/test/resources/tests/ppc4/Test.properties
@@ -0,0 +1 @@
+result=OUTPUT_PASS
diff --git a/eraser-base/src/test/resources/tests/ppc4/description b/eraser-base/src/test/resources/tests/ppc4/description
new file mode 100644
index 0000000000000000000000000000000000000000..0140c1516a0dea41a1fc44a850990d52c167309b
--- /dev/null
+++ b/eraser-base/src/test/resources/tests/ppc4/description
@@ -0,0 +1 @@
+Test item types and controlling items.
diff --git a/eraser-base/src/test/resources/tests/ppc4/input.eraser b/eraser-base/src/test/resources/tests/ppc4/input.eraser
new file mode 100644
index 0000000000000000000000000000000000000000..8687f881951aa3bc8766e209a87432a235e2a72a
--- /dev/null
+++ b/eraser-base/src/test/resources/tests/ppc4/input.eraser
@@ -0,0 +1,22 @@
+Mqtt: incoming="ppc3/" outgoing="oh2/in/" host="localhost" ;
+
+Group: id="my-first-group" aggregation="AND" ("ON", "OFF") items=["color1", "contact1", "image1", "location1"] ;
+Group: id="my-second-group" items=["datetime1", "default1"] ;
+Group: id="my-third-group" items=["dimmer1", "player1"] ;
+
+Group: id="my-empty-group" ;
+
+Color Item : id="color1" label="a Color Item" state="1,2,3" topic="item/hsb/color1/state";
+DateTime Item : id="datetime1" label="a DateTime Item" state="1543415826" topic="item/date/datetime1/state";
+Contact Item : id="contact1" label="a Contact Item" state="CLOSED" topic="item/bool/contact1/state";
+Dimmer Item : id="dimmer1" label="a Dimmer Item" state="123" topic="item/double/dimmer1/state" controls=["color1", "datetime1"];
+Image Item : id="image1" label="an Image Item" state="def" topic="item/str/image1/state" controls=[];
+Location Item : id="location1" label="a Location Item" state="ghi" topic="item/str/location1/state";
+Number Item : id="number1" label="a Number Item" state="456" topic="item/double/number1/state" controls=["string1"];
+Player Item : id="player1" label="a Player Item" state="jkl" topic="item/str/player1/state";
+RollerShutter Item : id="rollerShutter1" label="a RollerShutter Item" state="0" topic="item/str/rs1/state";
+Activity Item: id="activity";
+String Item : id="string1" label="a String Item" state="mno" topic="item/str/string1/state";
+Switch Item : id="switch1" label="a Switch Item" state="true" topic="item/bool/switch1/state" controls=["rollerShutter1"];
+Item : id="default1" label="a Default Item" state="pqr" topic="item/str/default1/state";
+Influx: host="localhost" ;
diff --git a/eraser-base/src/test/resources/tests/ppc4/output.eraser b/eraser-base/src/test/resources/tests/ppc4/output.eraser
new file mode 100644
index 0000000000000000000000000000000000000000..d4934300c2f7e3c0c8be65bf4f4932bbc4b7be52
--- /dev/null
+++ b/eraser-base/src/test/resources/tests/ppc4/output.eraser
@@ -0,0 +1,20 @@
+Color Item: id="color1" label="a Color Item" state="1,2,3" topic="item/hsb/color1/state" ;
+Contact Item: id="contact1" label="a Contact Item" state="CLOSED" topic="item/bool/contact1/state" ;
+Image Item: id="image1" label="an Image Item" state="def" topic="item/str/image1/state" ;
+Location Item: id="location1" label="a Location Item" state="ghi" topic="item/str/location1/state" ;
+DateTime Item: id="datetime1" label="a DateTime Item" state="1970-01-18T20:43:35.826Z" topic="item/date/datetime1/state" ;
+Item: id="default1" label="a Default Item" state="pqr" topic="item/str/default1/state" ;
+Dimmer Item: id="dimmer1" label="a Dimmer Item" state="123.0" topic="item/double/dimmer1/state" controls=["color1", "datetime1"] ;
+Player Item: id="player1" label="a Player Item" state="jkl" topic="item/str/player1/state" ;
+Number Item: id="number1" label="a Number Item" state="456.0" topic="item/double/number1/state" controls=["string1"] ;
+RollerShutter Item: id="rollerShutter1" label="a RollerShutter Item" state="0.0" topic="item/str/rs1/state" ;
+Activity Item: id="activity" ;
+String Item: id="string1" label="a String Item" state="mno" topic="item/str/string1/state" ;
+Switch Item: id="switch1" label="a Switch Item" state="ON" topic="item/bool/switch1/state" controls=["rollerShutter1"] ;
+Group: id="my-first-group" items=["color1", "contact1", "image1", "location1"] aggregation="AND" ("ON", "OFF") ;
+Group: id="my-second-group" items=["datetime1", "default1"] ;
+Group: id="my-third-group" items=["dimmer1", "player1"] ;
+Group: id="my-empty-group" ;
+Group: id="Unknown" items=["number1", "rollerShutter1", "activity", "string1", "switch1"] ;
+Mqtt: incoming="ppc3/" outgoing="oh2/in/" host="localhost" ;
+Influx: host="localhost" ;
diff --git a/eraser-base/src/test/resources/tests/ppc5/Test.properties b/eraser-base/src/test/resources/tests/ppc5/Test.properties
new file mode 100644
index 0000000000000000000000000000000000000000..616a3deddd4decffd5d0e6e3db7bb8ce3873562a
--- /dev/null
+++ b/eraser-base/src/test/resources/tests/ppc5/Test.properties
@@ -0,0 +1 @@
+result=OUTPUT_PASS
diff --git a/eraser-base/src/test/resources/tests/ppc5/description b/eraser-base/src/test/resources/tests/ppc5/description
new file mode 100644
index 0000000000000000000000000000000000000000..ff98c287292d9b2a62174084a242cf6e4d25c012
--- /dev/null
+++ b/eraser-base/src/test/resources/tests/ppc5/description
@@ -0,0 +1,2 @@
+Complete feature-test, i.e., minimal and maximal many members set for each parseable non-terminal.
+Also test, if multi-line statements work.
diff --git a/eraser-base/src/test/resources/tests/ppc5/input.eraser b/eraser-base/src/test/resources/tests/ppc5/input.eraser
new file mode 100644
index 0000000000000000000000000000000000000000..21cc14bc796965aae71d2bca119f60b0cdf640c5
--- /dev/null
+++ b/eraser-base/src/test/resources/tests/ppc5/input.eraser
@@ -0,0 +1,35 @@
+ChannelType: id="min-channel-type";
+ChannelType: label="Max Channel Type 1" id="max-channel-type-1" itemType="String" description="Channel type with all members set" category="Text" ;
+ChannelType: description="Channel type with all members set" id="max-channel-type-2" readOnly itemType="String" label="Max Channel Type 2" category="CustomCategory" ;
+
+// Channels will get sorted alphabetically in output
+Channel: type="min-channel-type" id="min-channel" ; // "type" must be set
+Channel: links=["min-item", "max-item"] id="max-channel" type="max-channel-type-1" ;
+
+Group: id="min-group" ;
+Group: items=["min-item", "max-item"] id="max-group" groups=["min-group"] aggregation="AND" ("one", "two") ;
+
+Number Item: id="min-item" ; // state will be set to default value
+Switch Item: topic="items/max"
+ controls=["min-item"] id="max-item" state="true" label="Item with all members set" category="not used"
+ metaData={"one":"true", "zero":"false"} ;
+
+// Parameters will get sorted alphabetically in output
+Parameter: id="min-parameter" ;
+Parameter
+:
+label="Max Parameter" required id="max-parameter" type="Decimal" description="Parameter with all members set" default="should be a number" ;
+
+ThingType: id="min-thing-type" ;
+ThingType: id="max-thing-type" label="Max Thing Type" description="Thing type with all members set" parameters=["min-parameter", "max-parameter"] channelTypes=["min-channel-type", "max-channel-type-1", "max-channel-type-2"] ;
+
+Thing: type="min-thing-type" id="min-thing" ; // "type" must be set
+Thing: id="max-thing" label="Max Thing" type="max-thing-type" channels=["min-channel", "max-channel"] ;
+
+Influx: ; // minimal Influx, most probably will fail in an actual system
+Mqtt: ; // minimal MQTT, most probably will fail in an actual system
+
+ML: activities = {
+  0 : "an interesting activity",
+  3: "rather boring activity"
+} ;
diff --git a/eraser-base/src/test/resources/tests/ppc5/output.eraser b/eraser-base/src/test/resources/tests/ppc5/output.eraser
new file mode 100644
index 0000000000000000000000000000000000000000..5498db37c271084736a1be692b318f92ab2c5b5e
--- /dev/null
+++ b/eraser-base/src/test/resources/tests/ppc5/output.eraser
@@ -0,0 +1,16 @@
+Thing: id="min-thing" type="min-thing-type" ;
+Thing: id="max-thing" label="Max Thing" type="max-thing-type" channels=["min-channel", "max-channel"] ;
+Number Item: id="min-item" state="0.0" ;
+Switch Item: id="max-item" label="Item with all members set" state="ON" category="not used" topic="items/max" controls=["min-item"] metaData={"one":"true", "zero":"false"} ;
+Group: id="min-group" ;
+Group: id="max-group" groups=["min-group"] items=["min-item", "max-item"] aggregation="AND" ("one", "two") ;
+ThingType: id="min-thing-type" ;
+ThingType: id="max-thing-type" label="Max Thing Type" description="Thing type with all members set" parameters=["min-parameter", "max-parameter"] channelTypes=["min-channel-type", "max-channel-type-1", "max-channel-type-2"] ;
+ChannelType: id="min-channel-type" ;
+ChannelType: id="max-channel-type-1" label="Max Channel Type 1" description="Channel type with all members set" itemType="String" category="Text" ;
+ChannelType: id="max-channel-type-2" label="Max Channel Type 2" description="Channel type with all members set" itemType="String" category="CustomCategory" readOnly ;
+Parameter: id="max-parameter" label="Max Parameter" description="Parameter with all members set" type="Decimal" default="should be a number" required ;
+Parameter: id="min-parameter" ;
+Channel: id="max-channel" type="max-channel-type-1" links=["min-item", "max-item"] ;
+Channel: id="min-channel" type="min-channel-type" ;
+ML: activities={0:"an interesting activity", 3:"rather boring activity"} ;
diff --git a/eraser.rest/.gitignore b/eraser.rest/.gitignore
index 84c048a73cc2e5dd24f807669eb99b0ce3123195..70b583e34c3316bcd77c807e2d6b85db5e7d49f6 100644
--- a/eraser.rest/.gitignore
+++ b/eraser.rest/.gitignore
@@ -1 +1,3 @@
 /build/
+/bin/
+logs/
diff --git a/eraser.rest/build.gradle b/eraser.rest/build.gradle
index 0f7c52ef57d6d2c0100b9bb34e0473c5f4a05e6b..0ac26261047eacbcc2b97709e7a5b4c1bc927b3b 100644
--- a/eraser.rest/build.gradle
+++ b/eraser.rest/build.gradle
@@ -1,37 +1,22 @@
 buildscript {
-    repositories {
-        mavenCentral()
-    }
     dependencies {
         classpath("org.springframework.boot:spring-boot-gradle-plugin:2.1.2.RELEASE")
     }
 }
 
 plugins {
-    id 'java'
     id 'io.franzbecker.gradle-lombok' version '1.14'
 }
 
-repositories {
-    mavenCentral()
-}
-
 apply plugin: 'org.springframework.boot'
 apply plugin: 'io.spring.dependency-management'
 
-sourceCompatibility = 1.8
-
 dependencies {
     compile project(':eraser-base')
-    compile group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: '2.9.8'
-    compile group: 'org.apache.logging.log4j', name: 'log4j-api', version: '2.11.1'
-    compile group: 'org.apache.logging.log4j', name: 'log4j-core', version: '2.11.1'
     compile 'org.springframework.boot:spring-boot-starter-web'
-    compile 'io.springfox:springfox-swagger2:2.9.2'
-    compile 'io.springfox:springfox-swagger-ui:2.9.2'
+    compile group: 'io.springfox', name: 'springfox-swagger2', version: '2.9.2'
+    compile group: 'io.springfox', name: 'springfox-swagger-ui', version: '2.9.2'
     testCompile 'org.springframework.boot:spring-boot-starter-test'
-    testCompile group: 'junit', name: 'junit', version: '4.12'
-    testCompile group: 'org.hamcrest', name: 'hamcrest-junit', version: '2.0.0.0'
 }
 
 sourceSets {
diff --git a/eraser.rest/src/main/resources/log4j2.xml b/eraser.rest/src/main/resources/log4j2.xml
index 0594576fac98ba859e411597c90c8e3d989378bd..867ec439d0a32dcb5f8b3e2d0c7485d7d8da418c 100644
--- a/eraser.rest/src/main/resources/log4j2.xml
+++ b/eraser.rest/src/main/resources/log4j2.xml
@@ -4,8 +4,8 @@
         <Console name="Console">
             <PatternLayout pattern="%highlight{%d{HH:mm:ss.SSS} %-5level} %c{1.} - %msg%n"/>
         </Console>
-        <RollingFile name="RollingFile" fileName="logs/jastadd-mquat.log"
-                    filePattern="logs/jastadd-mquat-%i.log">
+        <RollingFile name="RollingFile" fileName="logs/eraser.log"
+                    filePattern="logs/eraser-%i.log">
             <PatternLayout pattern="%d{HH:mm:ss.SSS} %-5level %logger{36} - %msg%n"/>
             <Policies>
                 <OnStartupTriggeringPolicy/>
diff --git a/eraser.spark/.gitignore b/eraser.spark/.gitignore
index 84c048a73cc2e5dd24f807669eb99b0ce3123195..70b583e34c3316bcd77c807e2d6b85db5e7d49f6 100644
--- a/eraser.spark/.gitignore
+++ b/eraser.spark/.gitignore
@@ -1 +1,3 @@
 /build/
+/bin/
+logs/
diff --git a/eraser.spark/build.gradle b/eraser.spark/build.gradle
index fb3151d88260b4899ed862101a54b3866d9d748f..5a577d15400aab9d58ac2fec4fae8589b0995020 100644
--- a/eraser.spark/build.gradle
+++ b/eraser.spark/build.gradle
@@ -1,24 +1,12 @@
 plugins {
-    id 'java'
     id 'application'
-    id 'io.franzbecker.gradle-lombok' version '1.14'
+    id 'io.franzbecker.gradle-lombok' version '3.0.0'
 }
 
-repositories {
-    mavenCentral()
-}
-
-sourceCompatibility = 1.8
-
 dependencies {
     compile project(':eraser-base')
-    compile group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: '2.9.8'
-    compile group: 'org.apache.logging.log4j', name: 'log4j-api', version: '2.11.1'
-    compile group: 'org.apache.logging.log4j', name: 'log4j-core', version: '2.11.1'
-    compile 'org.apache.logging.log4j:log4j-slf4j-impl:2.11.1'
-    compile 'com.sparkjava:spark-core:2.7.2'
-    testCompile group: 'junit', name: 'junit', version: '4.12'
-    testCompile group: 'org.hamcrest', name: 'hamcrest-junit', version: '2.0.0.0'
+    compile group: 'org.apache.logging.log4j', name: 'log4j-slf4j-impl', version: '2.11.2'
+    compile group: 'com.sparkjava', name: 'spark-core', version: '2.9.0'
 }
 
 run {
diff --git a/eraser.spark/src/main/java/de/tudresden/inf/st/eraser/spark/Application.java b/eraser.spark/src/main/java/de/tudresden/inf/st/eraser/spark/Application.java
index 6bc66c1e0dd89cbfb8a7ee7523857e970bbffbd3..4d8df7dfaa448fe34c6cef1ed51a30d4dd2bc1ae 100644
--- a/eraser.spark/src/main/java/de/tudresden/inf/st/eraser/spark/Application.java
+++ b/eraser.spark/src/main/java/de/tudresden/inf/st/eraser/spark/Application.java
@@ -3,13 +3,20 @@ package de.tudresden.inf.st.eraser.spark;
 import com.fasterxml.jackson.databind.ObjectMapper;
 import de.tudresden.inf.st.eraser.jastadd.model.*;
 import de.tudresden.inf.st.eraser.util.JavaUtils;
+import de.tudresden.inf.st.eraser.util.ParserUtils;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
 import spark.Request;
 import spark.Response;
 import spark.Spark;
 
-import java.util.*;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
 import java.util.concurrent.locks.Condition;
 import java.util.concurrent.locks.Lock;
+import java.util.function.Function;
+import java.util.stream.Collectors;
 
 /**
  * The main class to start the rest service.
@@ -18,47 +25,133 @@ import java.util.concurrent.locks.Lock;
  */
 public class Application {
 
-  private final Root model;
+  private static final Logger logger = LogManager.getLogger(Application.class);
+  private final Root root;
   private final Lock lock;
   private final Condition quitCondition;
 
-  private Application(Root model, Lock lock, Condition quitCondition) {
-    this.model = model;
+  private Application(Root root, Lock lock, Condition quitCondition) {
+    this.root = root;
     this.lock = lock;
     this.quitCondition = quitCondition;
   }
 
   private void createRules() {
     ObjectMapper mapper = new ObjectMapper();
-    Spark.get("/items/", (request, response) ->
-        model.items(),
-        mapper::writeValueAsString);
-
-    Spark.get("/activity/",
-        (request, response) -> wrapActivityList(model.getMachineLearningRoot().getActivityList()),
-        mapper::writeValueAsString);
-    Spark.get("/activity/current",
-        (request, response) -> JavaUtils.ifPresentOrElseReturn(model.currentActivity(),
-            this::wrapActivity,
-            () -> makeError(response, 204, "No activity recognized.")),
-        mapper::writeValueAsString);
-    Spark.get("/activity/:identifier",
-        (request, response) ->
-            JavaUtils.ifPresentOrElseReturn(model.resolveActivity(paramAsInt(request, "identifier")),
-                this::wrapActivity,
-                () -> makeError(response, 404, "No activity for identifier " + request.params("identifier"))),
-        mapper::writeValueAsString);
-
-    Spark.get("/events/",
-        (request, response) -> wrapChangeEventList(model.getMachineLearningRoot().getChangeEventList()),
-        mapper::writeValueAsString);
-    Spark.get("/events/:identifier",
-        (request, response) ->
-            JavaUtils.ifPresentOrElseReturn(model.resolveChangeEvent(paramAsInt(request, "identifier")),
-                this::wrapChangeEvent,
-                () -> makeError(response, 404, "No event for identifier " + request.params("identifier"))),
-        mapper::writeValueAsString);
 
+    Spark.path("/", () -> Spark.before((request, response) -> logger.debug("{}: {}", request.pathInfo(), request.body())));
+
+    Spark.path("/activity", () -> {
+
+      //--- GET /activity ---
+      Spark.get("",
+          (request, response) -> wrapActivityList(root.getMachineLearningRoot().getActivityList()),
+          mapper::writeValueAsString);
+
+      //--- GET /activity/current ---
+      Spark.get("/current",
+          (request, response) -> JavaUtils.ifPresentOrElseReturn(root.currentActivity(),
+              this::wrapActivity,
+              () -> makeError(response, 204, "No activity recognized.")),
+          mapper::writeValueAsString);
+
+      //--- PUT /activity/current ---
+      Spark.put("/current", (request, response) -> {
+        logger.info("request body: '{}', params: '{}', length={}", request.body(), request.params(), request.contentLength());
+        if (!root.getMachineLearningRoot().hasActivityRecognition()) {
+          return makeError(response, 404, "No activity recognition model found");
+        }
+        MachineLearningModel activityRecognition = root.getMachineLearningRoot().getActivityRecognition();
+        if (activityRecognition.canSetActivity()) {
+          activityRecognition.setActivity(Integer.valueOf(request.body()));
+          return "OK";
+        } else {
+          return makeError(response, 501, "Activity not editable for " + activityRecognition.getClass().getSimpleName());
+        }
+      });
+
+      //--- GET /activity/:identifier ---
+      Spark.get("/:identifier",
+          (request, response) ->
+              JavaUtils.ifPresentOrElseReturn(root.resolveActivity(paramAsInt(request, "identifier")),
+                  this::wrapActivity,
+                  () -> makeError(response, 404, "No activity for identifier " + request.params("identifier"))),
+          mapper::writeValueAsString);
+    });
+
+    Spark.path("/events", () -> {
+
+      //--- GET /events ---
+      Spark.get("",
+          (request, response) -> wrapChangeEventList(root.getMachineLearningRoot().getChangeEventList()),
+          mapper::writeValueAsString);
+
+      //--- GET /events/:identifier ---
+      Spark.get("/:identifier",
+          (request, response) ->
+              JavaUtils.ifPresentOrElseReturn(root.resolveChangeEvent(paramAsInt(request, "identifier")),
+                  this::wrapChangeEvent,
+                  () -> makeError(response, 404, "No event for identifier " + request.params("identifier"))),
+          mapper::writeValueAsString);
+    });
+
+    Spark.path("/model", () -> {
+
+      //--- GET /model/full ---
+      Spark.get("/full", (request, response) -> {
+        response.type("text/plain");
+        return root.prettyPrint();
+      });
+      Spark.path("/items", () -> {
+
+        //--- GET /model/items ---
+        Spark.get("",
+            (request, response) -> wrapItemList(root.getOpenHAB2Model().items()),
+            mapper::writeValueAsString);
+        Spark.path("/:identifier", () -> {
+
+          Spark.put("", (request, response) -> {
+            OpenHAB2Model openHAB2Model = root.getOpenHAB2Model();
+            Item item = ParserUtils.parseItem(request.body());
+            if (!openHAB2Model.resolveItem(item.getID()).isPresent()) {
+              root.getOpenHAB2Model().addNewItem(item);
+              response.status(201);
+              return "OK";
+            } else {
+              return makeError(response, 409, "Item already exists.");
+            }
+          });
+
+          //--- GET /model/items/:identifier/state ---
+          Spark.get("/state", (request, response) ->
+              safeItemRoute(request, response, Item::getStateAsString));
+
+          //--- PUT /model/items/:identifier/state ---
+          Spark.put("/state", (request, response) -> {
+            logger.info("request body: '{}', params: '{}', length={}", request.body(), request.params(), request.contentLength());
+            return safeItemRoute(request, response,
+                item -> {
+                  try {
+                    item.setStateFromString(request.body());
+                    return "OK";
+                  } catch (Exception e) {
+                    logger.catching(e);
+                    return makeError(response, 500, e.getMessage());
+                  }
+                });
+          });
+        });
+
+        //--- GET /model/items/:identifier/history ---
+        Spark.get("/:identifier/history",
+            (request, response) -> {
+              logger.info("request body: '{}', params: '{}', length={}", request.body(), request.params(), request.contentLength());
+              return safeItemRoute(request, response, item -> makeHistory(item, response));
+            });
+      });
+    });
+
+    //--- POST /system/exit ---
     Spark.post("/system/exit", (request, response) -> {
       try {
         lock.lock();
@@ -71,6 +164,36 @@ public class Application {
     });
   }
 
+  private Object safeItemRoute(Request request, Response response, Function<Item, String> action) {
+    return JavaUtils.ifPresentOrElseReturn(root.getOpenHAB2Model().resolveItem(request.params("identifier")), action,
+        () -> makeError(response, 404, "Item '" + request.params("identifier") + "' not found"));
+  }
+
+  private String makeHistory(Item item, Response response) {
+    response.type("text/plain");
+    InfluxAdapter influxAdapter = root.getInfluxRoot().influxAdapter();
+    influxAdapter.disableAsyncQuery();
+    List<? extends AbstractItemPoint> list;
+    if (item.asColorItem() != null) {
+      list = item.asColorItem().getHistory();
+    } else if (item.asDateTimeItem() != null) {
+      list = item.asDateTimeItem().getHistory();
+    } else if (item.asItemWithBooleanState() != null) {
+      list = item.asItemWithBooleanState().getHistory();
+    } else if (item.asItemWithDoubleState() != null) {
+      list = item.asItemWithDoubleState().getHistory();
+    } else if (item.asItemWithStringState() != null) {
+      list = item.asItemWithStringState().getHistory();
+    } else {
+      String message = "Can not make history for item of unknown type.";
+      logger.warn(message);
+      influxAdapter.enableAsyncQuery();
+      return message;
+    }
+    influxAdapter.enableAsyncQuery();
+    return list.stream().map(AbstractItemPoint::toString).collect(Collectors.joining("\n"));
+  }
+
   private String makeError(Response response, int status_code, String message) {
     response.status(status_code);
     return message;
@@ -89,31 +212,55 @@ public class Application {
   }
 
   private List<SimpleActivity> wrapActivityList(JastAddList<Activity> activities) {
-    List<SimpleActivity> result = new ArrayList<>(activities.getNumChild());
-    for (Activity activity : activities) {
-      result.add(wrapActivity(activity));
-    }
-    return result;
+    return JavaUtils.toStream(activities).map(this::wrapActivity).collect(Collectors.toList());
   }
 
   private List<SimpleChangeEvent> wrapChangeEventList(JastAddList<ChangeEvent> events) {
-    List<SimpleChangeEvent> result = new ArrayList<>(events.getNumChild());
-    for (ChangeEvent event : events) {
-      result.add(wrapChangeEvent(event));
+    return JavaUtils.toStream(events).map(this::wrapChangeEvent).collect(Collectors.toList());
+  }
+
+  private SimpleItem wrapItem(Item item) {
+    return SimpleItem.of(item.getID(),
+        item.getLabel(),
+        item.getTopic() != null ? item.getTopic().getTopicString() : null,
+        item.isFrozen(),
+        item.isSendState(),
+        item.getControllingList().stream().map(Item::getID).collect(Collectors.toList()),
+        wrapMetaData(item.getMetaDataList()));
+  }
+
+  private Map<String, String> wrapMetaData(JastAddList<ItemMetaData> itemMetaDataList) {
+    Map<String, String> result = new HashMap<>();
+    for (ItemMetaData metaData : itemMetaDataList) {
+      result.put(metaData.getKey(), metaData.getValue());
     }
     return result;
   }
 
+  private List<SimpleItem> wrapItemList(List<Item> items) {
+    return items.stream().map(this::wrapItem).collect(Collectors.toList());
+  }
+
   private int paramAsInt(Request request, String paramName) {
     return Integer.parseInt(request.params(paramName));
   }
 
   public static void start(int port, Root model, boolean createDummyMLData, Lock lock, Condition quit) {
-    Spark.port(port);
-    Application application = new Application(model, lock, quit);
-    if (createDummyMLData) {
-      new DummyDataCreator(model).addDummyDataToModel();
+    try {
+      Spark.port(port);
+      Application application = new Application(model, lock, quit);
+      if (createDummyMLData) {
+        new DummyDataCreator(model).addDummyDataToModel();
+      }
+      application.createRules();
+    } catch (Exception e) {
+      logger.catching(e);
+      try {
+        lock.lock();
+        quit.signalAll();
+      } finally {
+        lock.unlock();
+      }
     }
-    application.createRules();
   }
 }
diff --git a/eraser.spark/src/main/java/de/tudresden/inf/st/eraser/spark/DummyDataCreator.java b/eraser.spark/src/main/java/de/tudresden/inf/st/eraser/spark/DummyDataCreator.java
index f0ae79d88b5695a778370127ba23394b51c5b81c..65885823f7960465adadf30bbda4f82744c54903 100644
--- a/eraser.spark/src/main/java/de/tudresden/inf/st/eraser/spark/DummyDataCreator.java
+++ b/eraser.spark/src/main/java/de/tudresden/inf/st/eraser/spark/DummyDataCreator.java
@@ -3,6 +3,9 @@ package de.tudresden.inf.st.eraser.spark;
 import de.tudresden.inf.st.eraser.jastadd.model.*;
 import de.tudresden.inf.st.eraser.util.JavaUtils;
 
+import java.time.Duration;
+import java.time.Instant;
+
 /**
  * Creates some activities and change events.
  *
@@ -10,56 +13,65 @@ import de.tudresden.inf.st.eraser.util.JavaUtils;
  */
 class DummyDataCreator {
 
-  private final Root model;
+  private final Root root;
 
-  DummyDataCreator(Root model) {
-    this.model = model;
+  DummyDataCreator(Root root) {
+    this.root = root;
   }
 
   private void addDummyActivitiesToModel() {
-    MachineLearningRoot mlRoot = model.getMachineLearningRoot();
+    MachineLearningRoot mlRoot = root.getMachineLearningRoot();
     mlRoot.addActivity(new Activity(1, "Sitting in armchair"));
     mlRoot.addActivity(new Activity(2, "Going to sleep"));
     mlRoot.addActivity(new Activity(3, "Entering house"));
   }
 
   private void addDummyChangeEventsToModel() {
-    MachineLearningRoot mlRoot = model.getMachineLearningRoot();
+    MachineLearningRoot mlRoot = root.getMachineLearningRoot();
     Item iris1 = getOrCreateColorItem("iris1", "Hue Iris 1");
     Item go1 = getOrCreateColorItem("go1", "Hue Go 1");
     Item go2 = getOrCreateColorItem("go2", "Hue Go 2");
-    mlRoot.addChangeEvent(newRecognitionEvent(1, 1547637740, 1,
+    Instant now = Instant.now();
+    mlRoot.addChangeEvent(newRecognitionEvent(1, now, 1,
         new ChangedItem("green", iris1), new ChangedItem("green", go1)));
-    mlRoot.addChangeEvent(newRecognitionEvent(2, 1547637750, 1));
-    mlRoot.addChangeEvent(newRecognitionEvent(4, 1547623460, 2,
+    mlRoot.addChangeEvent(newRecognitionEvent(2, now.plusSeconds(1), 1));
+    mlRoot.addChangeEvent(newRecognitionEvent(3, now.plusSeconds(3), 2,
         new ChangedItem("off", go2)));
-    mlRoot.addChangeEvent(newManualChangeEvent(1501146256, 5,
+    mlRoot.addChangeEvent(newManualChangeEvent(4, now.plusSeconds(17),
         new ChangedItem("green", iris1),
         new ChangedItem("red", go1),
         new ChangedItem("#EE7F00", go2)));
   }
 
   private Item getOrCreateColorItem(String name, String label) {
-    return model.resolveItem(name).orElseGet(() -> new ColorItem(name, label, true, false, TupleHSB.of(0, 0, 0)));
+    return root.getOpenHAB2Model().resolveItem(name).orElseGet(() -> {
+      ColorItem result = new ColorItem();
+      result.setID(name);
+      result.setLabel(label);
+      result.disableSendState();
+      result.setState(TupleHSB.of(0, 0, 0));
+      result.enableSendState();
+      return result;
+    });
   }
 
-  private RecognitionEvent newRecognitionEvent(int identifier, long timestamp, int activityIdentifier, ChangedItem... changedItems) {
+  private RecognitionEvent newRecognitionEvent(int identifier, Instant when, int activityIdentifier, ChangedItem... changedItems) {
     RecognitionEvent result = new RecognitionEvent();
-    JavaUtils.ifPresentOrElse(model.resolveActivity(activityIdentifier), result::setActivity, () -> { throw new RuntimeException("No activity found for identifier " + activityIdentifier); });
-    initChangeEvent(result, identifier, timestamp, changedItems);
+    JavaUtils.ifPresentOrElse(root.resolveActivity(activityIdentifier), result::setActivity, () -> { throw new RuntimeException("No activity found for identifier " + activityIdentifier); });
+    initChangeEvent(result, identifier, when, changedItems);
     return result;
   }
 
   @SuppressWarnings("SameParameterValue")
-  private ManualChangeEvent newManualChangeEvent(int identifier, long timestamp, ChangedItem... changedItems) {
+  private ManualChangeEvent newManualChangeEvent(int identifier, Instant when, ChangedItem... changedItems) {
     ManualChangeEvent result = new ManualChangeEvent();
-    initChangeEvent(result, identifier, timestamp, changedItems);
+    initChangeEvent(result, identifier, when, changedItems);
     return result;
   }
 
-  private void initChangeEvent(ChangeEvent result, int identifier, long timestamp, ChangedItem... changedItems) {
+  private void initChangeEvent(ChangeEvent result, int identifier, Instant when, ChangedItem... changedItems) {
     result.setIdentifier(identifier);
-    result.setTimestamp(timestamp);
+    result.setCreated(when);
     for (ChangedItem changedItem : changedItems) {
       result.addChangedItem(changedItem);
     }
diff --git a/eraser.spark/src/main/java/de/tudresden/inf/st/eraser/spark/SimpleActivity.java b/eraser.spark/src/main/java/de/tudresden/inf/st/eraser/spark/SimpleActivity.java
index 5d914d8a8da8f3aab9cd68c92af5ba3235141460..826463ab6e80794a694852942560b5b886f36bed 100644
--- a/eraser.spark/src/main/java/de/tudresden/inf/st/eraser/spark/SimpleActivity.java
+++ b/eraser.spark/src/main/java/de/tudresden/inf/st/eraser/spark/SimpleActivity.java
@@ -1,9 +1,11 @@
 package de.tudresden.inf.st.eraser.spark;
 
-//import io.swagger.annotations.ApiModelProperty;
 import de.tudresden.inf.st.eraser.jastadd.model.Activity;
 import lombok.Data;
 
+import java.util.Collections;
+import java.util.List;
+
 /**
  * A recognizable activity.
  *
@@ -11,12 +13,11 @@ import lombok.Data;
  */
 @Data(staticConstructor = "of")
 public class SimpleActivity {
-//  @ApiModelProperty(notes = "Some identifier of this activity")
   public final int identifier;
-//  @ApiModelProperty(notes = "Name of the activity")
   public final String description;
+  public final List<String> items;
 
   static SimpleActivity createFrom(Activity activity) {
-    return SimpleActivity.of(activity.getIdentifier(), activity.getLabel());
+    return SimpleActivity.of(activity.getIdentifier(), activity.getLabel(), Collections.emptyList());
   }
 }
diff --git a/eraser.spark/src/main/java/de/tudresden/inf/st/eraser/spark/SimpleChangeEvent.java b/eraser.spark/src/main/java/de/tudresden/inf/st/eraser/spark/SimpleChangeEvent.java
index 07894ede8b266b46bf4aa364bb4495d07abc8740..6a1c1a91bc3d8cdc28d44b07b766168e645786bf 100644
--- a/eraser.spark/src/main/java/de/tudresden/inf/st/eraser/spark/SimpleChangeEvent.java
+++ b/eraser.spark/src/main/java/de/tudresden/inf/st/eraser/spark/SimpleChangeEvent.java
@@ -3,6 +3,7 @@ package de.tudresden.inf.st.eraser.spark;
 import lombok.AllArgsConstructor;
 import lombok.Data;
 
+import java.time.Instant;
 import java.util.List;
 
 /**
@@ -13,7 +14,7 @@ import java.util.List;
 @Data
 @AllArgsConstructor
 public abstract class SimpleChangeEvent {
-  public final long timestamp;
+  public final Instant created;
   public final int identifier;
   public final List<SimpleChangedItem> changed_items;
 }
diff --git a/eraser.spark/src/main/java/de/tudresden/inf/st/eraser/spark/SimpleItem.java b/eraser.spark/src/main/java/de/tudresden/inf/st/eraser/spark/SimpleItem.java
new file mode 100644
index 0000000000000000000000000000000000000000..2eac624367215c547ef4cfe0058fdcce337062fa
--- /dev/null
+++ b/eraser.spark/src/main/java/de/tudresden/inf/st/eraser/spark/SimpleItem.java
@@ -0,0 +1,22 @@
+package de.tudresden.inf.st.eraser.spark;
+
+import lombok.Data;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Safe representation of an item
+ *
+ * @author rschoene - Initial contribution
+ */
+@Data(staticConstructor = "of")
+public class SimpleItem {
+  public final String ID;
+  public final String label;
+  public final String topic;
+  public final boolean frozen;
+  public final boolean sendState;
+  public final List<String> controlling;
+  public final Map<String, String> metaData;
+}
diff --git a/eraser.spark/src/main/java/de/tudresden/inf/st/eraser/spark/SimpleManualChangeEvent.java b/eraser.spark/src/main/java/de/tudresden/inf/st/eraser/spark/SimpleManualChangeEvent.java
index 71e43cdbbcce6eb65704d280540220db880ed3b6..685436cd376b993403db98d4a6192b4de94975d5 100644
--- a/eraser.spark/src/main/java/de/tudresden/inf/st/eraser/spark/SimpleManualChangeEvent.java
+++ b/eraser.spark/src/main/java/de/tudresden/inf/st/eraser/spark/SimpleManualChangeEvent.java
@@ -5,6 +5,7 @@ import de.tudresden.inf.st.eraser.util.JavaUtils;
 import lombok.Data;
 import lombok.EqualsAndHashCode;
 
+import java.time.Instant;
 import java.util.List;
 import java.util.stream.Collectors;
 
@@ -18,12 +19,12 @@ import java.util.stream.Collectors;
 public class SimpleManualChangeEvent extends SimpleChangeEvent {
   public final String type = "manual";
 
-  public SimpleManualChangeEvent(long timestamp, int identifier, List<SimpleChangedItem> changedItems) {
-    super(timestamp, identifier, changedItems);
+  public SimpleManualChangeEvent(Instant created, int identifier, List<SimpleChangedItem> changedItems) {
+    super(created, identifier, changedItems);
   }
 
   static SimpleManualChangeEvent createFrom(ManualChangeEvent event) {
-    return new SimpleManualChangeEvent(event.getTimestamp(), event.getIdentifier(),
+    return new SimpleManualChangeEvent(event.getCreated(), event.getIdentifier(),
         JavaUtils.toStream(event.getChangedItems()).map(SimpleChangedItem::createFrom).collect(Collectors.toList()));
   }
 }
diff --git a/eraser.spark/src/main/java/de/tudresden/inf/st/eraser/spark/SimpleRecognitionEvent.java b/eraser.spark/src/main/java/de/tudresden/inf/st/eraser/spark/SimpleRecognitionEvent.java
index 587c3ae127da7cf3be1064a4351e36f6bbf81e7d..0a3821e4fc49d9fa2b2d0e75717da5477c0a3fa7 100644
--- a/eraser.spark/src/main/java/de/tudresden/inf/st/eraser/spark/SimpleRecognitionEvent.java
+++ b/eraser.spark/src/main/java/de/tudresden/inf/st/eraser/spark/SimpleRecognitionEvent.java
@@ -5,6 +5,7 @@ import de.tudresden.inf.st.eraser.util.JavaUtils;
 import lombok.Data;
 import lombok.EqualsAndHashCode;
 
+import java.time.Instant;
 import java.util.List;
 import java.util.stream.Collectors;
 
@@ -20,14 +21,14 @@ public class SimpleRecognitionEvent extends SimpleChangeEvent {
   public final String description;
   public final String type = "recognition";
 
-  public SimpleRecognitionEvent(long timestamp, int identifier, List<SimpleChangedItem> changedItems, int activity, String description) {
-    super(timestamp, identifier, changedItems);
+  public SimpleRecognitionEvent(Instant created, int identifier, List<SimpleChangedItem> changedItems, int activity, String description) {
+    super(created, identifier, changedItems);
     this.activity = activity;
     this.description = description;
   }
 
   static SimpleRecognitionEvent createFrom(RecognitionEvent event) {
-    return new SimpleRecognitionEvent(event.getTimestamp(), event.getIdentifier(),
+    return new SimpleRecognitionEvent(event.getCreated(), event.getIdentifier(),
         JavaUtils.toStream(event.getChangedItems()).map(SimpleChangedItem::createFrom).collect(Collectors.toList()),
         event.getActivity().getIdentifier(), event.getActivity().getLabel());
   }
diff --git a/eraser.spark/src/main/resources/log4j2.xml b/eraser.spark/src/main/resources/log4j2.xml
index 0594576fac98ba859e411597c90c8e3d989378bd..867ec439d0a32dcb5f8b3e2d0c7485d7d8da418c 100644
--- a/eraser.spark/src/main/resources/log4j2.xml
+++ b/eraser.spark/src/main/resources/log4j2.xml
@@ -4,8 +4,8 @@
         <Console name="Console">
             <PatternLayout pattern="%highlight{%d{HH:mm:ss.SSS} %-5level} %c{1.} - %msg%n"/>
         </Console>
-        <RollingFile name="RollingFile" fileName="logs/jastadd-mquat.log"
-                    filePattern="logs/jastadd-mquat-%i.log">
+        <RollingFile name="RollingFile" fileName="logs/eraser.log"
+                    filePattern="logs/eraser-%i.log">
             <PatternLayout pattern="%d{HH:mm:ss.SSS} %-5level %logger{36} - %msg%n"/>
             <Policies>
                 <OnStartupTriggeringPolicy/>
diff --git a/eraser.starter/.gitignore b/eraser.starter/.gitignore
index 84c048a73cc2e5dd24f807669eb99b0ce3123195..54c5c0b024dab04da2a43d4135ed67ed2a670b46 100644
--- a/eraser.starter/.gitignore
+++ b/eraser.starter/.gitignore
@@ -1 +1,2 @@
 /build/
+logs/
diff --git a/eraser.starter/build.gradle b/eraser.starter/build.gradle
index fe1e38b9e989fbf38d0ddfa58068b6c78264d97a..186e75a1e448ab9f0e61c5f8df2d3ac94da2f1f9 100644
--- a/eraser.starter/build.gradle
+++ b/eraser.starter/build.gradle
@@ -1,11 +1,9 @@
-//buildscript {
-//    repositories {
-//        mavenCentral()
-//    }
-//    dependencies {
-//        classpath("org.springframework.boot:spring-boot-gradle-plugin:2.1.2.RELEASE")
-//    }
-//}
+plugins {
+    id 'java'
+    id 'application'
+    id 'distribution'
+    id 'io.github.http-builder-ng.http-plugin' version '0.1.1'
+}
 
 repositories {
     mavenCentral()
@@ -13,37 +11,39 @@ repositories {
 
 sourceCompatibility = 1.8
 
-apply plugin: 'java'
-apply plugin: 'application'
-//apply plugin: 'org.springframework.boot'
-//apply plugin: 'io.spring.dependency-management'
-
 dependencies {
     compile project(':eraser-base')
-//    compile project(':eraser.rest')
     compile project(':eraser.spark')
     compile project(':feedbackloop.api')
     compile project(':feedbackloop.analyze')
     compile project(':feedbackloop.plan')
     compile project(':feedbackloop.execute')
-//    compile 'org.springframework.boot:spring-boot-starter-web'
-    compile 'com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:2.9.8'
-    compile 'com.fasterxml.jackson.core:jackson-databind:2.9.8'
-//    compile 'org.apache.commons:commons-lang3:8.1'
+    compile project(':feedbackloop.learner_backup')
+    compile project(':datasets')
+    compile group: 'com.fasterxml.jackson.dataformat', name: 'jackson-dataformat-yaml', version: '2.9.8'
     compile group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: '2.9.8'
-    compile group: 'org.apache.logging.log4j', name: 'log4j-api', version: '2.11.1'
-    compile group: 'org.apache.logging.log4j', name: 'log4j-core', version: '2.11.1'
-    compile 'net.sourceforge.argparse4j:argparse4j:0.8.1'
-    testCompile group: 'junit', name: 'junit', version: '4.12'
-    testCompile group: 'org.hamcrest', name: 'hamcrest-junit', version: '2.0.0.0'
+    compile group: 'net.sourceforge.argparse4j', name: 'argparse4j', version: '0.8.1'
+    //    compile project(':feedbackloop.learner')
+
 }
 
 run {
     mainClassName = 'de.tudresden.inf.st.eraser.starter.EraserStarter'
     standardInput = System.in
-//    if (project.hasProperty("appArgs")) {
-//        args Eval.me(appArgs)
-//    }
+}
+
+import io.github.httpbuilderng.http.HttpTask
+
+task shutdown (type: HttpTask) {
+    group = 'application'
+    description = 'Shuts down a running eraser application'
+
+    config {
+        request.uri = 'http://localhost:4567'
+    }
+    post {
+        request.uri.path = '/system/exit'
+    }
 }
 
 sourceSets {
@@ -53,3 +53,19 @@ sourceSets {
         }
     }
 }
+
+//distributions {
+//    main {
+//        contents {
+//            from {
+//                'src/main/resources/starter.eraser'
+//            }
+//        }
+//    }
+//}
+applicationDistribution.from("src/main/resources") {
+    include "starter.eraser"
+}
+applicationDistribution.from(".") {
+    include "starter-setting.yaml"
+}
diff --git a/eraser.starter/src/main/java/de/tudresden/inf/st/eraser/starter/EraserStarter.java b/eraser.starter/src/main/java/de/tudresden/inf/st/eraser/starter/EraserStarter.java
index ec147f0bb07e89363bec552207e5b80a418a83c9..acc4b93a471d0b3a02676d37cb0314c85756ed1c 100644
--- a/eraser.starter/src/main/java/de/tudresden/inf/st/eraser/starter/EraserStarter.java
+++ b/eraser.starter/src/main/java/de/tudresden/inf/st/eraser/starter/EraserStarter.java
@@ -8,11 +8,18 @@ import de.tudresden.inf.st.eraser.feedbackloop.api.Analyze;
 import de.tudresden.inf.st.eraser.feedbackloop.api.Execute;
 import de.tudresden.inf.st.eraser.feedbackloop.api.Plan;
 import de.tudresden.inf.st.eraser.feedbackloop.execute.ExecuteImpl;
+import de.tudresden.inf.st.eraser.feedbackloop.learner_backup.Learner;
+import de.tudresden.inf.st.eraser.feedbackloop.learner_backup.MachineLearningImpl;
 import de.tudresden.inf.st.eraser.feedbackloop.plan.PlanImpl;
 import de.tudresden.inf.st.eraser.jastadd.model.*;
 import de.tudresden.inf.st.eraser.openhab2.OpenHab2Importer;
+import de.tudresden.inf.st.eraser.openhab2.mqtt.MQTTUpdater;
 import de.tudresden.inf.st.eraser.spark.Application;
 import de.tudresden.inf.st.eraser.util.ParserUtils;
+import net.sourceforge.argparse4j.ArgumentParsers;
+import net.sourceforge.argparse4j.annotation.Arg;
+import net.sourceforge.argparse4j.inf.ArgumentParser;
+import net.sourceforge.argparse4j.inf.ArgumentParserException;
 import org.apache.logging.log4j.LogManager;
 import org.apache.logging.log4j.Logger;
 
@@ -20,6 +27,8 @@ import java.io.File;
 import java.io.IOException;
 import java.net.MalformedURLException;
 import java.net.URL;
+import java.util.ArrayList;
+import java.util.List;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.locks.Condition;
 import java.util.concurrent.locks.Lock;
@@ -36,15 +45,30 @@ import java.util.concurrent.locks.ReentrantLock;
  */
 public class EraserStarter {
 
+  private static class CommandLineOptions {
+    @Arg(dest = "config_file")
+    String configFile;
+  }
+
   private static final Logger logger = LogManager.getLogger(EraserStarter.class);
-//  private static final String NO_MAPE = "no_mape";
-//  public static final String NO_REST = "no_rest";
 
   @SuppressWarnings("ResultOfMethodCallIgnored")
   public static void main(String[] args) {
-    logger.info("Starting ERASER");
+    ArgumentParser parser = ArgumentParsers.newFor("eraser").build()
+        .defaultHelp(true)
+        .description("Starts the knowledge-base of OpenLicht");
+    parser.addArgument("-f", "--config-file")
+        .help("Path to the configuration YAML file")
+        .setDefault("starter-setting.yaml");
+    CommandLineOptions commandLineOptions = new CommandLineOptions();
+    try {
+      parser.parseArgs(args, commandLineOptions);
+    } catch (ArgumentParserException e) {
+      parser.handleError(e);
+      System.exit(1);
+    }
     ObjectMapper mapper = new ObjectMapper(new YAMLFactory());
-    File settingsFile = new File("starter-setting.yaml");
+    File settingsFile = new File(commandLineOptions.configFile);
     Setting settings;
     try {
       settings = mapper.readValue(settingsFile, Setting.class);
@@ -54,44 +78,145 @@ public class EraserStarter {
       System.exit(1);
       return;
     }
-    String openhabUrl = settings.openHabUrl;
-    String file = settings.file;
+    logger.info("Starting ERASER");
     boolean startRest = settings.rest.use;
 
-    logger.info("openhab url: {}, file: {}", openhabUrl, file);
+    Root root;
+    OpenHAB2Model model;
+    switch (settings.initModelWith) {
+      case openhab:
+        OpenHab2Importer importer = new OpenHab2Importer();
+        try {
+          model = importer.importFrom(new URL(settings.openhab.url));
+          root = model.getRoot();
+        } catch (MalformedURLException e) {
+          logger.error("Could not parse URL {}", settings.openhab.url);
+          logger.catching(e);
+          System.exit(1);
+          return;
+        }
+        logger.info("Imported model {}", model.description());
+        break;
+      case load:
+      default:
+        try {
+          root = ParserUtils.load(settings.load.realURL());
+          model = root.getOpenHAB2Model();
+        } catch (IOException | Parser.Exception e) {
+          logger.error("Problems parsing the given file {}", settings.load.file);
+          logger.catching(e);
+          System.exit(1);
+          return;
+        }
+    }
 
-    if (openhabUrl != null && file != null) {
-      logger.error("Both openhab url and file were set, can't process both. " +
-          "OpenHAB synchronization is done if MQTT url is set in the processed file. Exiting.");
-      System.exit(1);
+    // initialize backup learner
+    Learner learner = new Learner();
+
+    // initialize activity recognition
+    MachineLearningRoot machineLearningRoot = root.getMachineLearningRoot();
+    if (settings.activity.dummy) {
+      logger.info("Using dummy activity recognition, ignoring other settings for this");
+      machineLearningRoot.setActivityRecognition(DummyMachineLearningModel.createDefault());
+    } else {
+      MachineLearningImpl handler = new MachineLearningImpl(learner, MachineLearningImpl.GOAL_ACTIVITY_PHONE_AND_WATCH);
+      handler.setKnowledgeBaseRoot(root);
+      logger.info("Reading activity recognition from csv file {}", settings.activity.file);
+      handler.initActivities(settings.activity.realURL().getFile());
+      ExternalMachineLearningModel machineLearningModel = new ExternalMachineLearningModel();
+      machineLearningModel.setEncoder(handler);
+      machineLearningModel.setDecoder(handler);
+      root.getMachineLearningRoot().setActivityRecognition(machineLearningModel);
+
+      //Begin the Integration
+      Item item1=model.resolveItem("m_accel_x").get();
+      Item item2=model.resolveItem("m_accel_y").get();
+      Item item3=model.resolveItem("m_accel_z").get();
+      Item item4=model.resolveItem("m_rotation_x").get();
+      Item item5=model.resolveItem("m_rotation_y").get();
+      Item item6=model.resolveItem("m_rotation_z").get();
+      Item item7=model.resolveItem("w_accel_x").get();
+      Item item8=model.resolveItem("w_accel_y").get();
+      Item item9=model.resolveItem("w_accel_z").get();
+      Item item10=model.resolveItem("w_rotation_x").get();
+      Item item11=model.resolveItem("w_rotation_y").get();
+      Item item12=model.resolveItem("w_rotation_z").get();
+      //0.2717419,8.698134,4.471172,0.043741,0.515962,0.854318,1.8818425,4.9320555,8.145074,0.2374878,-0.032836914,0.3381958,working
+      item1.setStateFromString("0.2717419");
+      item2.setStateFromString("8.698134");
+      item3.setStateFromString("4.471172");
+      item4.setStateFromString("0.043741");
+      item5.setStateFromString("0.515962");
+      item6.setStateFromString("0.854318");
+      item7.setStateFromString("1.8818425");
+      item8.setStateFromString("4.9320555");
+      item9.setStateFromString("8.145074");
+      item10.setStateFromString("0.2374878");
+      item11.setStateFromString("-0.032836914");
+      item12.setStateFromString("0.3381958");
+
+      ArrayList<Item> newData=new ArrayList<>();
+      newData.add(item1);
+      newData.add(item2);
+      newData.add(item3);
+      newData.add(item4);
+      newData.add(item5);
+      newData.add(item6);
+      newData.add(item7);
+      newData.add(item8);
+      newData.add(item9);
+      newData.add(item10);
+      newData.add(item11);
+      newData.add(item12);
+      handler.newData(newData);
+      List<ItemPreference> preference=handler.classify().getPreferences();
+      for(ItemPreference preference1 : preference){
+        preference1.apply();
+      }
+    }
+
+    // initialize preference learning
+    if (settings.preference.dummy) {
+      logger.info("Using dummy preference learning, ignoring other settings for this");
+      machineLearningRoot.setPreferenceLearning(DummyMachineLearningModel.createDefault());
+    } else {
+      logger.info("Reading preference learning from csv file {}", settings.preference.file);
+      MachineLearningImpl handler = new MachineLearningImpl(learner, MachineLearningImpl.GOAL_PREFERENCE_BRIGHTNESS_IRIS);
+      handler.setKnowledgeBaseRoot(root);
+      handler.initPreferences(settings.preference.realURL().getFile());
+      ExternalMachineLearningModel machineLearningModel = new ExternalMachineLearningModel();
+      machineLearningModel.setEncoder(handler);
+      machineLearningModel.setDecoder(handler);
+      root.getMachineLearningRoot().setPreferenceLearning(machineLearningModel);
+      //working,medium,240,70
+      Item activity_item = model.resolveItem("activity").get();
+      String activity=activity_item.getStateAsString();
+      activity_item.setStateFromString(activity);
+      Item brightness_item = model.resolveItem("w_brightness").get();
+      brightness_item.setStateFromString("medium");
+      ArrayList<Item> newData1 = new ArrayList<>();
+      newData1.add(activity_item);
+      newData1.add(brightness_item);
+      handler.newData(newData1);
+      List<ItemPreference> preference=handler.classify().getPreferences();
+      for(ItemPreference preference1 : preference){
+        preference1.apply();
+      }
     }
-    if (openhabUrl == null && file == null) {
-      logger.error("Neither URL for openHAB import nor a file was given, can't start uninitialized. Exiting.");
+
+    machineLearningRoot.getPreferenceLearning().connectItems(settings.preference.items);
+    if (!machineLearningRoot.getActivityRecognition().check()) {
+      logger.fatal("Invalid activity recognition!");
       System.exit(1);
     }
-    Root model;
-    if (openhabUrl != null) {
-      OpenHab2Importer importer = new OpenHab2Importer();
-      try {
-        model = importer.importFrom(new URL(openhabUrl));
-      } catch (MalformedURLException e) {
-        logger.error("Could not parse URL {}", openhabUrl);
-        logger.catching(e);
-        System.exit(1);
-        return;
-      }
-      logger.info("Imported model {}", model.description());
-    } else { // file != null
-      try {
-        model = ParserUtils.load(file);
-      } catch (IOException | Parser.Exception e) {
-        logger.error("Problems parsing the given file {}", file);
-        logger.catching(e);
-        System.exit(1);
-        return;
-      }
+    if (!machineLearningRoot.getPreferenceLearning().check()) {
+      logger.fatal("Invalid preference learning!");
+      System.exit(1);
     }
 
+    Lock lock = new ReentrantLock();
+    Condition quitCondition = lock.newCondition();
+
     Analyze analyze = null;
     if (settings.useMAPE) {
       // configure and start mape loop
@@ -103,85 +228,80 @@ public class EraserStarter {
       analyze.setPlan(plan);
       plan.setExecute(execute);
 
-      analyze.setKnowledgeBase(model);
-      plan.setKnowledgeBase(model);
-      execute.setKnowledgeBase(model);
+      analyze.setKnowledgeBase(root);
+      plan.setKnowledgeBase(root);
+      execute.setKnowledgeBase(root);
 
       analyze.startAsThread(1, TimeUnit.SECONDS);
-      if (!startRest) {
-        // alternative exit condition
-        System.out.println("Hit [Enter] to exit");
-        try {
-          System.in.read();
-        } catch (IOException e) {
-          e.printStackTrace();
-        }
-
-        System.out.println("Stopping...");
-        analyze.stop();
-      }
     } else {
       logger.info("No MAPE loop this time");
     }
 
+    if (settings.mqttUpdate) {
+      logger.info("Starting MQTT updater");
+      Thread t = new Thread(() -> {
+        try (MQTTUpdater updater = new MQTTUpdater(root)) {
+          updater.start();
+          updater.waitUntilReady(5, TimeUnit.SECONDS);
+          lock.lock();
+          quitCondition.await();
+        } catch (IOException | InterruptedException e) {
+          logger.catching(e);
+        } finally {
+          lock.unlock();
+        }
+        logger.info("MQTT update stopped");
+      }, "MQTT-Updater");
+      t.setDaemon(true);
+      t.start();
+    }
+
     if (startRest) {
       // start REST-API in new thread
       logger.info("Starting REST server");
-//      Thread t = new Thread(new ThreadGroup("REST-API"), () -> SpringApplication.run(Application.class, new String[0]));
-//      Thread t = new Thread(new ThreadGroup("REST-API"), () -> Application.main(new String[0]));
-      Lock lock = new ReentrantLock();
-      Condition quitCondition = lock.newCondition();
-      Thread t = new Thread(new ThreadGroup("REST-API"),
-          () -> Application.start(settings.rest.port, model, settings.rest.createDummyMLData, lock, quitCondition));
+      Thread t = new Thread(
+          () -> Application.start(settings.rest.port, root, settings.rest.createDummyMLData, lock, quitCondition),
+          "REST-API");
       t.setDaemon(true);
       t.start();
-      logger.info("Waiting until request is send to '/exit'");
+      logger.info("Waiting until POST request is send to 'http://localhost:{}/system/exit'", settings.rest.port);
       try {
         lock.lock();
-        quitCondition.await();
+        if (t.isAlive()) {
+          quitCondition.await();
+        }
       } catch (InterruptedException e) {
         logger.warn("Waiting was interrupted");
       } finally {
         lock.unlock();
       }
-      if (analyze != null) {
-        analyze.stop();
-      }
     } else {
       logger.info("No REST server this time");
+      System.out.println("Hit [Enter] to exit");
+      try {
+        System.in.read();
+      } catch (IOException e) {
+        e.printStackTrace();
+      }
+      System.out.println("Stopping...");
+      try {
+        lock.lock();
+        quitCondition.signalAll();
+      } finally {
+        lock.unlock();
+      }
+    }
+    if (analyze != null) {
+      analyze.stop();
+    }
+    InfluxAdapter influxAdapter = root.getInfluxRoot().influxAdapter();
+    if (influxAdapter != null) {
+      try {
+        influxAdapter.close();
+      } catch (Exception e) {
+        logger.catching(e);
+      }
     }
     logger.info("I'm done here.");
   }
-
-  /*
-   ** Using argument parsing **
-
-    ArgumentParser parser = ArgumentParsers.newFor("Eraser starter").build()
-      .defaultHelp(true)
-      .description("This Starter combines and starts all modules. This includes:\n" +
-          "  - Knowledge-Base in eraser-base\n" +
-          "  - Feedback loop in feedbackloop.{analyze,plan,execute}\n" +
-          "  - REST-API in eraser-rest");
-    parser.addArgument("-o", "--openhab")
-        .metavar("URL")
-        .help("Initialize with openHAB at the given URL");
-    parser.addArgument("--no-mape")
-        .action(Arguments.storeTrue())
-        .setDefault(false)
-        .dest(NO_MAPE)
-        .help("Disable the feedback loop");
-    parser.addArgument("--no-rest")
-        .action(Arguments.storeTrue())
-        .setDefault(false)
-        .dest(NO_REST)
-        .help("Disable the REST-API");
-    parser.addArgument("-f", "--file")
-        .help("*.eraser file to initialize knowledge base");
-    Namespace ns = parser.parseArgsOrFail(args);
-
-    String openhabUrl = ns.getString("openhab");
-    String file = ns.getString("file");
-    boolean initializeMape = !ns.getBoolean(NO_MAPE);
-    boolean startRest = !ns.getBoolean(NO_REST);
-   */
 }
diff --git a/eraser.starter/src/main/java/de/tudresden/inf/st/eraser/starter/Setting.java b/eraser.starter/src/main/java/de/tudresden/inf/st/eraser/starter/Setting.java
index f4a1d1538da6b32cca5f889b728bf8847365c567..09af126b0a66f54aa2b6c88fb04bda9cb7218cb3 100644
--- a/eraser.starter/src/main/java/de/tudresden/inf/st/eraser/starter/Setting.java
+++ b/eraser.starter/src/main/java/de/tudresden/inf/st/eraser/starter/Setting.java
@@ -1,18 +1,86 @@
 package de.tudresden.inf.st.eraser.starter;
 
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.nio.file.Paths;
+import java.util.List;
+
 /**
  * Setting bean.
  *
  * @author rschoene - Initial contribution
  */
+@SuppressWarnings("WeakerAccess")
 class Setting {
+  private final static Logger logger = LogManager.getLogger(Setting.class);
   public class Rest {
+    /** Start the REST server. Default: true. */
     public boolean use = true;
+    /** Port of the REST server. Default: 4567. */
     public int port = 4567;
+    /** Add some dummy data for activities and events. Only effective when using the REST server. Default: false. */
     public boolean createDummyMLData = false;
   }
+  public class FileContainer {
+    public String file;
+    /** Whether the file is external (not shipped with the JAR). Default: false. */
+    public boolean external = false;
+    /** Get the URL to the {@link #file} */
+    URL realURL() {
+      if (external) {
+        try {
+          return Paths.get(file).toUri().normalize().toURL();
+        } catch (MalformedURLException e) {
+          logger.catching(e);
+          return null;
+        }
+      } else {
+        return Setting.class.getClassLoader().getResource(file);
+      }
+    }
+  }
+  public class MLContainer extends FileContainer {
+    /** Use dummy model in which the current activity is directly editable. Default: false. */
+    public boolean dummy = false;
+    /** Model id. Default: 1.*/
+    public int id = 1;
+    /** Items to connect to inputs */
+    public List<String> items;
+    /** Item to change with classification result */
+    public String affectedItem;
+  }
+  public class OpenHabContainer {
+    /** The URL from which to import and at which openHAB is running */
+    public String url;
+    /** The metadata namespace for items */
+    public String metadataNamespace;
+  }
+  public enum InitModelWith {
+    /** Load the initial model from a file */
+    load,
+    /** Load the initial model by importing from openHAB */
+    openhab
+  }
   public Rest rest;
+  /** Start the feedback loop. Default: true. */
   public boolean useMAPE = true;
-  public String file;
-  public String openHabUrl;
+  /** Initialize the knowledge base with a file.
+   OpenHAB synchronization is done if MQTT url is set in the processed file.
+   <br>
+   child "file": File to read in. Expected format = eraser
+   */
+  public FileContainer load;
+  /** Model for activity recognition. If dummy is true, then the file parameter is ignored. */
+  public MLContainer activity;
+  /** Model for preference learning. If dummy is true, then the file parameter is ignored. */
+  public MLContainer preference;
+  /** Initialize the knowledge base by importing data from openHAB. */
+  public OpenHabContainer openhab;
+  /** Get updates from openHAB into the knowledge base. Default: true. */
+  public boolean mqttUpdate = true;
+  /** Method to initialize model. Possible values: "load", "openhab". Default: "load". */
+  public InitModelWith initModelWith = InitModelWith.load;
 }
diff --git a/eraser.starter/src/main/resources/log4j2.xml b/eraser.starter/src/main/resources/log4j2.xml
index 0594576fac98ba859e411597c90c8e3d989378bd..900e8c305d176e82d8f8a3c6beb1e96acfe186a8 100644
--- a/eraser.starter/src/main/resources/log4j2.xml
+++ b/eraser.starter/src/main/resources/log4j2.xml
@@ -4,8 +4,8 @@
         <Console name="Console">
             <PatternLayout pattern="%highlight{%d{HH:mm:ss.SSS} %-5level} %c{1.} - %msg%n"/>
         </Console>
-        <RollingFile name="RollingFile" fileName="logs/jastadd-mquat.log"
-                    filePattern="logs/jastadd-mquat-%i.log">
+        <RollingFile name="RollingFile" fileName="logs/eraser.log"
+                    filePattern="logs/eraser-%i.log">
             <PatternLayout pattern="%d{HH:mm:ss.SSS} %-5level %logger{36} - %msg%n"/>
             <Policies>
                 <OnStartupTriggeringPolicy/>
@@ -18,5 +18,9 @@
             <AppenderRef ref="Console"/>
             <AppenderRef ref="RollingFile"/>
         </Root>
+        <Logger name="org.eclipse.jetty" level="info" additivity="false">
+            <AppenderRef ref="Console"/>
+            <AppenderRef ref="RollingFile"/>
+        </Logger>
     </Loggers>
 </Configuration>
diff --git a/eraser.starter/src/main/resources/preference.eg b/eraser.starter/src/main/resources/preference.eg
new file mode 100644
index 0000000000000000000000000000000000000000..9c5e508c6466ebacdce5ea905bb60fd23ed499bd
--- /dev/null
+++ b/eraser.starter/src/main/resources/preference.eg
@@ -0,0 +1,24 @@
+encog,BasicNetwork,java,3.4.0,1,1548842414446
+[BASIC]
+[BASIC:PARAMS]
+[BASIC:NETWORK]
+beginTraining=0
+connectionLimit=0
+contextTargetOffset=0,0,0
+contextTargetSize=0,0,0
+endTraining=2
+hasContext=f
+inputCount=4
+layerCounts=4,8,5
+layerFeedCounts=4,7,4
+layerContextCount=0,0,0
+layerIndex=0,4,12
+output=0.5121833792,-0.9601322536,-0.8758172535,-0.9979697503,0.9622664202,-0.9999734797,0.1592770336,0.799331477,-0.4541506621,0.9665504755,0.9965543485,1,0,0.9333333333,-0.2727272727,0.3898305085,1
+outputCount=4
+weightIndex=0,32,67
+weights=-1.0786840029,-2.2924792202,-1.9930859783,1.7958491576,0.1959961268,-2.0543645855,0.0362033078,0.2318024444,0.6379648215,0.6228614294,0.6327962466,1.4938586483,2.7154918438,-0.6821478373,-0.5520417752,-0.790812791,-0.6920871265,1.9974666857,2.3298834154,0.4485384863,-2.6899092835,0.031268515,-0.5795866919,-0.0980217125,-0.484143335,0.1511539927,0.420087673,-2.5050298291,0.2594955046,-1.095053346,1.3237034623,-1.0362683252,-1.4462851652,0.3753510899,-1.601258201,2.069584469,0.3818308747,1.9103650822,-1.0462844546,17.7814469175,0.6795954934,-0.0542764038,0.9582859267,0.3301209469,0.1352880767,0.0400876451,-0.1261986993,0.5055884018,0.6447792243,2.3674963793,1.1415287221,0.6956421989,0.3954445374,-0.0968051838,-5.3921438906,-1.0800675361,-1.4491070517,0.299983963,0.4432921269,-0.1229873384,0.3763334954,1.4430117456,2.3128771304,2.6904772398,-0.2334282302,2.656365234,-0.429273975
+biasActivation=0,1,1
+[BASIC:ACTIVATION]
+"org.encog.engine.network.activation.ActivationTANH"
+"org.encog.engine.network.activation.ActivationTANH"
+"org.encog.engine.network.activation.ActivationLinear"
diff --git a/eraser.starter/src/main/resources/starter.eraser b/eraser.starter/src/main/resources/starter.eraser
new file mode 100644
index 0000000000000000000000000000000000000000..a8dc0c5731c9f5ce106c55aa4d2a87fc8f85df4f
--- /dev/null
+++ b/eraser.starter/src/main/resources/starter.eraser
@@ -0,0 +1,40 @@
+Color Item: id="iris1_item" label="Iris 1" state="121,88,68" topic="iris1_item";
+Number Item: id="datetime_month" label="Month" state="1" topic="datetime_month";
+Number Item: id="datetime_day" label="Day" state="31" topic="datetime_day";
+Number Item: id="datetime_hour" label="Hour" state="13" topic="datetime_hour";
+Number Item: id="datetime_minute" label="Minute" state="37" topic="datetime_minute";
+Number Item: id="bias" label="bias item" state="1";
+
+
+String Item: id="m_accel_x" label="" state="0.2717419" topic="m_accel_x";
+String Item: id="m_accel_y" label="" state="8.698134" topic="m_accel_y";
+String Item: id="m_accel_z" label="" state="4.471172" topic="m_accel_z";
+String Item: id="m_rotation_x" label="" state="0.043741" topic="m_rotation_x";
+String Item: id="m_rotation_y" label="" state="0.515962" topic="m_rotation_y";
+String Item: id="m_rotation_z" label="" state="0.854318" topic="m_rotation_z";
+String Item: id="w_accel_x" label="" state="1.8818425" topic="w_accel_x";
+String Item: id="w_accel_y" label="" state="4.9320555" topic="w_accel_y";
+String Item: id="w_accel_z" label="" state="8.145074" topic="w_accel_z";
+String Item: id="w_rotation_x" label="" state="0.2374878" topic="w_rotation_x";
+String Item: id="w_rotation_y" label="" state="-0.032836914" topic="w_rotation_y";
+String Item: id="w_rotation_z" label="" state="0.3381958" topic="w_rotation_y";
+String Item: id="w_brightness" label="" state="bright" topic="w_brightness";
+//String Item: id="activity" label="" state="lying" topic="activity";
+Activity Item: id="activity" topic="activity";
+
+Group: id="Lights" items=["iris1_item"];
+Group: id="Datetime" items=["datetime_month", "datetime_day", "datetime_hour", "datetime_minute"];
+
+Mqtt: incoming="oh2/out/" outgoing="oh2/in/" host="localhost:1883" ;
+//Mqtt: incoming="oh2/out/" outgoing="oh2/in/" host="192.168.1.250" ;
+
+Influx: host="172.22.1.152" ;
+//"working", "walking", "dancing", "lying", "getting up", "reading"
+ML: activities={
+  0: "working",
+  1: "walking",
+  2: "dancing",
+  3: "lying",
+  4: "getting up",
+  5: "reading"
+} ;
diff --git a/eraser.starter/starter-setting.yaml b/eraser.starter/starter-setting.yaml
index 0f2b6b8175e0518d2f66feca543e117383765d0a..e8f438cf3eeb22d71d44c675e7328e61ba612562 100644
--- a/eraser.starter/starter-setting.yaml
+++ b/eraser.starter/starter-setting.yaml
@@ -1,18 +1,64 @@
+# Settings for Eraser. Files are relative to the "resources" directory of this module.
+
 # Start the feedback loop. Default: true.
 useMAPE: true
 
 rest:
   # Start the REST server. Default: true.
   use: true
-
   # Port of the REST server. Default: 4567.
   port: 4567
-
   # Add some dummy data for activities and events. Only effective when using the REST server. Default: false.
-  createDummyMLData: true
+  createDummyMLData: false
+
+# Initialize the knowledge base with a file.
+# OpenHAB synchronization is done if MQTT url is set in the processed file
+load:
+  # File to read in. Expected format = eraser
+#  # Option 1: Use built-in file
+#  file: starter.eraser
+#  external: false
+  # Option 2: Use external file
+  file: src/main/resources/starter.eraser
+  external: true
+
+# Model for activity recognition. If dummy is true, then the file parameter is ignored.
+activity:
+  # File to read in. Expected format = csv   /Users/boqi/eraser/datasets
+  file: ../datasets/backup/activity_data.csv
+  external: true
+  # Use dummy model in which the current activity is directly editable. Default: false.
+  dummy: false
+  # Model id. Default: 1.
+  id: 1
+  # Item to change with classification result
+  affectedItem: activity
+
+# Model for preference learning. If dummy is true, then the file parameter is ignored.
+preference:
+  # File to read in. Expected format = csv
+  file: ../datasets/backup/preference_data.csv
+  external: true
+  # Use dummy model in which the current activity is directly editable. Default: false.
+  dummy: false
+  # Model id. Default: 1.
+  id: 1
+  # Items to connect to inputs
+  items:
+    - activity
+    - w_brightness
+  # Item to change with classification result
+  affectedItem: iris1_item
+
+# Initialize the knowledge base by importing data from openHAB.
+openhab:
+  # The URL from which to import and at which openHAB is running
+  url: "127.0.0.1:8080"
+  # The metadata namespace for items
+  metadataNamespace: fuenf
 
-# Initialize the knowledge base with a file. No default.
-file: /data/git/eraser/eraser.starter/src/main/resources/skywriter-hue.eraser
+# Get updates from openHAB into the knowledge base. Default: true.
+mqttUpdate: true
 
-# Initialize the knowledge base by importing data from openHAB. No default.
-#openHabUrl: "127.0.0.1:48080"
+# Method to initialize model. Possible values: "load", "openhab". Default: "load".
+initModelWith: load
diff --git a/feedbackloop.analyze/.gitignore b/feedbackloop.analyze/.gitignore
index 83ccc54d0286590d31587a71aaa789e780bff41e..70b583e34c3316bcd77c807e2d6b85db5e7d49f6 100644
--- a/feedbackloop.analyze/.gitignore
+++ b/feedbackloop.analyze/.gitignore
@@ -1,2 +1,3 @@
 /build/
 /bin/
+logs/
diff --git a/feedbackloop.analyze/build.gradle b/feedbackloop.analyze/build.gradle
index d14ed3e7311d54cdaec24165733da7a7caff7115..361f5c1429190208b8a70cbb889bf6c1cbd56993 100644
--- a/feedbackloop.analyze/build.gradle
+++ b/feedbackloop.analyze/build.gradle
@@ -1,28 +1,6 @@
-repositories {
-    mavenCentral()
-}
-
-sourceCompatibility = 1.8
-
-apply plugin: 'java'
-apply plugin: 'application'
-
 dependencies {
     compile project(':eraser-base')
     compile project(':feedbackloop.api')
-    compile group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: '2.9.8'
-    compile group: 'org.apache.logging.log4j', name: 'log4j-api', version: '2.11.1'
-    compile group: 'org.apache.logging.log4j', name: 'log4j-core', version: '2.11.1'
-    testCompile group: 'junit', name: 'junit', version: '4.12'
-    testCompile group: 'org.hamcrest', name: 'hamcrest-junit', version: '2.0.0.0'
-}
-
-run {
-    mainClassName = 'de.tudresden.inf.st.eraser.feedbackloop.analyze.Main'
-    standardInput = System.in
-    if (project.hasProperty("appArgs")) {
-        args Eval.me(appArgs)
-    }
 }
 
 sourceSets {
diff --git a/feedbackloop.analyze/src/main/java/de/tudresden/inf/st/eraser/feedbackloop/analyze/AnalyzeImpl.java b/feedbackloop.analyze/src/main/java/de/tudresden/inf/st/eraser/feedbackloop/analyze/AnalyzeImpl.java
index e09c4e474c9861f126a098f3a49d1986cfc46d0d..5e9f8cfb5476ae25219cebd704e17e2e2b78cb9d 100644
--- a/feedbackloop.analyze/src/main/java/de/tudresden/inf/st/eraser/feedbackloop/analyze/AnalyzeImpl.java
+++ b/feedbackloop.analyze/src/main/java/de/tudresden/inf/st/eraser/feedbackloop/analyze/AnalyzeImpl.java
@@ -2,6 +2,7 @@ package de.tudresden.inf.st.eraser.feedbackloop.analyze;
 
 import de.tudresden.inf.st.eraser.feedbackloop.api.Analyze;
 import de.tudresden.inf.st.eraser.feedbackloop.api.Plan;
+import de.tudresden.inf.st.eraser.jastadd.model.Activity;
 import de.tudresden.inf.st.eraser.jastadd.model.Root;
 import org.apache.logging.log4j.LogManager;
 import org.apache.logging.log4j.Logger;
@@ -15,11 +16,11 @@ public class AnalyzeImpl implements Analyze {
 
   private Root knowledgeBase;
   private Plan plan;
-  private String mostRecentActivity;
+  private Activity mostRecentActivity;
   private Logger logger = LogManager.getLogger(AnalyzeImpl.class);
 
   public AnalyzeImpl() {
-    this.mostRecentActivity = "";
+    this.mostRecentActivity = null;
   }
 
   @Override
@@ -39,13 +40,17 @@ public class AnalyzeImpl implements Analyze {
 
   @Override
   public void analyzeLatestChanges() {
-    // TODO change to "currentActivity" and track Activity objects, instead of Strings
-    String currentActivity = knowledgeBase.currentActivityName();
-    if (!currentActivity.equals(mostRecentActivity)) {
-      // new! inform plan!
-      logger.debug("Found new activity '{}'", currentActivity);
-      mostRecentActivity = currentActivity;
-      informPlan(currentActivity);
-    }
+    knowledgeBase.currentActivity().ifPresent(activity -> {
+      if (!activity.equals(mostRecentActivity)) {
+        // new! inform plan!
+        logger.info("Found new activity '{}'", activity.getLabel());
+        mostRecentActivity = activity;
+        try {
+          informPlan(activity);
+        } catch (Exception e) {
+          logger.catching(e);
+        }
+      }
+    });
   }
 }
diff --git a/feedbackloop.analyze/src/main/resources/log4j2.xml b/feedbackloop.analyze/src/main/resources/log4j2.xml
index 0594576fac98ba859e411597c90c8e3d989378bd..867ec439d0a32dcb5f8b3e2d0c7485d7d8da418c 100644
--- a/feedbackloop.analyze/src/main/resources/log4j2.xml
+++ b/feedbackloop.analyze/src/main/resources/log4j2.xml
@@ -4,8 +4,8 @@
         <Console name="Console">
             <PatternLayout pattern="%highlight{%d{HH:mm:ss.SSS} %-5level} %c{1.} - %msg%n"/>
         </Console>
-        <RollingFile name="RollingFile" fileName="logs/jastadd-mquat.log"
-                    filePattern="logs/jastadd-mquat-%i.log">
+        <RollingFile name="RollingFile" fileName="logs/eraser.log"
+                    filePattern="logs/eraser-%i.log">
             <PatternLayout pattern="%d{HH:mm:ss.SSS} %-5level %logger{36} - %msg%n"/>
             <Policies>
                 <OnStartupTriggeringPolicy/>
diff --git a/feedbackloop.api/.gitignore b/feedbackloop.api/.gitignore
index 83ccc54d0286590d31587a71aaa789e780bff41e..70b583e34c3316bcd77c807e2d6b85db5e7d49f6 100644
--- a/feedbackloop.api/.gitignore
+++ b/feedbackloop.api/.gitignore
@@ -1,2 +1,3 @@
 /build/
 /bin/
+logs/
diff --git a/feedbackloop.api/build.gradle b/feedbackloop.api/build.gradle
index 5264c7d2bc1f37c38a464fc021d9a0e9465a7998..ba194086c7c858a3435af764a8783f38231d2c69 100644
--- a/feedbackloop.api/build.gradle
+++ b/feedbackloop.api/build.gradle
@@ -1,19 +1,11 @@
-repositories {
-    mavenCentral()
+plugins {
+    id 'io.franzbecker.gradle-lombok' version '3.0.0'
 }
 
-sourceCompatibility = 1.8
-
-apply plugin: 'java'
-
 dependencies {
     compile project(':eraser-base')
-    compile project(':commons.color:')
-    compile group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: '2.9.8'
-    compile group: 'org.apache.logging.log4j', name: 'log4j-api', version: '2.11.1'
-    compile group: 'org.apache.logging.log4j', name: 'log4j-core', version: '2.11.1'
-    testCompile group: 'junit', name: 'junit', version: '4.12'
-    testCompile group: 'org.hamcrest', name: 'hamcrest-junit', version: '2.0.0.0'
+    compile project(':commons.color')
+    compile group: 'org.encog', name: 'encog-core', version: '3.4'
 }
 
 sourceSets {
diff --git a/feedbackloop.api/src/main/java/de/tudresden/inf/st/eraser/feedbackloop/api/Analyze.java b/feedbackloop.api/src/main/java/de/tudresden/inf/st/eraser/feedbackloop/api/Analyze.java
index bdc70275aeea34e8dae963bdd23ac840c651fef9..d590f1e45f98f2830e0dc645c79c563fc13737d3 100644
--- a/feedbackloop.api/src/main/java/de/tudresden/inf/st/eraser/feedbackloop/api/Analyze.java
+++ b/feedbackloop.api/src/main/java/de/tudresden/inf/st/eraser/feedbackloop/api/Analyze.java
@@ -1,5 +1,6 @@
 package de.tudresden.inf.st.eraser.feedbackloop.api;
 
+import de.tudresden.inf.st.eraser.jastadd.model.Activity;
 import de.tudresden.inf.st.eraser.jastadd.model.Root;
 
 import java.util.concurrent.Executors;
@@ -31,7 +32,7 @@ public interface Analyze {
     executorService.shutdownNow();
   }
 
-  default void informPlan(String activity) {
+  default void informPlan(Activity activity) {
     getPlan().planToMatchPreferences(activity);
   }
 }
diff --git a/feedbackloop.api/src/main/java/de/tudresden/inf/st/eraser/feedbackloop/api/EncogModel.java b/feedbackloop.api/src/main/java/de/tudresden/inf/st/eraser/feedbackloop/api/EncogModel.java
new file mode 100644
index 0000000000000000000000000000000000000000..c0bc0336ea49e810ac5930c30600e271e65c6861
--- /dev/null
+++ b/feedbackloop.api/src/main/java/de/tudresden/inf/st/eraser/feedbackloop/api/EncogModel.java
@@ -0,0 +1,44 @@
+package de.tudresden.inf.st.eraser.feedbackloop.api;
+
+import lombok.Getter;
+import lombok.Setter;
+import org.encog.neural.networks.layers.Layer;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * 
+ * This class represents an object that contains all information of a trained neural network.
+ * 
+ * For now: weights and the {@link Layer}s
+ * 
+ * @author Bierzyns - initial contribution
+ * */
+@Getter
+@Setter
+public class EncogModel {
+	/**
+	 * todo
+	 */
+	private String modelType;
+	private List<Double> weights;
+	private List<Layer> layers;
+
+	public EncogModel(String model) {
+		modelType = model;
+	}
+
+	public Layer getInputLayer() {
+		return Objects.requireNonNull(layers, "Layers not set yet").get(0);
+	}
+
+	public Layer getOutputLayer() {
+		return Objects.requireNonNull(layers, "Layers not set yet").get(layers.size() - 1);
+	}
+
+	public List<Layer> getHiddenLayers() {
+		return Objects.requireNonNull(layers, "Layers not set yet").subList(1, layers.size() - 1);
+	}
+}
diff --git a/feedbackloop.api/src/main/java/de/tudresden/inf/st/eraser/feedbackloop/api/Execute.java b/feedbackloop.api/src/main/java/de/tudresden/inf/st/eraser/feedbackloop/api/Execute.java
index dd923208037d76ec7e78d8b1652951fbec7bef50..c7ecb57d535b832d59d76d9840813a6d8a72ca2d 100644
--- a/feedbackloop.api/src/main/java/de/tudresden/inf/st/eraser/feedbackloop/api/Execute.java
+++ b/feedbackloop.api/src/main/java/de/tudresden/inf/st/eraser/feedbackloop/api/Execute.java
@@ -1,6 +1,5 @@
 package de.tudresden.inf.st.eraser.feedbackloop.api;
 
-import de.tudresden.inf.st.eraser.commons.color.ColorUtils;
 import de.tudresden.inf.st.eraser.commons.color.ColorUtils.RGBvalues;
 import de.tudresden.inf.st.eraser.jastadd.model.ItemPreference;
 import de.tudresden.inf.st.eraser.jastadd.model.Root;
@@ -16,9 +15,22 @@ import java.util.Map;
  */
 public interface Execute {
 
+  /**
+   * Setter for the knowledge base to use
+   * @param knowledgeBase the knowledge base to use
+   */
   void setKnowledgeBase(Root knowledgeBase);
 
+  /**
+   * <b>Deprecated</b>: Use {@link #updateItems(List)} instead.
+   * @param brightnessAndRgbForItems Map, keys are item names, values are RGB and brightness values
+   */
+  @Deprecated
   void updateItems(Map<String, Tuple<Integer, RGBvalues>> brightnessAndRgbForItems);
 
+  /**
+   * Updates items according to given preferences
+   * @param preferences tuples containing item and its new HSB value
+   */
   void updateItems(List<ItemPreference> preferences);
 }
diff --git a/feedbackloop.api/src/main/java/de/tudresden/inf/st/eraser/feedbackloop/api/Learner.java b/feedbackloop.api/src/main/java/de/tudresden/inf/st/eraser/feedbackloop/api/Learner.java
index 07855cd001fdcaaef3927c901ac9bab9bbe2007a..a08a6fc2a444b4a30fd4f234acfd4449c075d444 100644
--- a/feedbackloop.api/src/main/java/de/tudresden/inf/st/eraser/feedbackloop/api/Learner.java
+++ b/feedbackloop.api/src/main/java/de/tudresden/inf/st/eraser/feedbackloop/api/Learner.java
@@ -1,14 +1,18 @@
 package de.tudresden.inf.st.eraser.feedbackloop.api;
 
-import java.util.ArrayList;
+import java.io.File;
+import java.io.InputStream;
+import java.net.URL;
+import java.util.List;
+
+import org.encog.util.arrayutil.NormalizedField;
 
-import de.tudresden.inf.st.eraser.feedbackloop.api.model.Model;
 import de.tudresden.inf.st.eraser.jastadd.model.Root;
 
 /**
  * 
- * Learner which handles the training and retraining of algorithms 
- * and models used for activity recognition, preference learning and data pre-processing. 
+ * Learner which handles the training and retraining of neural networks 
+ * and models used for activity recognition and  preference learning.
  * 
  * @author Bierzyns - initial contribution
  * 
@@ -18,46 +22,114 @@ public interface Learner {
 	void setKnowledgeBase(Root knowledgeBase);
 	
 	/**
-	 * Method for loading data set, which used for initial training. This method exists only for development purposes. 
+	 * Method for loading data set, which used for initial training. This method exists only for development purposes. The csvFolderPath 
+	 * variable should be set to ensure that the data is read from the correct place.
 	 * 
-	 * @param dataSetName - name of data set that is loaded from the data set folder e.g. data1.arff (Weka)
+	 * @param dataSetName - name of data set that is loaded from the data set folder e.g. data1.csv
+	 * @param targetColumns - number of the columns that contain label of the corresponding csv data set
 	 * @param modelID - ID of the model that will be trained with this data set
 	 * @return true - data set loading was successful
 	 * */
-	boolean loadDataSet(String dataSetName, int modelID);
+	boolean loadDataSet(String dataSetName, List<Integer> targetColumns, int modelID);
+
+	/**
+	 * Method for loading a neural network from a file.
+	 * Please note that the normalizer are note loaded file , because it is assumed that the mins and maxes are saved anyway in the meta data of the data sets or items.
+	 *
+	 * @param file       file to load the model from
+	 * @param modelID - ID of the BasicNetwork.
+	 * @param inputMaxes - list that contains max values of all input columns (sensors) e.g. light intensity 100
+	 * @param inputMins - list that contains min values of all input columns (sensors) e.g. light intensity 0
+	 * @param targetMaxes - list that contains max values of all output columns (results) e.g. brigthness 100 for preference learning
+	 * @param targetMins - list that contains min values of all output columns (results) e.g. brigthness 0 for preference learning
+	 * @return true - model loading was successful
+	 * */
+	boolean loadModelFromFile(File file, int modelID, List<Integer> inputMaxes, List<Integer> inputMins, List<Integer> targetMaxes,
+	                          List<Integer> targetMins);
+
+	/**
+     * Method for loading a neural network from an input stream.
+     * Please note that the normalizer are note loaded file , because it is assumed that the mins and maxes are saved anyway in the meta data of the data sets or items.
+     *
+   	 * @param input       stream to load the model from
+     * @param modelID - ID of the BasicNetwork.
+     * @param inputMaxes - list that contains max values of all input columns (sensors) e.g. light intensity 100
+     * @param inputMins - list that contains min values of all input columns (sensors) e.g. light intensity 0
+     * @param targetMaxes - list that contains max values of all output columns (results) e.g. brigthness 100 for preference learning 
+     * @param targetMins - list that contains min values of all output columns (results) e.g. brigthness 0 for preference learning 
+     * @return true - model loading was successful
+     * */
+	boolean loadModelFromFile(InputStream input, int modelID, List<Integer> inputMaxes, List<Integer> inputMins, List<Integer> targetMaxes,
+	                          List<Integer> targetMins);
 	
 	/**
 	 * Method for the initial training of algorithms and models. That uses external data set for training.
 	 * 
-	 * @param modelID - ID of model that will be trained
+	 * @param inputCount - number of neurons in the input layer [here to simplify code reading]
+     * @param outputCount - number of neurons in the output layer [here to simplify code reading]
+     * @param hiddenCount -number of hidden layers in the network
+     * @param hiddenNeuronCount - number of neurons in the hidden layers for now 
+     * @param modelID - ID of the BasicNetwork.
+     * @param inputMaxes - list that contains max values of all input columns (sensors) e.g. light intensity 100
+     * @param inputMins - list that contains min values of all input columns (sensors) e.g. light intensity 0
+     * @param targetMaxes - list that contains max values of all output columns (results) e.g. brigthness 100 for preference learning 
+     * @param targetMins - list that contains min values of all output columns (results) e.g. brigthness 0 for preference learning 
+     * 
 	 * @return true - training of the model was successful started 
 	 * */
-	boolean train(int modelID);
+	boolean train(int inputCount, int outputCount, int hiddenCount, int hiddenNeuronCount, int modelID,
+            List<Integer> inputMaxes, List<Integer> inputMins, List<Integer> targetMaxes,
+            List<Integer> targetMins);
 	
 	/**
-	 * Method for the initial training of algorithms and models. That uses data set provided by Knowledge Base. Format of Data?
+	 * Method for the initial training of algorithms and models. That uses data set provided by Knowledge Base. 
 	 * 
-	 * @param set - data set on which the training is based on
-	 * @param modelID - ID of model that will be trained
+	 * @param data - data set on which the training is based on
+	 * @param inputCount - number of neurons in the input layer [here to simplify code reading]
+     * @param outputCount - number of neurons in the output layer [here to simplify code reading]
+     * @param hiddenCount - number of hidden layers in the network
+     * @param hiddenNeuronCount - number of neurons in the hidden layers for now 
+     * @param modelID - ID of the BasicNetwork.
+     * @param inputMaxes - list that contains max values of all input columns (sensors) e.g. light intensity 100
+     * @param inputMins - list that contains min values of all input columns (sensors) e.g. light intensity 0
+     * @param targetMaxes - list that contains max values of all output columns (results) e.g. brigthness 100 for preference learning 
+     * @param targetMins - list that contains min values of all output columns (results) e.g. brigthness 0 for preference learning 
 	 * @return true - training of the model was successful started 
 	 * */
-	boolean train(ArrayList<Integer> set, int modelID);
+	boolean train(double[][] data, int inputCount, int outputCount, int hiddenCount, int hiddenNeuronCount, int modelID,
+            List<Integer> inputMaxes, List<Integer> inputMins, List<Integer> targetMaxes,
+            List<Integer> targetMins, List<Integer> targetColumns);
 	
 	/**
-	 * Method for adapting existing models. Format of new data?
+	 * Method for adapting existing models.
 	 * 
-	 * @param set - data set on which the retraining is based on
+	 * @param data - data set on which the retraining is based on
+	 * @param targetColumns - number of the columns thta contain labels/target
 	 * @param modelID - ID of the model that will be retrained
 	 * @return true - retraining of model was started successfully
 	 * */
-	boolean reTrain(ArrayList<Integer> set, int modelID);
+	boolean reTrain(double[][] data, List<Integer> targetColumns, int modelID);
 	
 	/**
 	 * Method for getting the information about an trained model.
 	 * 
-	 *  @param modelID - ID of the model of which information are requested from
-	 *  @return Model - Object that contains the information of the requested model  
+	 * @param modelID - ID of the model of which information is requested from
+	 * @return Model - Object that contains the information of the requested model  
 	 * */
-	Model getTrainedModel(int modelID); 
+	EncogModel getTrainedModel(int modelID);
+
+	@Deprecated
+	EncogModel getTrainedModel(URL url, int modelID);
 	
+	/**
+	 * 
+	 * Method for getting normalizer of a model for a specific column/input.
+	 * 
+	 * @param modelID - ID of the model of which normalizer is requested from
+	 * @param columnNr - number of columns the normalizer can be used for
+	 * 
+	 * @return {@link NormalizedField} - the normalizer
+	 * */
+	NormalizedField getNormalizerInput(int modelID, int columnNr);
+
 }
diff --git a/feedbackloop.api/src/main/java/de/tudresden/inf/st/eraser/feedbackloop/api/Plan.java b/feedbackloop.api/src/main/java/de/tudresden/inf/st/eraser/feedbackloop/api/Plan.java
index f0c48bad8609f50833b886215d26d62fe4c4f7f8..8c237d06d64684d6386e1bc8de5a73146d7a1485 100644
--- a/feedbackloop.api/src/main/java/de/tudresden/inf/st/eraser/feedbackloop/api/Plan.java
+++ b/feedbackloop.api/src/main/java/de/tudresden/inf/st/eraser/feedbackloop/api/Plan.java
@@ -1,5 +1,6 @@
 package de.tudresden.inf.st.eraser.feedbackloop.api;
 
+import de.tudresden.inf.st.eraser.jastadd.model.Activity;
 import de.tudresden.inf.st.eraser.jastadd.model.ItemPreference;
 import de.tudresden.inf.st.eraser.jastadd.model.Root;
 
@@ -18,7 +19,7 @@ public interface Plan {
 
   Execute getExecute();
 
-  void planToMatchPreferences(String activity);
+  void planToMatchPreferences(Activity activity);
 
   default void informExecute(List<ItemPreference> preferences) {
     getExecute().updateItems(preferences);
diff --git a/feedbackloop.api/src/main/java/de/tudresden/inf/st/eraser/feedbackloop/api/model/Model.java b/feedbackloop.api/src/main/java/de/tudresden/inf/st/eraser/feedbackloop/api/model/Model.java
deleted file mode 100644
index 3a51f014248b5aaf1507877bd9d10f978915f221..0000000000000000000000000000000000000000
--- a/feedbackloop.api/src/main/java/de/tudresden/inf/st/eraser/feedbackloop/api/model/Model.java
+++ /dev/null
@@ -1,26 +0,0 @@
-package de.tudresden.inf.st.eraser.feedbackloop.api.model;
-
-/**
- * 
- * This class represents an object that contains all information of a trained model.
- * 
- * @author Bierzyns - initial contribution
- * */
-public class Model {
-
-	private String modelType;
-	
-	public Model(String model) {
-		modelType=model;
-	}
-	
-	
-	/**
-	 * Getter for model type.
-	 * 
-	 * @return modelType - Possible results: NN or DT
-	 * */
-	String getModelType(){
-		return modelType;
-	};
-}
diff --git a/feedbackloop.api/src/main/resources/log4j2.xml b/feedbackloop.api/src/main/resources/log4j2.xml
index 0594576fac98ba859e411597c90c8e3d989378bd..867ec439d0a32dcb5f8b3e2d0c7485d7d8da418c 100644
--- a/feedbackloop.api/src/main/resources/log4j2.xml
+++ b/feedbackloop.api/src/main/resources/log4j2.xml
@@ -4,8 +4,8 @@
         <Console name="Console">
             <PatternLayout pattern="%highlight{%d{HH:mm:ss.SSS} %-5level} %c{1.} - %msg%n"/>
         </Console>
-        <RollingFile name="RollingFile" fileName="logs/jastadd-mquat.log"
-                    filePattern="logs/jastadd-mquat-%i.log">
+        <RollingFile name="RollingFile" fileName="logs/eraser.log"
+                    filePattern="logs/eraser-%i.log">
             <PatternLayout pattern="%d{HH:mm:ss.SSS} %-5level %logger{36} - %msg%n"/>
             <Policies>
                 <OnStartupTriggeringPolicy/>
diff --git a/feedbackloop.execute/.gitignore b/feedbackloop.execute/.gitignore
index 83ccc54d0286590d31587a71aaa789e780bff41e..70b583e34c3316bcd77c807e2d6b85db5e7d49f6 100644
--- a/feedbackloop.execute/.gitignore
+++ b/feedbackloop.execute/.gitignore
@@ -1,2 +1,3 @@
 /build/
 /bin/
+logs/
diff --git a/feedbackloop.execute/build.gradle b/feedbackloop.execute/build.gradle
index 4365ddbc914e60874556d7bfa5cfcf9531c130a0..361f5c1429190208b8a70cbb889bf6c1cbd56993 100644
--- a/feedbackloop.execute/build.gradle
+++ b/feedbackloop.execute/build.gradle
@@ -1,28 +1,6 @@
-repositories {
-    mavenCentral()
-}
-
-sourceCompatibility = 1.8
-
-apply plugin: 'java'
-apply plugin: 'application'
-
 dependencies {
     compile project(':eraser-base')
     compile project(':feedbackloop.api')
-    compile group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: '2.9.8'
-    compile group: 'org.apache.logging.log4j', name: 'log4j-api', version: '2.11.1'
-    compile group: 'org.apache.logging.log4j', name: 'log4j-core', version: '2.11.1'
-    testCompile group: 'junit', name: 'junit', version: '4.12'
-    testCompile group: 'org.hamcrest', name: 'hamcrest-junit', version: '2.0.0.0'
-}
-
-run {
-    mainClassName = 'de.tudresden.inf.st.eraser.feedbackloop.execute.Main'
-    standardInput = System.in
-    if (project.hasProperty("appArgs")) {
-        args Eval.me(appArgs)
-    }
 }
 
 sourceSets {
diff --git a/feedbackloop.execute/src/main/java/de/tudresden/inf/st/eraser/feedbackloop/execute/ExecuteImpl.java b/feedbackloop.execute/src/main/java/de/tudresden/inf/st/eraser/feedbackloop/execute/ExecuteImpl.java
index 1482639bf3b29d6785a7faa3ba167b13cdb6992e..80d2346312445914306e6c224d1f9de1b766cf6d 100644
--- a/feedbackloop.execute/src/main/java/de/tudresden/inf/st/eraser/feedbackloop/execute/ExecuteImpl.java
+++ b/feedbackloop.execute/src/main/java/de/tudresden/inf/st/eraser/feedbackloop/execute/ExecuteImpl.java
@@ -9,10 +9,7 @@ import de.tudresden.inf.st.eraser.util.Tuple;
 import org.apache.logging.log4j.LogManager;
 import org.apache.logging.log4j.Logger;
 
-import java.util.List;
-import java.util.Map;
-import java.util.Optional;
-import java.util.Set;
+import java.util.*;
 import java.util.function.Consumer;
 
 /**
@@ -32,6 +29,7 @@ public class ExecuteImpl implements Execute {
 
   @Override
   public void updateItems(Map<String, Tuple<Integer, RGBvalues>> brightnessAndRgbForItems) {
+    List<ItemPreference> preferences = new ArrayList<>();
     for (Map.Entry<String, Tuple<Integer, RGBvalues>> entry : brightnessAndRgbForItems.entrySet()) {
       String itemId = entry.getKey();
       resolveOrLogError(itemId, item -> {
@@ -49,34 +47,21 @@ public class ExecuteImpl implements Execute {
           hsb = HSBvalues255.of(0, 100, brightness);
         }
         hsb.ensureBounds();
-        if (item instanceof ColorItem) {
-          ((ColorItem) item).setState(TupleHSB.of(hsb.hue, hsb.saturation, hsb.brightness));
-        } else if (item instanceof ItemWithDoubleState) {
-          ((ItemWithDoubleState) item).setState(brightness);
-        } else {
-          logger.warn("Can not set {} for item {}", hsb, item);
-        }
+        preferences.add(new ItemPreferenceColor(item, TupleHSB.of(hsb.hue, hsb.saturation, hsb.brightness)));
       });
     }
+    updateItems(preferences);
   }
 
   @Override
   public void updateItems(List<ItemPreference> preferences) {
     for (ItemPreference preference : preferences) {
-      Item item = preference.getItem();
-      TupleHSB preferredHSB = preference.getPreferredHSB();
-      if (item instanceof ColorItem) {
-        ((ColorItem) item).setState(preferredHSB);
-      } else if (item instanceof ItemWithDoubleState) {
-        ((ItemWithDoubleState) item).setState(preferredHSB.brightness);
-      } else {
-        logger.warn("Can not set {} for item {}", preferredHSB, item);
-      }
+      preference.apply();
     }
   }
 
   private void resolveOrLogError(String itemId, Consumer<? super Item> consumer) {
-    Optional<Item> optionalItem = knowledgeBase.resolveItem(itemId);
+    Optional<Item> optionalItem = knowledgeBase.getOpenHAB2Model().resolveItem(itemId);
     if (!optionalItem.isPresent()) {
       logger.warn("Could not resolve '{}' as an item.", itemId);
     }
diff --git a/feedbackloop.execute/src/main/java/de/tudresden/inf/st/eraser/feedbackloop/execute/Main.java b/feedbackloop.execute/src/main/java/de/tudresden/inf/st/eraser/feedbackloop/execute/Main.java
deleted file mode 100644
index a191317a8a4211a725640e9739cf2ce25b53739f..0000000000000000000000000000000000000000
--- a/feedbackloop.execute/src/main/java/de/tudresden/inf/st/eraser/feedbackloop/execute/Main.java
+++ /dev/null
@@ -1,10 +0,0 @@
-package de.tudresden.inf.st.eraser.feedbackloop.execute;
-
-import de.tudresden.inf.st.eraser.jastadd.model.*;
-
-public class Main {
-
-  public static void main(String[] args) {
-    System.out.println("Hello World!");
-  }
-}
diff --git a/feedbackloop.execute/src/main/resources/log4j2.xml b/feedbackloop.execute/src/main/resources/log4j2.xml
index 0594576fac98ba859e411597c90c8e3d989378bd..867ec439d0a32dcb5f8b3e2d0c7485d7d8da418c 100644
--- a/feedbackloop.execute/src/main/resources/log4j2.xml
+++ b/feedbackloop.execute/src/main/resources/log4j2.xml
@@ -4,8 +4,8 @@
         <Console name="Console">
             <PatternLayout pattern="%highlight{%d{HH:mm:ss.SSS} %-5level} %c{1.} - %msg%n"/>
         </Console>
-        <RollingFile name="RollingFile" fileName="logs/jastadd-mquat.log"
-                    filePattern="logs/jastadd-mquat-%i.log">
+        <RollingFile name="RollingFile" fileName="logs/eraser.log"
+                    filePattern="logs/eraser-%i.log">
             <PatternLayout pattern="%d{HH:mm:ss.SSS} %-5level %logger{36} - %msg%n"/>
             <Policies>
                 <OnStartupTriggeringPolicy/>
diff --git a/feedbackloop.execute/src/test/java/de/tudresden/inf/st/eraser/feedbackloop/execute/ExecuteImplTest.java b/feedbackloop.execute/src/test/java/de/tudresden/inf/st/eraser/feedbackloop/execute/ExecuteImplTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..f3f79ac72847b1c8c251c96a6d011014479f7115
--- /dev/null
+++ b/feedbackloop.execute/src/test/java/de/tudresden/inf/st/eraser/feedbackloop/execute/ExecuteImplTest.java
@@ -0,0 +1,129 @@
+package de.tudresden.inf.st.eraser.feedbackloop.execute;
+
+import de.tudresden.inf.st.eraser.feedbackloop.api.Execute;
+import de.tudresden.inf.st.eraser.jastadd.model.*;
+import de.tudresden.inf.st.eraser.util.TestUtils;
+import de.tudresden.inf.st.eraser.util.TestUtils.ModelAndItem;
+import org.junit.Assert;
+import org.junit.Test;
+
+import java.util.Collections;
+
+/**
+ * Test updating items within the Execute component.
+ *
+ * @author rschoene - Initial contribution
+ */
+public class ExecuteImplTest {
+
+  private static final double DELTA = 0.001;
+
+  @Test
+  public void testColorControlledByOneNumber() {
+    ModelAndItem mai = TestUtils.createModelAndItem(0);
+    OpenHAB2Model model = mai.model;
+    NumberItem numberItem = mai.item;
+
+    ColorItem lamp = new ColorItem();
+    lamp.setID("lamp");
+    lamp.setState(TupleHSB.of(0, 0, 0), false);
+    lamp.enableSendState();
+    TestUtils.getDefaultGroup(model).addItem(lamp);
+
+    numberItem.addControlling(lamp);
+
+    Execute execute = new ExecuteImpl();
+    execute.setKnowledgeBase(model.getRoot());
+
+    Assert.assertEquals(0, numberItem.getState(), DELTA);
+    Assert.assertEquals(TupleHSB.of(0, 0, 0), lamp.getState());
+
+    ItemPreference preference = new ItemPreferenceColor(lamp, TupleHSB.of(1, 2, 3));
+    execute.updateItems(Collections.singletonList(preference));
+
+    Assert.assertEquals(3, numberItem.getState(), DELTA);
+    Assert.assertEquals(TupleHSB.of(1, 2, 3), lamp.getState());
+  }
+
+  @Test
+  public void testColorControlledByOneBoolean() {
+    OpenHAB2Model model = TestUtils.createModelAndItem(0).model;
+
+    ItemWithBooleanState button = new SwitchItem();
+    button.setID("button");
+    button.setState(false, false);
+    button.enableSendState();
+    TestUtils.getDefaultGroup(model).addItem(button);
+
+    ColorItem lamp = new ColorItem();
+    lamp.setID("lamp");
+    lamp.setState(TupleHSB.of(0, 0, 0), false);
+    lamp.enableSendState();
+    TestUtils.getDefaultGroup(model).addItem(lamp);
+
+    button.addControlling(lamp);
+
+    Execute execute = new ExecuteImpl();
+    execute.setKnowledgeBase(model.getRoot());
+
+    Assert.assertFalse(button.getState());
+    Assert.assertEquals(TupleHSB.of(0, 0, 0), lamp.getState());
+
+    ItemPreference preference = new ItemPreferenceColor(lamp, TupleHSB.of(1, 2, 3));
+    execute.updateItems(Collections.singletonList(preference));
+
+    Assert.assertTrue(button.getState());
+    Assert.assertEquals(TupleHSB.of(1, 2, 3), lamp.getState());
+  }
+
+  @Test
+  public void testColorControlledByMany() {
+    ModelAndItem mai = TestUtils.createModelAndItem(0);
+    OpenHAB2Model model = mai.model;
+    NumberItem numberItem = mai.item;
+
+    Group g = TestUtils.getDefaultGroup(model);
+
+    StringItem stringItem = new StringItem();
+    stringItem.setState("0", false);
+    g.addItem(stringItem);
+
+    SwitchItem booleanItem = new SwitchItem();
+    booleanItem.setState(false, false);
+    g.addItem(booleanItem);
+
+    ColorItem colorItem = new ColorItem();
+    colorItem.setState(TupleHSB.of(0, 0, 0), false);
+    g.addItem(colorItem);
+
+    ColorItem lamp = new ColorItem();
+    lamp.setID("lamp");
+    lamp.setState(TupleHSB.of(0, 0, 0), false);
+    lamp.enableSendState();
+    TestUtils.getDefaultGroup(model).addItem(lamp);
+
+    lamp.addControlledBy(numberItem);
+    lamp.addControlledBy(stringItem);
+    lamp.addControlledBy(booleanItem);
+    lamp.addControlledBy(colorItem);
+
+    Execute execute = new ExecuteImpl();
+    execute.setKnowledgeBase(model.getRoot());
+
+    Assert.assertEquals(0, numberItem.getState(), DELTA);
+    Assert.assertEquals("0", stringItem.getState());
+    Assert.assertFalse(booleanItem.getState());
+    Assert.assertEquals(TupleHSB.of(0, 0, 0), colorItem.getState());
+    Assert.assertEquals(TupleHSB.of(0, 0, 0), lamp.getState());
+
+    ItemPreference preference = new ItemPreferenceColor(lamp, TupleHSB.of(1, 2, 3));
+    execute.updateItems(Collections.singletonList(preference));
+
+    Assert.assertEquals(3, numberItem.getState(), DELTA);
+    Assert.assertEquals("1,2,3", stringItem.getState());
+    Assert.assertTrue(booleanItem.getState());
+    Assert.assertEquals(TupleHSB.of(1, 2, 3), colorItem.getState());
+    Assert.assertEquals(TupleHSB.of(1, 2, 3), lamp.getState());
+  }
+
+}
diff --git a/feedbackloop.learner/.gitignore b/feedbackloop.learner/.gitignore
index 4a95481e611021403799e1b0a49ae9c4f18d2e2f..86db2fb7dd0bbe9d7ec2c79d624db271fcfd8e10 100644
--- a/feedbackloop.learner/.gitignore
+++ b/feedbackloop.learner/.gitignore
@@ -1,2 +1,3 @@
 /bin/
 /build/
+logs/
diff --git a/feedbackloop.learner/.settings/org.eclipse.buildship.core.prefs b/feedbackloop.learner/.settings/org.eclipse.buildship.core.prefs
deleted file mode 100644
index b1886adb46c085de842f1283c1a3c25151bfc988..0000000000000000000000000000000000000000
--- a/feedbackloop.learner/.settings/org.eclipse.buildship.core.prefs
+++ /dev/null
@@ -1,2 +0,0 @@
-connection.project.dir=..
-eclipse.preferences.version=1
diff --git a/feedbackloop.learner/build.gradle b/feedbackloop.learner/build.gradle
index f6e3fbeabed049106c8e5760001ee8b16fa08ec7..3ad295bf3c112d55e31fb6d185cef21ead1da66d 100644
--- a/feedbackloop.learner/build.gradle
+++ b/feedbackloop.learner/build.gradle
@@ -1,20 +1,9 @@
-repositories {
-    mavenCentral()
-}
-
-sourceCompatibility = 1.8
-
-apply plugin: 'java'
 apply plugin: 'application'
 
 dependencies {
     compile project(':eraser-base')
     compile project(':feedbackloop.api')
-    compile group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: '2.9.8'
-    compile group: 'org.apache.logging.log4j', name: 'log4j-api', version: '2.11.1'
-    compile group: 'org.apache.logging.log4j', name: 'log4j-core', version: '2.11.1'
-    testCompile group: 'junit', name: 'junit', version: '4.12'
-    testCompile group: 'org.hamcrest', name: 'hamcrest-junit', version: '2.0.0.0'
+    compile group: 'org.encog', name: 'encog-core', version: '3.4'
 }
 
 run {
diff --git a/feedbackloop.learner/src/main/java/de/tudresden/inf/st/eraser/feedbackloop/learner/Dataset.java b/feedbackloop.learner/src/main/java/de/tudresden/inf/st/eraser/feedbackloop/learner/Dataset.java
new file mode 100644
index 0000000000000000000000000000000000000000..0b09cbd2c73a753fa2de5b74b8bbe509e8e75a36
--- /dev/null
+++ b/feedbackloop.learner/src/main/java/de/tudresden/inf/st/eraser/feedbackloop/learner/Dataset.java
@@ -0,0 +1,36 @@
+package de.tudresden.inf.st.eraser.feedbackloop.learner;
+
+import org.encog.util.csv.ReadCSV;
+
+import java.util.List;
+
+/**
+ * This class is an representation of a data set in csv format.
+ * The data is saved and made accessible by {@link ReadCSV} and the numbers of the target columns is saved as meta-data.
+ * 
+ *  @author Bierzyns - initial contribution
+ * 
+ * */
+public class Dataset {
+
+    private final ReadCSV csv;
+    private final List<Integer> targetColumns;
+
+    public Dataset(ReadCSV csv, List<Integer> targetColumns) {
+        this.csv = csv;
+        this.targetColumns = targetColumns;
+    }
+
+    public int getColumnCount() {
+        return csv.getColumnCount();
+    }
+
+    public List<Integer> getTargetColumns() {
+        return targetColumns;
+    }
+
+    public ReadCSV getCsv() {
+        return csv;
+    }
+
+}
diff --git a/feedbackloop.learner/src/main/java/de/tudresden/inf/st/eraser/feedbackloop/learner/LearnerHelper.java b/feedbackloop.learner/src/main/java/de/tudresden/inf/st/eraser/feedbackloop/learner/LearnerHelper.java
new file mode 100644
index 0000000000000000000000000000000000000000..f23b58105b78ea6b84a3f11211e843eb28f2cf13
--- /dev/null
+++ b/feedbackloop.learner/src/main/java/de/tudresden/inf/st/eraser/feedbackloop/learner/LearnerHelper.java
@@ -0,0 +1,150 @@
+package de.tudresden.inf.st.eraser.feedbackloop.learner;
+
+import de.tudresden.inf.st.eraser.feedbackloop.api.EncogModel;
+import de.tudresden.inf.st.eraser.jastadd.model.*;
+import org.apache.commons.math3.stat.StatUtils;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.encog.engine.network.activation.ActivationFunction;
+import org.encog.engine.network.activation.ActivationLinear;
+import org.encog.engine.network.activation.ActivationSigmoid;
+import org.encog.engine.network.activation.ActivationTANH;
+import org.encog.neural.networks.layers.Layer;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * Transformation of a {@link EncogModel} into a {@link NeuralNetworkRoot}.
+ *
+ * @author rschoene - Initial contribution
+ */
+public class LearnerHelper {
+
+  private static final Logger logger = LogManager.getLogger(LearnerHelper.class);
+
+  // Activation Functions
+  private static DoubleArrayDoubleFunction sigmoid = inputs -> Math.signum(Arrays.stream(inputs).sum());
+  private static DoubleArrayDoubleFunction tanh = inputs -> Math.tanh(Arrays.stream(inputs).sum());
+  private static DoubleArrayDoubleFunction function_one = inputs -> 1.0;
+
+  public static NeuralNetworkRoot transform(EncogModel encogModel) {
+    NeuralNetworkRoot result = NeuralNetworkRoot.createEmpty();
+    List<Double> weights = encogModel.getWeights();
+    logger.debug("Got {} weights", weights.size());
+
+    List<List<Neuron>> allNeurons = new ArrayList<>();
+    // inputs
+    Layer inputLayer = encogModel.getInputLayer();
+    reportLayer("input", inputLayer);
+    List<Neuron> inputNeurons = new ArrayList<>();
+    for (int i = 0; i < nonBiasNeuronCount(inputLayer); ++i) {
+      InputNeuron inputNeuron = new InputNeuron();
+      result.addInputNeuron(inputNeuron);
+      inputNeurons.add(inputNeuron);
+    }
+    addBiasIfNeeded(inputLayer, result.getHiddenNeuronList(), inputNeurons);
+    allNeurons.add(inputNeurons);
+
+    // hidden layer
+    List<Neuron> currentNeurons;
+    for (Layer hiddenLayer : encogModel.getHiddenLayers()) {
+      reportLayer("one hidden", hiddenLayer);
+      currentNeurons = new ArrayList<>();
+      allNeurons.add(currentNeurons);
+      for (int i = 0; i < nonBiasNeuronCount(hiddenLayer); ++i) {
+        HiddenNeuron hiddenNeuron = new HiddenNeuron();
+        setActivationFunction(hiddenNeuron, hiddenLayer.getActivationFunction());
+        result.addHiddenNeuron(hiddenNeuron);
+        currentNeurons.add(hiddenNeuron);
+      }
+      addBiasIfNeeded(hiddenLayer, result.getHiddenNeuronList(), currentNeurons);
+    }
+
+    // output layer
+    OutputLayer outputLayer = new OutputLayer();
+    Layer modelOutputLayer = encogModel.getOutputLayer();
+    reportLayer("output", modelOutputLayer);
+    List<Neuron> outputNeurons = new ArrayList<>();
+    for (int i = 0; i < nonBiasNeuronCount(modelOutputLayer); ++i) {
+      OutputNeuron outputNeuron = new OutputNeuron();
+      setActivationFunction(outputNeuron, modelOutputLayer.getActivationFunction());
+      outputLayer.addOutputNeuron(outputNeuron);
+      outputNeurons.add(outputNeuron);
+    }
+    result.setOutputLayer(outputLayer);
+    allNeurons.add(outputNeurons);
+    logger.debug("Created a total of {} neurons",
+        allNeurons.stream()
+            .map(list -> Integer.toString(list.size()))
+            .collect(Collectors.joining("+")));
+
+    // set weights from back to front, and from top to bottom
+    int weightIndex = 0;
+    for (int layer = allNeurons.size() - 1; layer > 0; --layer) {
+      List<Neuron> rightList = allNeurons.get(layer);
+      List<Neuron> leftList = allNeurons.get(layer - 1);
+      for (int rightIndex = 0; rightIndex < rightList.size(); rightIndex++) {
+        for (int leftIndex = 0; leftIndex < leftList.size(); leftIndex++) {
+          if (rightList.get(rightIndex) instanceof BiasNeuron) {
+            continue;
+          }
+          leftList.get(leftIndex).connectTo(rightList.get(rightIndex), weights.get(weightIndex++));
+        }
+      }
+    }
+    if (weightIndex != weights.size()) {
+      logger.error("No all weights used (only {} of {}). Loaded wrong model!", weightIndex, weights.size());
+    }
+
+    outputLayer.setCombinator(LearnerHelper::predictor);
+    logger.info("Created model with {} input, {} hidden and {} output neurons",
+        result.getNumInputNeuron(), result.getNumHiddenNeuron(), result.getOutputLayer().getNumOutputNeuron());
+    return result;
+  }
+
+  private static void addBiasIfNeeded(Layer layer, JastAddList<HiddenNeuron> neuronList, List<Neuron> localNeuronList) {
+    if (layer.hasBias()) {
+      BiasNeuron bias = new BiasNeuron();
+      neuronList.add(bias);
+      localNeuronList.add(bias);
+    }
+  }
+
+  private static int nonBiasNeuronCount(Layer layer) {
+    return layer.getNeuronCount() - (layer.hasBias() ? 1 : 0);
+  }
+
+  private static void reportLayer(String name, Layer layer) {
+    logger.debug("{} layer has {} neurons {}",
+        name, layer.getNeuronCount(), layer.hasBias() ? "(including bias)" : "");
+  }
+
+  private static void setActivationFunction(HiddenNeuron neuron, ActivationFunction function) {
+    if (function instanceof ActivationTANH) {
+      neuron.setActivationFormula(tanh);
+    } else if (function instanceof ActivationLinear) {
+      neuron.setActivationFormula(function_one);
+    } else if (function instanceof ActivationSigmoid) {
+      neuron.setActivationFormula(sigmoid);
+    } else {
+      throw new IllegalArgumentException("Unknown activation function " + function.getClass().getName());
+    }
+  }
+
+    private static double predictor(double[] inputs) {
+    int index = 0;
+    double maxInput = StatUtils.max(inputs);
+    for (int i = 0; i < inputs.length; i++) {
+      if (inputs[i] == maxInput) {
+        index = i;
+      }
+    }
+    //outputs from learner
+    final double[] outputs = new double[]{2.0, 1.0, 3.0, 0.0};
+    return outputs[index];
+  }
+
+}
diff --git a/feedbackloop.learner/src/main/java/de/tudresden/inf/st/eraser/feedbackloop/learner/LearnerImpl.java b/feedbackloop.learner/src/main/java/de/tudresden/inf/st/eraser/feedbackloop/learner/LearnerImpl.java
index a229d11eda814a21920c2b9fa40400fe59bb24d8..83edf6b7f195d9be6420369d4590955c42449735 100644
--- a/feedbackloop.learner/src/main/java/de/tudresden/inf/st/eraser/feedbackloop/learner/LearnerImpl.java
+++ b/feedbackloop.learner/src/main/java/de/tudresden/inf/st/eraser/feedbackloop/learner/LearnerImpl.java
@@ -1,52 +1,230 @@
 package de.tudresden.inf.st.eraser.feedbackloop.learner;
 
-import java.util.ArrayList;
-
+import de.tudresden.inf.st.eraser.feedbackloop.api.EncogModel;
 import de.tudresden.inf.st.eraser.feedbackloop.api.Learner;
-import de.tudresden.inf.st.eraser.feedbackloop.api.model.Model;
 import de.tudresden.inf.st.eraser.jastadd.model.Root;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.encog.neural.flat.FlatNetwork;
+import org.encog.neural.networks.BasicNetwork;
+import org.encog.neural.networks.layers.BasicLayer;
+import org.encog.neural.networks.layers.Layer;
+import org.encog.util.arrayutil.NormalizedField;
+import org.encog.util.csv.CSVFormat;
+import org.encog.util.csv.ReadCSV;
+
+import java.io.File;
+import java.io.InputStream;
+import java.net.URL;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.*;
 
 /**
  * Implementation of the Learner.
  *
  * @author Bierzyns - Initial contribution
  */
-public class LearnerImpl implements Learner{
-
-	private Root knowledgeBase;
-	
-	@Override
-	public void setKnowledgeBase(Root knowledgeBase) {
-		 this.knowledgeBase = knowledgeBase;
-	}
-
-	@Override
-	public boolean loadDataSet(String dataSetName, int modelID) {
-		// TODO Auto-generated method stub
-		return false;
-	}
-
-	@Override
-	public boolean train(int modelID) {
-		// TODO Auto-generated method stub
-		return false;
-	}
-
-	@Override
-	public boolean train(ArrayList<Integer> set, int modelID) {
-		// TODO Auto-generated method stub
-		return false;
-	}
-
-	@Override
-	public boolean reTrain(ArrayList<Integer> set, int modelID) {
-		// TODO Auto-generated method stub
-		return false;
-	}
-
-	@Override
-	public Model getTrainedModel(int modelID) {
-		return new Model("NN");
-	}
+public class LearnerImpl implements Learner {
+
+
+  private Root knowledgeBase;
+  private static final Logger logger = LogManager.getLogger(LearnerImpl.class);
+  private Path csvFolderPath = Paths.get("src", "main", "resources");
+  private String modelFolderPath = ".";
+  private CSVFormat format = new CSVFormat('.', ',');
+  private Map<Integer, Dataset> datasets = new HashMap<>();
+  private Map<Integer, Network> models = new HashMap<>();
+
+
+  @Override
+  public void setKnowledgeBase(Root knowledgeBase) {
+    this.knowledgeBase = knowledgeBase;
+  }
+
+  public void setCsvFolderPath(String csvFolderPath) {
+    this.csvFolderPath = Paths.get(csvFolderPath);
+  }
+
+  public void setModelFolderPath(String modelFolderPath) {
+    this.modelFolderPath = modelFolderPath;
+  }
+
+  @Override
+  public boolean loadDataSet(String dataSetName, List<Integer> targetColumns, int modelID) {
+    Path realDataSetPath = csvFolderPath.resolve(dataSetName);
+    logger.debug("Load data set from file {}", realDataSetPath);
+    try {
+      Dataset set = new Dataset(new ReadCSV(realDataSetPath.toFile().getAbsoluteFile(), false, format), targetColumns);
+      datasets.put(modelID, set);
+    } catch (Exception e) {
+      e.printStackTrace();
+      return false;
+    }
+    return true;
+  }
+
+  @Override
+  public boolean loadModelFromFile(File file, int modelID, List<Integer> inputMaxes, List<Integer> inputMins, List<Integer> targetMaxes,
+                                   List<Integer> targetMins) {
+    logger.debug("Load model from file {}", file);
+    models.put(modelID, new Network(file.getAbsolutePath(), modelID, inputMaxes, inputMins, targetMaxes, targetMins));
+    return true;
+  }
+
+  @Override
+  public boolean loadModelFromFile(InputStream input, int modelID, List<Integer> inputMaxes, List<Integer> inputMins, List<Integer> targetMaxes,
+                                   List<Integer> targetMins) {
+    logger.debug("Load model from input stream");
+    models.put(modelID, new Network(input, modelID, inputMaxes, inputMins, targetMaxes, targetMins));
+    return true;
+  }
+
+  @Override
+  public boolean train(int inputCount, int outputCount, int hiddenCount, int hiddenNeuronCount, int modelID,
+                       List<Integer> inputMaxes, List<Integer> inputMins, List<Integer> targetMaxes,
+                       List<Integer> targetMins) {
+    // Method for the initial training of algorithms and models. That uses external data set for training.
+
+    if (datasets.get(modelID) != null) {
+      Dataset set = datasets.get(modelID);
+
+      ReadCSV csv = set.getCsv();
+
+      Network model = new Network(inputCount, outputCount, hiddenCount, hiddenNeuronCount, modelID, inputMaxes,
+          inputMins, targetMaxes, targetMins);
+
+      ArrayList<Double> input = new ArrayList<>();
+      ArrayList<Double> target = new ArrayList<>();
+
+      while (csv.next()) {
+        logger.debug("Train next csv row");
+        for (int i = 0; i < csv.getColumnCount(); i++) {
+          int col_nr = i + 1;
+          if (set.getTargetColumns().contains(col_nr)) {
+            target.add(csv.getDouble(i));
+          } else {
+            input.add(csv.getDouble(i));
+          }
+        }
+
+        model.train(input, target);
+        input.clear();
+        target.clear();
+      }
+
+      models.put(modelID, model);
+      model.saveModel(modelFolderPath);
+
+      return true;
+    }
+    return false;
+  }
+
+  @Override
+  public boolean train(double[][] data, int inputCount, int outputCount, int hiddenCount, int hiddenNeuronCount, int modelID,
+                       List<Integer> inputMaxes, List<Integer> inputMins, List<Integer> targetMaxes,
+                       List<Integer> targetMins, List<Integer> targetColumns) {
+
+    Network model = new Network(inputCount, outputCount, hiddenCount, hiddenNeuronCount, modelID, inputMaxes,
+        inputMins, targetMaxes, targetMins);
+
+    return reTrainModel(model, data, targetColumns, modelID);
+  }
+
+
+  @Override
+  public boolean reTrain(double[][] data, List<Integer> targetColumns, int modelID) {
+
+    Network model = models.get(modelID);
+
+    return reTrainModel(model, data, targetColumns, modelID);
+  }
+
+  private boolean reTrainModel(Network model, double[][] data, List<Integer> targetColumns, int modelID) {
+    List<Double> input = new ArrayList<>();
+    List<Double> target = new ArrayList<>();
+
+    for (int i = 0; i < data.length; i++) {
+
+      for (int j = 0; j < data[0].length; j++) {
+        int col_nr = j + 1;
+        if (targetColumns.contains(col_nr)) {
+          target.add(data[i][j]);
+        } else {
+          input.add(data[i][j]);
+        }
+
+        model.train(input, target);
+        input.clear();
+        target.clear(); 
+      }
+
+    }
+
+    models.put(modelID, model);
+    model.saveModel(modelFolderPath);
+
+    return true;
+  }
+
+
+  @Override
+  public EncogModel getTrainedModel(int modelID) {
+    return fillModel(modelID);
+  }
+
+  @Override
+  public EncogModel getTrainedModel(URL url, int modelID) {
+    return fillModel(modelID);
+  }
+
+  private EncogModel fillModel(int modelID) {
+    EncogModel encogModel = new EncogModel("NN");
+    BasicNetwork nn = models.get(modelID).getNetwork();
+
+    ArrayList<Double> weightsList = new ArrayList<>();
+    String weights = nn.dumpWeights();
+    String[] split = weights.split(",");
+
+    for (int i = 0; i < (split.length); i++) {
+      weightsList.add(Double.valueOf(split[i]));
+    }
+
+    encogModel.setWeights(weightsList);
+
+    // do not use getLayers() because it is not restored immediately on load from file
+    FlatNetwork flat = nn.getFlat();
+    List<Layer> layers = new ArrayList<>(flat.getLayerCounts().length);
+    logger.debug("layer counts: {}", Arrays.toString(flat.getLayerCounts()));
+    for (int j = 0; j < flat.getLayerCounts().length; j++) {
+//      boolean hasBias = j != 0 && j != flat.getLayerCounts().length - 1;
+      boolean hasBias = flat.getLayerCounts()[j] != flat.getLayerFeedCounts()[j];
+      Layer l = new BasicLayer(flat.getActivationFunctions()[j], hasBias, flat.getLayerCounts()[j]);
+      l.setBiasActivation(flat.getBiasActivation()[j]);
+      layers.add(0, l);
+    }
+
+    encogModel.setLayers(layers);
+
+    return encogModel;
+  }
+
+
+  /**
+   * @param modelID
+   * @return
+   */
+  public NormalizedField getNormalizerInput(int modelID, int columnNr) {
+    return models.get(modelID).getNormalizersIn().get(columnNr);
+  }
+
+  /**
+   * @param modelID
+   * @return
+   */
+  public NormalizedField getNormalizerTar(int modelID, int columnNr) {
+    return models.get(modelID).getNormalizersTar().get(columnNr);
+  }
 
 }
diff --git a/feedbackloop.learner/src/main/java/de/tudresden/inf/st/eraser/feedbackloop/learner/Main.java b/feedbackloop.learner/src/main/java/de/tudresden/inf/st/eraser/feedbackloop/learner/Main.java
index 24a5b62570a115fee266bc26cad2015656bee974..0f82201e8bcffb278656fb821d6efb3e55008791 100644
--- a/feedbackloop.learner/src/main/java/de/tudresden/inf/st/eraser/feedbackloop/learner/Main.java
+++ b/feedbackloop.learner/src/main/java/de/tudresden/inf/st/eraser/feedbackloop/learner/Main.java
@@ -1,8 +1,259 @@
 package de.tudresden.inf.st.eraser.feedbackloop.learner;
 
+import de.tudresden.inf.st.eraser.feedbackloop.api.Learner;
+import de.tudresden.inf.st.eraser.feedbackloop.api.EncogModel;
+import de.tudresden.inf.st.eraser.jastadd.model.*;
+import org.apache.commons.math3.stat.StatUtils;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+import java.io.File;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.function.Function;
+
+@SuppressWarnings("unused")
 public class Main {
+	private static final Logger logger = LogManager.getLogger(Main.class);
+	private static class InitialDataConfig {
+		static List<Integer> inputMins = Arrays.asList(
+				7,  // min month
+				1,  // min day
+				10, // min hour
+				1   // min minute
+		);
+		static List<Integer> inputMaxes = Arrays.asList(
+				9,  // max month
+				31, // max day
+				21, // max hour
+				60  // max minute
+		);
+		static List<Integer> targetMins = Collections.singletonList(0);
+		static List<Integer> targetMaxes = Collections.singletonList(3);
+		static String csv_filename = "initial_data.csv";
+		static String encog_filename = Paths.get("src", "main", "resources").toFile().getAbsolutePath() + "/";
+		static int inputCount = 4;
+		static int outputCount = 1;
+		static int hiddenCount = 0;
+		static int hiddenNeuronCount = 7;
+		static List<Integer> targetColumns = Collections.singletonList(4);
+	}
 
 	public static void main(String[] args) {
-		System.out.println("Hello World!");
+//		loadFromCsv();
+		loadFromEncog();
+	}
+
+	private static void loadFromCsv() {
+		Learner learner = new LearnerImpl();
+		learner.loadDataSet(InitialDataConfig.csv_filename, InitialDataConfig.targetColumns,1);
+		learner.train(
+				InitialDataConfig.inputCount, InitialDataConfig.outputCount, InitialDataConfig.hiddenCount,
+				InitialDataConfig.hiddenNeuronCount, 1, InitialDataConfig.inputMaxes, InitialDataConfig.inputMins,
+				InitialDataConfig.targetMaxes, InitialDataConfig.targetMins);
+
+		printModel(learner.getTrainedModel(1));
+	}
+
+	private static void loadFromEncog() {
+		Learner learner = new LearnerImpl();
+		learner.loadModelFromFile(
+				new File(InitialDataConfig.encog_filename), 1,
+				InitialDataConfig.inputMaxes, InitialDataConfig.inputMins,
+				InitialDataConfig.targetMaxes, InitialDataConfig.targetMins);
+		printModel(learner.getTrainedModel(1));
+    NeuralNetworkRoot eraserModel = LearnerHelper.transform(learner.getTrainedModel(1));
+	}
+
+	private static void printModel(EncogModel encogModel) {
+		logger.info("Model Type is: " + encogModel.getModelType());
+		logger.info("Model Weights are: " + encogModel.getWeights());
+		logger.info("Model layers are: " + encogModel.getLayers());
+//		logger.info("Model input normal neutrons: " + model.getInputLayerNumber());
+//		logger.info("Model input bias neutron: " + model.getInputBias());
+//		logger.info("Model hidden normal neutrons: " + model.gethiddenLayerNumber());
+//		logger.info("Model hidden bias neutron: " + model.getHiddenBias());
+//		logger.info("Model output neutrons: " + model.getOutputLayerNumber());
+//		logger.info("Model input activation function: " + model.getInputActivationFunction());
+//		logger.info("Model hidden activation function: " + model.getHiddenActivationFunction());
+//		logger.info("Model output activation function: " + model.getOutputActivationFunction());
+	}
+
+	//(ArrayList<Integer> list3
+	private static Root createModel(ArrayList<Double> normalizedInputs, int inputBiasNumber) {
+
+		//create KB Model
+		Root root = Root.createEmptyRoot();
+		Group group = new Group();
+		group.setID("Group1");
+		root.getOpenHAB2Model().addGroup(group);
+
+		NumberItem monthItem = new NumberItem();
+		monthItem.setState(normalizedInputs.get(0));
+		monthItem.setID("month");
+		monthItem.setLabel("datetime-month");
+
+		NumberItem dayItem = new NumberItem();
+		dayItem.setState(normalizedInputs.get(1));
+		dayItem.setID("day");
+		dayItem.setLabel("datetime-day");
+
+		NumberItem hourItem = new NumberItem();
+		hourItem.setState(normalizedInputs.get(2));
+		hourItem.setID("hour");
+		hourItem.setLabel("datetime-hour");
+
+		NumberItem minuteItem = new NumberItem();
+		minuteItem.setState(normalizedInputs.get(3));
+		minuteItem.setID("minute");
+		minuteItem.setLabel("datetime-minute");
+
+		if (inputBiasNumber == 1) {
+			NumberItem biasItem = new NumberItem();
+			biasItem.setState(1);
+			biasItem.setID("bias");
+			biasItem.setLabel("bias");
+			group.addItem(biasItem);
+		}
+		group.addItem(monthItem);
+		group.addItem(dayItem);
+		group.addItem(hourItem);
+		group.addItem(minuteItem);
+		return root;
+	}
+
+	/**
+	 * Purpose: Create a neural network with 3 layers
+	 */
+	private static void createBrightnessNetwork(ArrayList<Double> all_weights, int inputBiasNumber, int hiddenlayernumber,
+												int hiddenlayerbias, ArrayList<Double> normalizedInputsandOutput) {
+		Root root = createModel(normalizedInputsandOutput, inputBiasNumber);
+		OpenHAB2Model model = root.getOpenHAB2Model();
+		Item monthItem = model.resolveItem("month").orElseThrow(
+				() -> new RuntimeException("Month not found"));
+		Item dayItem = model.resolveItem("day").orElseThrow(
+				() -> new RuntimeException("Day not found"));
+		Item hourItem = model.resolveItem("hour").orElseThrow(
+				() -> new RuntimeException("Hour not found"));
+		Item minuteItem = model.resolveItem("minute").orElseThrow(
+				() -> new RuntimeException("Minute not found"));
+		Item biasItem = model.resolveItem("bias").orElseThrow(
+				() -> new RuntimeException("Bias not found"));
+
+		NeuralNetworkRoot nn = new NeuralNetworkRoot();
+
+		// Activation Functions
+		DoubleArrayDoubleFunction sigmoid = inputs -> Math.signum(Arrays.stream(inputs).sum());
+		DoubleArrayDoubleFunction tanh = inputs -> Math.tanh(Arrays.stream(inputs).sum());
+		DoubleArrayDoubleFunction function_one = inputs -> 1.0;
+
+		//Weights outputs from learner Module
+		ArrayList<Double> weights = all_weights;
+		// input layer
+		InputNeuron month = new InputNeuron();
+		month.setItem(monthItem);
+		InputNeuron day = new InputNeuron();
+		day.setItem(dayItem);
+		InputNeuron hour = new InputNeuron();
+		hour.setItem(hourItem);
+		InputNeuron minute = new InputNeuron();
+		minute.setItem(minuteItem);
+		InputNeuron bias = new InputNeuron();
+		bias.setItem(biasItem);
+
+		nn.addInputNeuron(month);
+		nn.addInputNeuron(day);
+		nn.addInputNeuron(hour);
+		nn.addInputNeuron(minute);
+		nn.addInputNeuron(bias);
+
+		// output layer
+		OutputLayer outputLayer = new OutputLayer();
+		OutputNeuron output0 = new OutputNeuron();
+		output0.setActivationFormula(tanh);
+		OutputNeuron output1 = new OutputNeuron();
+		output1.setActivationFormula(tanh);
+		OutputNeuron output2 = new OutputNeuron();
+		output2.setActivationFormula(tanh);
+		OutputNeuron output3 = new OutputNeuron();
+		output3.setActivationFormula(tanh);
+
+		outputLayer.addOutputNeuron(output0);
+		outputLayer.addOutputNeuron(output1);
+		outputLayer.addOutputNeuron(output2);
+		outputLayer.addOutputNeuron(output3);
+
+		outputLayer.setCombinator(inputs -> predictor(inputs, normalizedInputsandOutput));
+		//outputLayer.setCombinator(inputs -> StatUtils.max(inputs) );
+		nn.setOutputLayer(outputLayer);
+
+		// hidden layer
+		int hiddenSum = hiddenlayernumber + hiddenlayerbias;
+		HiddenNeuron[] hiddenNeurons = new HiddenNeuron[hiddenSum];
+		for (int i = 0; i < (hiddenNeurons.length); i++) {
+			if (i == hiddenlayernumber) {
+				HiddenNeuron hiddenNeuron = new HiddenNeuron();
+				hiddenNeuron.setActivationFormula(function_one);
+				hiddenNeurons[i] = hiddenNeuron;
+				nn.addHiddenNeuron(hiddenNeuron);
+				bias.connectTo(hiddenNeuron, 1.0);
+				hiddenNeuron.connectTo(output0, weights.get(i));
+				hiddenNeuron.connectTo(output1, weights.get(i + hiddenSum));
+				hiddenNeuron.connectTo(output2, weights.get(i + hiddenSum * 2));
+				hiddenNeuron.connectTo(output3, weights.get(i + hiddenSum * 3));
+			} else {
+				HiddenNeuron hiddenNeuron = new HiddenNeuron();
+				hiddenNeuron.setActivationFormula(tanh);
+				hiddenNeurons[i] = hiddenNeuron;
+				nn.addHiddenNeuron(hiddenNeuron);
+
+				month.connectTo(hiddenNeuron, weights.get((hiddenNeurons.length * 4) + i * 5));
+				day.connectTo(hiddenNeuron, weights.get((hiddenNeurons.length * 4 + 1) + i * 5));
+				hour.connectTo(hiddenNeuron, weights.get((hiddenNeurons.length * 4 + 2) + i * 5));
+				minute.connectTo(hiddenNeuron, weights.get((hiddenNeurons.length * 4 + 3) + i * 5));
+				bias.connectTo(hiddenNeuron, weights.get((hiddenNeurons.length * 4 + 4) + i * 5));
+				hiddenNeuron.connectTo(output0, weights.get(i));
+				hiddenNeuron.connectTo(output1, weights.get(i + hiddenSum));
+				hiddenNeuron.connectTo(output2, weights.get(i + hiddenSum * 2));
+				hiddenNeuron.connectTo(output3, weights.get(i + hiddenSum * 3));
+			}
+		}
+		root.getMachineLearningRoot().setPreferenceLearning(nn);
+		System.out.println(root.prettyPrint());
+
+		List<String> output = new ArrayList<>();
+		Function<DoubleNumber, String> leafToString = classification -> Double.toString(classification.number);
+		Function<NeuralNetworkRoot, DoubleNumber> classify = NeuralNetworkRoot::classify;
+		DoubleNumber classification = classify.apply(nn);
+		output.add(leafToString.apply(classification));
+		System.out.println(output);
+
+
+
+
+	}
+		private static double predictor(double[] inputs, ArrayList<Double> normalizedInputsandOutputs){
+			int index = 0;
+			double maxinput = StatUtils.max(inputs);
+			for (int i = 0; i < inputs.length; i++) {
+				if (inputs[i] == maxinput) {
+					index = i;
+				}
+			}
+			//outputs from learner
+			ArrayList<Double> outputs = new ArrayList<Double>(Arrays.asList(normalizedInputsandOutputs.get(4),
+					normalizedInputsandOutputs.get(5), normalizedInputsandOutputs.get(6),
+					normalizedInputsandOutputs.get(7)));
+			double output = outputs.get(index);
+			return output;
+		}
+		//[BasicMLData:0.0,0.0,0.0,0.0]
+	//inputs:
+	//[BasicMLData:-1.0,0.2666666666666666,-0.6363636363636364,-0.5593220338983051]
+	//outputs:
+	//[BasicMLData:0.36743405976714366,-0.6610085416169492,-0.9999999998849812,-0.9999999224507112]
+
 	}
-}
diff --git a/feedbackloop.learner/src/main/java/de/tudresden/inf/st/eraser/feedbackloop/learner/Network.java b/feedbackloop.learner/src/main/java/de/tudresden/inf/st/eraser/feedbackloop/learner/Network.java
new file mode 100644
index 0000000000000000000000000000000000000000..e3467c7fda1544befee165d0316f11e9b35e8d27
--- /dev/null
+++ b/feedbackloop.learner/src/main/java/de/tudresden/inf/st/eraser/feedbackloop/learner/Network.java
@@ -0,0 +1,202 @@
+package de.tudresden.inf.st.eraser.feedbackloop.learner;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.encog.engine.network.activation.ActivationSigmoid;
+import org.encog.ml.data.MLDataSet;
+import org.encog.ml.data.basic.BasicMLDataSet;
+import org.encog.ml.train.MLTrain;
+import org.encog.neural.networks.BasicNetwork;
+import org.encog.neural.networks.layers.BasicLayer;
+import org.encog.neural.networks.training.propagation.back.Backpropagation;
+import org.encog.persist.EncogDirectoryPersistence;
+import org.encog.util.arrayutil.NormalizationAction;
+import org.encog.util.arrayutil.NormalizedField;
+import org.encog.util.simple.EncogUtility;
+
+/**
+ * Network class serves as interface to encog BasicNetwork and holdsfunctions for handling the BasicNetwork (training, input, output and inference)
+ *
+ * @author Bierzynski - initial contribution
+ */
+public class Network {
+  private static final Logger logger = LogManager.getLogger(Network.class);
+  private BasicNetwork network;
+  private int modelID;
+  private ArrayList<NormalizedField> normalizersIn;
+  private ArrayList<NormalizedField> normalizersTar;
+
+  /**
+   * Constructor for when the neural network is created from data.
+   *
+   * @param inputCount        number of neurons in the input layer
+   * @param outputCount       number of neurons in the output layer
+   * @param hiddenCount       number of hidden layers in the network
+   * @param hiddenNeuronCount number of neurons in the hidden layers for now
+   * @param modelID           ID of the BasicNetwork.
+   * @param inputMaxes        list that contains max values of all input columns (sensors) e.g. light intensity 100
+   * @param inputMins         list that contains min values of all input columns (sensors) e.g. light intensity 0
+   * @param targetMaxes       list that contains max values of all output columns (results) e.g. brightness 100 for preference learning
+   * @param targetMins        list that contains min values of all output columns (results) e.g. brightness 0 for preference learning
+   */
+  public Network(int inputCount, int outputCount, int hiddenCount, int hiddenNeuronCount, int modelID,
+                 List<Integer> inputMaxes, List<Integer> inputMins, List<Integer> targetMaxes,
+                 List<Integer> targetMins) {
+
+    normalizersIn = new ArrayList<>();
+    normalizersTar = new ArrayList<>();
+    this.modelID = modelID;
+
+    network = new BasicNetwork();
+
+    network.addLayer(new BasicLayer(null, true, inputCount));
+
+    for (int i = 0; i < hiddenCount; i++) {
+      network.addLayer(new BasicLayer(new ActivationSigmoid(), true, hiddenNeuronCount));
+    }
+
+    network.addLayer(new BasicLayer(new ActivationSigmoid(), false, outputCount));
+    network.getStructure().finalizeStructure();
+    network.reset();
+
+    addNormalizer(inputMaxes, inputMins, normalizersIn);
+    addNormalizer(targetMaxes, targetMins, normalizersTar);
+  }
+
+  private void addNormalizer(List<Integer> maxes, List<Integer> mins, ArrayList<NormalizedField> normalizers) {
+    for (int j = 0; j < maxes.size(); j++) {
+      NormalizedField normalizer = new NormalizedField("in_" + j, NormalizationAction.Normalize,
+          maxes.get(j), mins.get(j));
+      normalizers.add(normalizer);
+    }
+  }
+
+  /**
+   * Constructor for when the neural network is loaded from a file.
+   * Please note that the normalizer are note loaded file , because it is assumed that the mins and maxes are saved anyway in the meta data of the data sets or items.
+   *
+   * @param path        path to the save folder of the model files e.g. C:\models\
+   * @param modelID     ID of the BasicNetwork.
+   * @param inputMaxes  list that contains max values of all input columns (sensors) e.g. light intensity 100
+   * @param inputMins   list that contains min values of all input columns (sensors) e.g. light intensity 0
+   * @param targetMaxes list that contains max values of all output columns (results) e.g. brightness 100 for preference learning
+   * @param targetMins  list that contains min values of all output columns (results) e.g. brightness 0 for preference learning
+   */
+  public Network(String path, int modelID, List<Integer> inputMaxes, List<Integer> inputMins, List<Integer> targetMaxes,
+                 List<Integer> targetMins) {
+    this(() -> (BasicNetwork) EncogDirectoryPersistence.loadObject(new File(path, "NN_" + modelID)), modelID, inputMaxes, inputMins, targetMaxes, targetMins);
+  }
+
+  /**
+   * Constructor for when the neural network is loaded from an input stream.
+   * Please note that the normalizer are note loaded file , because it is assumed that the mins and maxes are saved anyway in the meta data of the data sets or items.
+   *
+   * @param input       stream to load the model from
+   * @param modelID     ID of the BasicNetwork.
+   * @param inputMaxes  list that contains max values of all input columns (sensors) e.g. light intensity 100
+   * @param inputMins   list that contains min values of all input columns (sensors) e.g. light intensity 0
+   * @param targetMaxes list that contains max values of all output columns (results) e.g. brightness 100 for preference learning
+   * @param targetMins  list that contains min values of all output columns (results) e.g. brightness 0 for preference learning
+   */
+  public Network(InputStream input, int modelID, List<Integer> inputMaxes, List<Integer> inputMins, List<Integer> targetMaxes,
+                 List<Integer> targetMins) {
+    this(() -> (BasicNetwork) EncogDirectoryPersistence.loadObject(input), modelID, inputMaxes, inputMins, targetMaxes, targetMins);
+  }
+
+  private Network(LoadEncogModel loader, int modelID, List<Integer> inputMaxes, List<Integer> inputMins, List<Integer> targetMaxes,
+                 List<Integer> targetMins) {
+    this.modelID = modelID;
+
+    normalizersIn = new ArrayList<>();
+    normalizersTar = new ArrayList<>();
+
+    network = loader.load();
+
+    addNormalizer(inputMaxes, inputMins, normalizersIn);
+    addNormalizer(targetMaxes, targetMins, normalizersTar);
+  }
+
+  @FunctionalInterface
+  interface LoadEncogModel {
+    BasicNetwork load();
+  }
+
+  /**
+   * Method to save the trained {@link BasicNetwork} to a file.
+   * File name is always NN_modelID
+   *
+   * @param savePath path to the folder in which the model file should be placed
+   */
+  public void saveModel(String savePath) {
+    EncogDirectoryPersistence.saveObject(new File(savePath + "NN_" + modelID), network);
+  }
+
+  /**
+   * Method for training the {@link BasicNetwork}.
+   * One row training is implemented here for now to optimize preference learning.
+   *
+   * @param input  input part of the data row which should be used for training
+   * @param target target/output part of the data row which should be used for training
+   */
+  public void train(List<Double> input, List<Double> target) {
+    double[][] INPUT = new double[1][input.size()];
+    double[][] IDEAL = new double[1][target.size()];
+
+    for (int i = 0; i < input.size(); i++) {
+      INPUT[0][i] = normalizersIn.get(i).normalize(input.get(i));
+    }
+
+    for (int j = 0; j < target.size(); j++) {
+      IDEAL[0][j] = normalizersTar.get(j).normalize(target.get(j));
+    }
+
+    MLDataSet trainingSet = new BasicMLDataSet(INPUT, IDEAL);
+    MLTrain train = new Backpropagation(network, trainingSet, 3.5, 0.3);
+
+    do {
+      train.iteration();
+    } while (train.getError() > 0.005);
+
+    train.finishTraining();
+  }
+
+  /**
+   * Method that uses the {@link BasicNetwork} to predict/classify/.. something based on an input.
+   *
+   * @param inputVector data that should be processed
+   */
+  public double[] computeResult(List<Double> inputVector) {
+    double[] output = new double[normalizersTar.size()];
+    double[] input = new double[inputVector.size()];
+
+    for (int i = 0; i < inputVector.size(); i++) {
+      input[i] = normalizersIn.get(i).normalize(inputVector.get(i));
+    }
+
+    network.compute(input, output);
+
+    for (int j = 0; j < normalizersTar.size(); j++) {
+      output[j] = normalizersTar.get(j).deNormalize(output[j]);
+    }
+
+    return output;
+  }
+
+  public BasicNetwork getNetwork() {
+    return network;
+  }
+
+  public ArrayList<NormalizedField> getNormalizersIn() {
+    return normalizersIn;
+  }
+
+  public ArrayList<NormalizedField> getNormalizersTar() {
+    return normalizersTar;
+  }
+}
diff --git a/feedbackloop.learner/src/main/resources/1.eg b/feedbackloop.learner/src/main/resources/1.eg
new file mode 100644
index 0000000000000000000000000000000000000000..03f16b277e8a85c539e7d18fa12bbf0a0b23bbcd
--- /dev/null
+++ b/feedbackloop.learner/src/main/resources/1.eg
@@ -0,0 +1,24 @@
+encog,BasicNetwork,java,3.4.0,1,1549011876959
+[BASIC]
+[BASIC:PARAMS]
+[BASIC:NETWORK]
+beginTraining=0
+connectionLimit=0
+contextTargetOffset=0,0,0
+contextTargetSize=0,0,0
+endTraining=2
+hasContext=f
+inputCount=4
+layerCounts=4,8,5
+layerFeedCounts=4,7,4
+layerContextCount=0,0,0
+layerIndex=0,4,12
+output=0.7897697775,-0.4093295807,-0.9998836757,-1,0.8474456805,-0.2366377403,-0.9973602461,-0.9999999955,-0.4764290146,0.461275072,-0.0649442099,1,0,-0.6666666667,-0.4545454545,0.6949152542,1
+outputCount=4
+weightIndex=0,32,67
+weights=-1.1570349988,-0.8578879992,-1.0549159925,-3.7244521995,0.9574302229,-2.0931447828,0.896591315,-1.4483568363,1.4900254439,0.7366819855,0.6613521579,-0.609388338,-2.5552763401,1.2462544768,-0.6792334441,-3.3093626419,-2.155033701,-3.0549866186,-4.46456694,6.3689156226,-0.7553336123,-4.3167151702,-0.2075796591,-0.2387765659,-5.3558179654,0.3870219651,2.1254188856,-0.8070693427,16.3409581377,-0.0181269232,-3.5894371967,-0.6128690116,18.1886181385,-2.1919701152,0.7652143515,-0.5972333161,0.5485570992,2.8445160284,-3.1372144242,-6.0940916584,-12.1412546646,3.3344157392,-1.5344530226,4.1146550246,1.3873868104,1.3560253856,-0.8830370237,3.4646973293,0.45451005,19.2628819453,-1.4746031138,0.1239027404,0.5654616795,-0.3474956993,-0.7700441522,-0.2617909991,-0.9181158122,0.2756671819,0.0437957183,-7.3920564965,-0.4480327205,-2.520553979,-1.2363459836,-0.7189965057,1.2558119854,-0.5462053441,0.4060233037
+biasActivation=0,1,1
+[BASIC:ACTIVATION]
+"org.encog.engine.network.activation.ActivationTANH"
+"org.encog.engine.network.activation.ActivationTANH"
+"org.encog.engine.network.activation.ActivationLinear"
diff --git a/feedbackloop.learner/src/main/resources/NN_1 b/feedbackloop.learner/src/main/resources/NN_1
new file mode 120000
index 0000000000000000000000000000000000000000..c47a1c155ec39abc4357baa43dbafad60eb2988a
--- /dev/null
+++ b/feedbackloop.learner/src/main/resources/NN_1
@@ -0,0 +1 @@
+1.eg
\ No newline at end of file
diff --git a/feedbackloop.learner/src/main/resources/initial_data.csv b/feedbackloop.learner/src/main/resources/initial_data.csv
new file mode 100644
index 0000000000000000000000000000000000000000..12c125ce2bbdc9ec6bb9762f05f6a83c3b863bb0
--- /dev/null
+++ b/feedbackloop.learner/src/main/resources/initial_data.csv
@@ -0,0 +1,418 @@
+7,20,12,13,2
+7,20,14,40,1
+7,20,14,40,2
+7,21,13,2,2
+7,21,13,2,2
+7,21,14,23,2
+7,21,14,23,2
+7,21,15,41,2
+7,21,16,54,2
+7,21,16,54,2
+7,21,17,45,3
+7,22,12,28,3
+7,22,15,35,2
+7,22,15,35,2
+7,22,18,59,3
+7,22,18,59,3
+7,23,12,32,2
+7,23,12,32,2
+7,23,16,7,2
+7,23,16,7,2
+7,23,16,7,2
+7,23,16,7,2
+7,23,16,7,2
+7,24,12,4,0
+7,24,12,4,0
+7,24,12,4,1
+7,24,14,38,2
+7,24,14,38,2
+7,24,18,54,3
+7,25,12,31,0
+7,25,12,32,1
+7,25,12,32,1
+7,25,15,6,3
+7,25,18,56,3
+7,26,13,41,2
+7,26,19,14,3
+7,27,11,39,2
+7,27,11,39,3
+7,27,11,46,3
+7,27,11,46,2
+7,27,13,8,2
+7,27,13,8,2
+7,27,13,9,2
+7,27,13,45,2
+7,27,13,45,2
+7,27,15,38,3
+7,28,12,12,2
+7,28,12,13,2
+7,28,12,41,2
+7,28,12,41,2
+7,28,12,41,2
+7,28,14,0,1
+7,28,14,0,2
+7,28,15,21,3
+7,28,18,56,3
+7,29,10,9,1
+7,29,10,9,1
+7,29,10,9,1
+7,29,11,54,0
+7,29,11,54,0
+7,29,11,54,0
+7,29,11,54,1
+7,29,14,10,2
+7,29,16,44,2
+7,29,16,44,2
+7,30,16,7,3
+7,30,18,45,3
+7,31,13,2,0
+7,31,13,2,1
+7,31,13,3,1
+7,31,13,3,1
+7,31,13,3,1
+7,31,18,39,3
+8,1,12,22,0
+8,1,12,22,1
+8,1,14,20,2
+8,1,14,20,2
+8,1,14,20,2
+8,1,15,55,3
+8,1,18,31,3
+8,1,18,37,3
+8,1,18,37,3
+8,1,19,2,3
+8,1,19,2,3
+8,1,20,5,3
+8,2,10,9,2
+8,2,10,9,1
+8,2,10,9,2
+8,2,10,9,2
+8,2,13,58,2
+8,2,13,58,2
+8,2,15,44,3
+8,2,15,44,3
+8,2,15,44,3
+8,2,17,21,3
+8,2,17,21,3
+8,2,17,21,3
+8,3,13,31,1
+8,3,13,31,2
+8,3,13,32,2
+8,3,16,43,3
+8,4,13,20,1
+8,4,13,20,2
+8,4,18,27,3
+8,5,13,37,2
+8,5,13,37,2
+8,5,18,33,3
+8,6,11,24,3
+8,6,11,24,3
+8,6,11,24,3
+8,6,13,50,3
+8,7,13,4,2
+8,7,13,4,2
+8,7,14,56,3
+8,8,12,13,2
+8,8,12,13,2
+8,8,15,51,2
+8,8,15,51,2
+8,8,15,51,3
+8,9,13,32,2
+8,9,13,32,2
+8,9,13,32,2
+8,9,15,8,2
+8,9,15,8,2
+8,9,15,8,2
+8,9,16,19,2
+8,10,11,32,0
+8,10,11,32,1
+8,10,11,32,1
+8,10,13,13,1
+8,10,13,13,1
+8,10,13,13,2
+8,10,16,42,3
+8,10,16,42,3
+8,11,14,6,2
+8,11,14,7,2
+8,11,18,54,3
+8,11,18,54,3
+8,11,18,54,3
+8,12,12,27,1
+8,12,12,27,1
+8,12,12,28,1
+8,12,13,53,2
+8,12,13,53,2
+8,12,13,53,2
+8,12,15,21,3
+8,13,13,16,1
+8,13,13,16,1
+8,13,13,16,1
+8,13,14,14,2
+8,13,14,14,2
+8,13,16,11,3
+8,13,17,18,3
+8,14,13,7,1
+8,14,13,7,1
+8,14,13,7,1
+8,14,13,7,1
+8,14,13,7,2
+8,14,13,7,2
+8,14,15,6,3
+8,15,14,5,2
+8,15,14,5,2
+8,15,14,6,2
+8,15,14,6,2
+8,15,16,41,3
+8,15,16,41,3
+8,15,17,30,3
+8,16,13,40,2
+8,16,13,40,2
+8,16,17,52,3
+8,16,17,53,3
+8,17,13,34,1
+8,17,13,35,2
+8,17,14,7,2
+8,17,19,2,3
+8,18,10,21,3
+8,18,11,14,2
+8,18,11,14,2
+8,18,11,14,2
+8,18,11,14,2
+8,18,14,25,2
+8,18,14,25,3
+8,18,14,25,2
+8,18,18,18,3
+8,18,18,19,3
+8,19,18,33,3
+8,19,18,33,3
+8,19,18,33,3
+8,19,18,33,3
+8,20,14,28,2
+8,20,14,28,2
+8,20,14,28,2
+8,20,14,28,2
+8,20,17,8,3
+8,20,18,22,3
+8,21,11,24,1
+8,21,11,24,1
+8,21,11,24,1
+8,21,15,34,3
+8,21,18,55,3
+8,22,12,3,1
+8,22,12,4,2
+8,22,12,4,2
+8,22,13,51,2
+8,22,13,51,2
+8,22,13,51,2
+8,22,18,12,3
+8,22,18,12,3
+8,22,18,12,3
+8,22,18,12,3
+8,22,18,40,3
+8,22,18,40,3
+8,23,13,42,1
+8,23,13,42,1
+8,23,17,32,3
+8,23,19,28,3
+8,23,20,27,3
+8,23,20,27,3
+8,23,21,49,3
+8,24,14,0,2
+8,24,14,0,2
+8,24,14,0,2
+8,24,14,0,2
+8,24,15,4,3
+8,24,15,4,3
+8,24,16,2,3
+8,24,16,3,3
+8,24,16,37,3
+8,24,17,9,3
+8,24,17,14,3
+8,25,13,34,1
+8,25,13,34,1
+8,25,13,34,1
+8,25,13,34,1
+8,25,13,34,1
+8,25,15,1,3
+8,25,17,58,3
+8,26,10,29,0
+8,26,10,29,0
+8,26,10,29,0
+8,26,10,29,0
+8,26,10,29,0
+8,26,16,42,3
+8,26,16,42,3
+8,26,18,41,3
+8,26,18,41,3
+8,27,13,41,2
+8,27,13,41,2
+8,27,13,41,2
+8,27,13,41,2
+8,27,17,42,3
+8,28,11,9,1
+8,28,11,9,1
+8,28,12,14,0
+8,28,12,14,1
+8,28,12,14,0
+8,28,15,3,2
+8,28,15,3,2
+8,28,16,31,3
+8,28,17,40,3
+8,29,14,44,3
+8,29,17,25,3
+8,30,12,5,0
+8,30,12,5,0
+8,30,12,5,0
+8,30,13,32,1
+8,30,13,32,1
+8,30,13,56,2
+8,30,14,23,2
+8,30,14,23,2
+8,30,14,23,2
+8,30,14,23,2
+8,30,14,41,2
+8,30,14,41,2
+8,30,14,41,2
+8,30,15,50,3
+8,30,17,0,3
+8,30,18,59,3
+8,30,18,59,3
+8,31,14,31,2
+8,31,14,31,2
+8,31,14,31,2
+8,31,17,59,3
+8,31,18,0,3
+9,1,16,13,3
+9,1,16,13,3
+9,1,16,13,3
+9,1,17,41,3
+9,2,13,44,1
+9,2,13,44,1
+9,2,13,44,1
+9,2,14,49,2
+9,2,14,49,2
+9,2,14,49,2
+9,2,16,6,3
+9,2,16,6,3
+9,2,17,2,3
+9,3,16,9,3
+9,3,17,35,3
+9,3,17,36,3
+9,4,12,57,1
+9,4,12,57,1
+9,4,15,8,3
+9,4,15,34,3
+9,4,16,26,3
+9,4,16,26,3
+9,4,18,37,3
+9,4,18,37,3
+9,4,18,37,3
+9,6,11,18,0
+9,6,11,18,0
+9,6,12,54,1
+9,6,12,54,1
+9,6,14,21,2
+9,6,14,21,2
+9,6,19,20,3
+9,7,11,50,0
+9,7,14,17,2
+9,7,14,57,3
+9,7,14,57,3
+9,7,16,56,3
+9,7,16,56,3
+9,7,16,56,3
+9,7,16,56,3
+9,7,18,38,3
+9,7,18,38,3
+9,8,11,4,2
+9,8,11,4,2
+9,8,11,13,0
+9,8,11,13,0
+9,8,11,13,0
+9,8,11,13,0
+9,8,11,13,0
+9,8,11,14,0
+9,8,11,14,1
+9,8,11,14,1
+9,8,12,1,0
+9,8,12,1,0
+9,8,12,1,0
+9,8,12,1,0
+9,8,12,1,0
+9,8,12,1,1
+9,8,12,36,0
+9,8,12,36,0
+9,8,12,36,0
+9,8,12,36,0
+9,8,12,36,0
+9,8,13,37,1
+9,8,13,37,1
+9,8,13,37,1
+9,8,14,20,2
+9,8,14,20,2
+9,8,18,20,3
+9,9,12,47,1
+9,9,12,47,2
+9,9,12,47,2
+9,9,19,5,3
+9,10,13,15,1
+9,10,13,15,1
+9,10,13,15,0
+9,10,16,49,3
+9,10,19,6,3
+9,10,21,5,3
+9,11,14,16,2
+9,11,14,16,2
+9,11,14,16,2
+9,11,18,41,3
+9,12,14,43,2
+9,12,14,43,2
+9,12,14,43,2
+9,12,16,14,3
+9,12,17,12,3
+9,12,17,12,2
+9,12,17,12,3
+9,12,17,12,2
+9,12,20,44,3
+9,13,19,52,3
+9,14,14,39,2
+9,14,14,39,2
+9,14,15,14,3
+9,14,17,29,3
+9,14,17,29,3
+9,14,17,29,3
+9,15,11,41,1
+9,15,11,41,1
+9,15,13,4,1
+9,15,14,3,1
+9,15,14,3,2
+9,16,12,36,1
+9,16,12,36,1
+9,16,12,36,1
+9,16,12,36,1
+9,16,12,48,1
+9,16,12,48,1
+9,16,13,51,1
+9,16,13,51,2
+9,16,13,51,1
+9,16,15,13,3
+9,16,15,14,3
+9,16,15,14,3
+9,17,10,27,0
+9,17,10,27,0
+9,17,11,10,0
+9,17,11,10,0
+9,17,11,10,0
+9,17,12,43,1
+9,17,12,43,1
+9,17,12,43,1
+9,17,13,32,1
+9,17,13,32,1
+9,17,14,5,1
+9,17,14,5,2
+9,17,14,6,2
+9,17,15,7,3
+9,17,15,49,3
+9,17,15,49,3
+9,17,18,12,3
+9,17,18,13,3
diff --git a/feedbackloop.learner/src/main/resources/log4j2.xml b/feedbackloop.learner/src/main/resources/log4j2.xml
index 481178a66aaaf356ef87241bf07871f4e808c3ee..867ec439d0a32dcb5f8b3e2d0c7485d7d8da418c 100644
--- a/feedbackloop.learner/src/main/resources/log4j2.xml
+++ b/feedbackloop.learner/src/main/resources/log4j2.xml
@@ -4,8 +4,8 @@
         <Console name="Console">
             <PatternLayout pattern="%highlight{%d{HH:mm:ss.SSS} %-5level} %c{1.} - %msg%n"/>
         </Console>
-        <RollingFile name="RollingFile" fileName="logs/jastadd-mquat.log"
-                    filePattern="logs/jastadd-mquat-%i.log">
+        <RollingFile name="RollingFile" fileName="logs/eraser.log"
+                    filePattern="logs/eraser-%i.log">
             <PatternLayout pattern="%d{HH:mm:ss.SSS} %-5level %logger{36} - %msg%n"/>
             <Policies>
                 <OnStartupTriggeringPolicy/>
@@ -19,4 +19,4 @@
             <AppenderRef ref="RollingFile"/>
         </Root>
     </Loggers>
-</Configuration>
\ No newline at end of file
+</Configuration>
diff --git a/feedbackloop.learner_backup/.gitignore b/feedbackloop.learner_backup/.gitignore
new file mode 100644
index 0000000000000000000000000000000000000000..54c5c0b024dab04da2a43d4135ed67ed2a670b46
--- /dev/null
+++ b/feedbackloop.learner_backup/.gitignore
@@ -0,0 +1,2 @@
+/build/
+logs/
diff --git a/feedbackloop.learner_backup/build.gradle b/feedbackloop.learner_backup/build.gradle
new file mode 100644
index 0000000000000000000000000000000000000000..6199fa8da5b905d3d5d4cb02d6c6858218f3ffdd
--- /dev/null
+++ b/feedbackloop.learner_backup/build.gradle
@@ -0,0 +1,34 @@
+apply plugin: 'application'
+
+dependencies {
+    compile project(':eraser-base')
+    compile group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: '2.9.8'
+    compile group: 'org.apache.logging.log4j', name: 'log4j-api', version: '2.11.2'
+    compile group: 'org.apache.logging.log4j', name: 'log4j-core', version: '2.11.2'
+    testCompile group: 'junit', name: 'junit', version: '4.12'
+    testCompile group: 'org.hamcrest', name: 'hamcrest-junit', version: '2.0.0.0'
+    compile group: 'org.encog', name: 'encog-core', version: '3.4'
+    implementation group: 'com.opencsv', name: 'opencsv', version: '4.1'
+    implementation group: 'commons-io', name: 'commons-io', version: '2.5'
+    implementation group: 'org.apache.httpcomponents', name: 'httpclient', version: '4.5.8'
+    implementation group: 'org.apache.httpcomponents', name: 'fluent-hc', version: '4.5.8'
+    // https://mvnrepository.com/artifact/org.apache.spark/spark-mllib
+    //runtime group: 'org.apache.spark', name: 'spark-mllib_2.10', version: '1.3.0'
+    compile group: 'com.sparkjava', name: 'spark-core', version: '2.9.0'
+}
+
+run {
+    mainClassName = 'de.tudresden.inf.st.eraser.feedbackloop.learner_backup.Main'
+    standardInput = System.in
+    if (project.hasProperty("appArgs")) {
+        args Eval.me(appArgs)
+    }
+}
+
+sourceSets {
+    main {
+        java {
+            srcDir 'src/main/java'
+        }
+    }
+}
diff --git a/feedbackloop.learner_backup/src/main/java/de/tudresden/inf/st/eraser/feedbackloop.learner_backup/CsvTransfer.java b/feedbackloop.learner_backup/src/main/java/de/tudresden/inf/st/eraser/feedbackloop.learner_backup/CsvTransfer.java
new file mode 100644
index 0000000000000000000000000000000000000000..1c08268d59dd4003149e551d2e3e60895d30e3f6
--- /dev/null
+++ b/feedbackloop.learner_backup/src/main/java/de/tudresden/inf/st/eraser/feedbackloop.learner_backup/CsvTransfer.java
@@ -0,0 +1,60 @@
+package de.tudresden.inf.st.eraser.feedbackloop.learner_backup;
+
+import java.io.*;
+import java.util.*;
+
+import com.opencsv.CSVReader;
+import com.opencsv.CSVWriter;
+
+
+public class CsvTransfer {
+    public static void main(String[] args){
+        Learner learner=new Learner();
+        learner.train("datasets/backup/activity_data.csv","datasets/backup/preference_data.csv");
+    }
+
+    //learner.train("datasets/backup/activity_data.csv","datasets/backup/preference_data.csv");
+    /**private static final String CSV_FILE_PATH
+            = "datasets/backup/activity_data_example.csv";
+    private static final String OUTPUT_FILE_PATH
+            = "datasets/backup/activity_data.csv";
+    public static void main(String[] args)
+    {
+        addDataToCSV(CSV_FILE_PATH, OUTPUT_FILE_PATH);
+    }
+    public static void addDataToCSV(String input, String output)
+    {
+        File input_file = new File(input);
+        File output_file = new File(output);
+        try {
+            // create FileWriter object with file as parameter
+            FileReader reader = new FileReader(input_file);
+            CSVReader csv_reader = new CSVReader(reader);
+            String[] nextRecord;
+            FileWriter writer = new FileWriter(output_file);
+            CSVWriter csv_writer = new CSVWriter(writer, ',',
+                    CSVWriter.NO_QUOTE_CHARACTER,
+                    CSVWriter.DEFAULT_ESCAPE_CHARACTER,
+                    CSVWriter.DEFAULT_LINE_END);
+            List<String[]> data = new ArrayList<String[]>();
+
+            while ((nextRecord = csv_reader.readNext()) != null) {
+                data.add(nextRecord);
+
+                for (String cell : nextRecord) {
+                    System.out.print(cell + "\t");
+                }
+                System.out.println();
+            }
+            csv_writer.writeAll(data);
+            writer.close();
+            csv_reader.close();
+        }
+        catch (Exception e) {
+            // TODO Auto-generated catch block
+            e.printStackTrace();
+        }
+    }*/
+}
+
+
diff --git a/feedbackloop.learner_backup/src/main/java/de/tudresden/inf/st/eraser/feedbackloop.learner_backup/DummyPreference.java b/feedbackloop.learner_backup/src/main/java/de/tudresden/inf/st/eraser/feedbackloop.learner_backup/DummyPreference.java
new file mode 100644
index 0000000000000000000000000000000000000000..59f3ad9f43fd11eab315e145b1780d4152b82181
--- /dev/null
+++ b/feedbackloop.learner_backup/src/main/java/de/tudresden/inf/st/eraser/feedbackloop.learner_backup/DummyPreference.java
@@ -0,0 +1,75 @@
+package de.tudresden.inf.st.eraser.feedbackloop.learner_backup;
+import com.opencsv.CSVWriter;
+
+import java.io.FileWriter;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Random;
+
+public class DummyPreference {
+    private static String activity;  //Activity: walking, reading, working, dancing, lying, getting up
+    private static String watch_brightness; //dark: <45; dimmer 45-70; bright >70;
+    private static String light_color_openhab_H; //red 7; green 120; blue 240; yellow 60; sky blue 180; purple 300;
+    private static String brightness_output; //1-100**/
+    private static Random random = new Random();
+
+    public static void main(String[] args) {
+        creator();
+    }
+    static void creator(){
+
+
+        try{
+            FileWriter writer = new FileWriter("datasets/backup/preference_data.csv",true);
+            CSVWriter csv_writer = new CSVWriter(writer, ',',
+                    CSVWriter.NO_QUOTE_CHARACTER,
+                    CSVWriter.DEFAULT_ESCAPE_CHARACTER,
+                    CSVWriter.DEFAULT_LINE_END);
+
+
+            //activity="walking" green
+            activity ="walking";
+
+           // activity ="reading";
+            csv_writer.writeAll(generator("walking","green"));
+            csv_writer.writeAll(generator("reading","sky blue"));
+            csv_writer.writeAll(generator("working","blue"));
+            csv_writer.writeAll(generator("dancing","purple"));
+            csv_writer.writeAll(generator("lying","red"));
+            csv_writer.writeAll(generator("getting up","yellow"));
+            csv_writer.close();
+            writer.close();
+        }catch (IOException e){e.printStackTrace();}
+    }
+    static List<String[]> generator(String activity_input, String color){
+        List<String[]> data = new ArrayList<String[]>();
+        activity = activity_input;
+        light_color_openhab_H =color;
+        //100 walking with different lighting intensity
+        for (int i=0; i<100; i++){
+            String[] add_data = new String[4];
+            int brightness = random.nextInt(3000);
+            System.out.println(brightness);
+            if (brightness<45){
+                watch_brightness = "dark";
+                brightness_output ="100";
+            }else if(45<=brightness && brightness<200){
+                watch_brightness = "dimmer";
+                brightness_output ="40";
+            }else if( 200<=brightness && brightness<1000){
+                watch_brightness = "medium";
+                brightness_output ="70";
+            }else{
+                watch_brightness = "bright";
+                brightness_output ="0";
+            }
+            add_data[0] = activity;
+            add_data[1] = watch_brightness;
+            add_data[2] = light_color_openhab_H;
+            add_data[3] = brightness_output;
+            data.add(add_data);
+        }
+        return data;
+    }
+}
diff --git a/feedbackloop.learner_backup/src/main/java/de/tudresden/inf/st/eraser/feedbackloop.learner_backup/Learner.java b/feedbackloop.learner_backup/src/main/java/de/tudresden/inf/st/eraser/feedbackloop.learner_backup/Learner.java
new file mode 100644
index 0000000000000000000000000000000000000000..5d8021d8faf7519d9ec15305f0752cfbcd742a0c
--- /dev/null
+++ b/feedbackloop.learner_backup/src/main/java/de/tudresden/inf/st/eraser/feedbackloop.learner_backup/Learner.java
@@ -0,0 +1,191 @@
+package de.tudresden.inf.st.eraser.feedbackloop.learner_backup;
+
+import java.io.File;
+import java.io.IOException;
+//import java.util.ArrayList;
+//import com.sun.javafx.tools.packager.Log;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+//import org.encog.ConsoleStatusReportable;
+import org.encog.Encog;
+import org.encog.ml.MLClassification;
+//import org.encog.ml.MLRegression;
+import org.encog.ml.data.MLData;
+import org.encog.ml.data.versatile.NormalizationHelper;
+import org.encog.ml.data.versatile.VersatileMLDataSet;
+import org.encog.ml.data.versatile.columns.ColumnDefinition;
+import org.encog.ml.data.versatile.columns.ColumnType;
+import org.encog.ml.data.versatile.sources.CSVDataSource;
+import org.encog.ml.data.versatile.sources.VersatileDataSource;
+import org.encog.ml.factory.MLMethodFactory;
+import org.encog.ml.model.EncogModel;
+import org.encog.neural.networks.BasicNetwork;
+import org.encog.util.csv.CSVFormat;
+import static org.encog.persist.EncogDirectoryPersistence.*;
+
+public class Learner {
+
+    /**
+     * intial train
+     * */
+    private File save_activity_model_file;
+    private File save_preference_model_file;
+    private File csv_file;
+    private VersatileMLDataSet a_data;
+    private VersatileMLDataSet p_data;
+    private EncogModel a_model;
+    private EncogModel p_model;
+    private NormalizationHelper activity_helper;
+    private NormalizationHelper preference_helper;
+    private MLClassification a_best_method;
+    private MLClassification p_best_method;
+    private final Logger logger = LogManager.getLogger(Learner.class);
+
+    public Learner() {
+        try {
+            save_activity_model_file = File.createTempFile("activity_model", "eg");
+        } catch (IOException e) {
+            // use local alternative
+            save_activity_model_file = new File("activity_model.eq");
+        }
+        try {
+            save_preference_model_file = File.createTempFile("preference_model", "eg");
+        } catch (IOException e) {
+            // use local alternative
+            save_preference_model_file = new File("preference_model.eg");
+        }
+        save_activity_model_file.deleteOnExit();
+        save_preference_model_file.deleteOnExit();
+    }
+
+    private void activityDataAnalyser(String activity_csv_url){
+        VersatileDataSource a_souce;
+        String csv_url_activity;
+        csv_url_activity = activity_csv_url;
+        this.csv_file = new File(csv_url_activity);
+        a_souce = new CSVDataSource(csv_file,false,CSVFormat.DECIMAL_POINT);
+        a_data = new VersatileMLDataSet(a_souce);
+        String[] activity_inputs={"m_accel_x","m_accel_y", "m_accel_z",
+                "m_rotation_x","m_rotation_y", "m_rotation_z",
+                "w_accel_x","w_accel_y", "w_accel_z",
+                "w_rotation_x","w_rotation_y","w_rotation_z"
+        };
+        for(int i=0; i < activity_inputs.length; i++){
+            a_data.defineSourceColumn(activity_inputs[i], i, ColumnType.continuous);
+        }
+        ColumnDefinition outputColumn = a_data.defineSourceColumn("labels", 12, ColumnType.nominal);
+        a_data.defineSingleOutputOthersInput(outputColumn);
+        a_data.analyze();
+        a_model = new EncogModel(a_data);
+        a_model.selectMethod(a_data, MLMethodFactory.TYPE_FEEDFORWARD);
+        a_data.normalize();
+        activity_helper = a_data.getNormHelper();
+    }
+
+    private void preferenceDataAnalyser(String preference_csv_url){
+        VersatileDataSource p_source;
+        String csv_url_preference;
+        csv_url_preference = preference_csv_url;
+        this.csv_file = new File(csv_url_preference);
+        p_source = new CSVDataSource(csv_file,false,CSVFormat.DECIMAL_POINT);
+        p_data = new VersatileMLDataSet(p_source);
+        p_data.defineSourceColumn("activity", 0, ColumnType.nominal);
+        p_data.defineSourceColumn("w_brightness", 1, ColumnType.nominal);
+        ColumnDefinition outputColumn1 = p_data.defineSourceColumn("label1", 2, ColumnType.continuous);
+        ColumnDefinition outputColumn2 = p_data.defineSourceColumn("label2", 3, ColumnType.continuous);
+        ColumnDefinition[] outputs = new ColumnDefinition[2];
+        outputs[0] = outputColumn1;
+        outputs[1] = outputColumn2;
+        p_data.defineMultipleOutputsOthersInput(outputs);
+        p_data.analyze();
+        p_model = new EncogModel(p_data);
+        p_model.selectMethod(p_data, MLMethodFactory.TYPE_FEEDFORWARD);
+        p_data.normalize();
+        preference_helper = p_data.getNormHelper();
+    }
+
+    void train(String activity_url,String preference_url){
+        activity_train(activity_url);
+        preference_train(preference_url);
+        Encog.getInstance().shutdown();
+
+    }
+
+    void activity_train(String activity_csv_url){
+        logger.info("Activity training is beginning ... ...");
+        activityDataAnalyser(activity_csv_url);
+        a_model.holdBackValidation(0.3, true, 1001);
+        a_model.selectTrainingType(a_data);
+        a_best_method = (MLClassification)a_model.crossvalidate(5, true);
+        saveEncogModel(save_activity_model_file);
+        logger.info("Activity training is finished ... ...");
+        Encog.getInstance().shutdown();
+    }
+    void preference_train(String prefence_csv_url){
+        logger.info("Preference training is beginning ... ...");
+        preferenceDataAnalyser(prefence_csv_url);
+        p_model.holdBackValidation(0.3, true, 1001);
+        p_model.selectTrainingType(p_data);
+        p_best_method = (MLClassification)p_model.crossvalidate(5, true);
+        saveEncogModel(save_preference_model_file);
+        logger.info("Preference training is finished ... ...");
+        Encog.getInstance().shutdown();
+    }
+
+    String[] predictor(String[] new_data){
+        String[] preference_data = new String[2];
+        String[] result = new String[3];
+        String[] activity_data= new String[12];
+        for(int i=0; i<new_data.length;i++){
+            activity_data[i]=new_data[i];
+        }
+        result[0] = activity_predictor(activity_data);
+        preference_data[0]=result[0];
+        preference_data[1]=new_data[12];
+        result[1] = preference_predictor(preference_data)[0];
+        result[2] = preference_predictor(preference_data)[1];
+        Encog.getInstance().shutdown();
+        return result;
+    }
+
+    String activity_predictor(String[] new_data){
+        logger.info("Activity predicting ... ...");
+        String activity_result;
+        activityDataAnalyser("../datasets/backup/activity_data.csv");
+        BasicNetwork activity_method = (BasicNetwork) loadObject(save_activity_model_file);
+        MLData input = activity_helper.allocateInputVector();
+        activity_helper.normalizeInputVector(new_data,input.getData(),false);
+        MLData output = activity_method.compute(input);
+        activity_result = activity_helper.denormalizeOutputVectorToString(output)[0];
+        Encog.getInstance().shutdown();
+        logger.debug("Activity Predictor result is: {}",activity_result);
+        return activity_result;
+    }
+
+    String[] preference_predictor(String[] new_data){
+        logger.info("Activity predicting ... ...");
+        String[] preference_result;
+        preference_result = new String[2];
+        preferenceDataAnalyser("../datasets/backup/preference_data.csv");
+        BasicNetwork preference_method = (BasicNetwork)loadObject(save_preference_model_file);
+        MLData input = preference_helper.allocateInputVector();
+        preference_helper.normalizeInputVector(new_data, input.getData(),false);
+        MLData output = preference_method.compute(input);
+        preference_result[0] = preference_helper.denormalizeOutputVectorToString(output)[0];
+        preference_result[1] = preference_helper.denormalizeOutputVectorToString(output)[1];
+        Encog.getInstance().shutdown();
+        logger.debug("Preference Predictor result is, Color:  {}",Math.round(Float.valueOf(preference_result[0])));
+        logger.debug("Preference Predictor result is, Brightness: {} ",Math.round(Float.valueOf(preference_result[1])));
+        return preference_result;
+    }
+    private void saveEncogModel(File modelFile){
+        if (modelFile.equals(save_activity_model_file)) {
+          saveObject(modelFile, this.a_best_method);
+        } else {
+            saveObject(modelFile, this.p_best_method);
+        }
+
+    }
+}
+
+
diff --git a/feedbackloop.learner_backup/src/main/java/de/tudresden/inf/st/eraser/feedbackloop.learner_backup/MachineLearningImpl.java b/feedbackloop.learner_backup/src/main/java/de/tudresden/inf/st/eraser/feedbackloop.learner_backup/MachineLearningImpl.java
new file mode 100644
index 0000000000000000000000000000000000000000..dc16aa2353ea216a49d2d1e75d8cfcff4d36abbc
--- /dev/null
+++ b/feedbackloop.learner_backup/src/main/java/de/tudresden/inf/st/eraser/feedbackloop.learner_backup/MachineLearningImpl.java
@@ -0,0 +1,211 @@
+package de.tudresden.inf.st.eraser.feedbackloop.learner_backup;
+
+
+
+//import com.sun.javafx.tools.packager.Log;
+import de.tudresden.inf.st.eraser.jastadd.model.*;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+import java.time.Instant;
+import java.util.*;
+import java.util.stream.Collectors;
+
+
+public class MachineLearningImpl implements MachineLearningDecoder, MachineLearningEncoder {
+
+  private List<Item> target_item_list;
+  private List<Item> relevant_item_list;
+  private String[] preference_result;
+
+  private Root root;
+  private final Logger logger = LogManager.getLogger(MachineLearningImpl.class);
+  private final Learner learner;
+  private final int goal;
+
+  public static final int GOAL_ACTIVITY_PHONE_AND_WATCH = 1;
+  public static final int GOAL_PREFERENCE_BRIGHTNESS_IRIS = 2;
+  public String activity_result;
+  private String[] activites= new String[]{"working", "walking", "dancing", "lying", "getting up", "reading"};
+  public MachineLearningImpl(Learner learner, int goal) {
+    this.learner = learner;
+    this.goal = goal;
+  }
+
+  @Override
+  public void setKnowledgeBaseRoot(Root root) {
+    this.root = root;
+    updateItems();
+  }
+
+  private void updateItems() {
+    OpenHAB2Model model = root.getOpenHAB2Model();
+    List<String> targetItemNames, relevantItemNames;
+    switch (this.goal) {
+      case GOAL_ACTIVITY_PHONE_AND_WATCH:
+        targetItemNames = Collections.singletonList("activity");
+        relevantItemNames = Arrays.asList(
+            "m_accel_x",
+            "m_accel_y",
+            "m_accel_z",
+            "m_rotation_x",
+            "m_rotation_y",
+            "m_rotation_z",
+            "w_accel_x",
+            "w_accel_y",
+            "w_accel_z",
+            "w_rotation_x",
+            "w_rotation_y",
+            "w_rotation_z"
+        );
+        break;
+      case GOAL_PREFERENCE_BRIGHTNESS_IRIS:
+        targetItemNames = Collections.singletonList("iris1_item");
+        relevantItemNames = Arrays.asList(
+            "activity",
+            "w_brightness"
+        );
+        break;
+      default:
+        logger.error("Unknown goal value ({}) set", this.goal);
+        targetItemNames = Collections.emptyList();
+        relevantItemNames = Collections.emptyList();
+    }
+    target_item_list = targetItemNames.stream()
+        .map(name -> resolve(model, name))
+        .filter(Objects::nonNull)
+        .collect(Collectors.toList());
+    relevant_item_list = relevantItemNames.stream()
+        .map(name -> resolve(model, name))
+        .filter(Objects::nonNull)
+        .collect(Collectors.toList());
+  }
+
+  private Item resolve(OpenHAB2Model model, String id) {
+    Optional<Item> maybeItem = model.resolveItem(id);
+    if (maybeItem.isPresent()) {
+      return maybeItem.get();
+    } else {
+      logger.warn("Could not find item with id {}", id);
+      return null;
+    }
+  }
+
+  @Override
+  public void newData(List<Item> changedItems) {
+    /* String topic = changedItems.get(0).getTopic().toString();
+     if(topic.equals("oh2/samsung/items1")){
+     new_data[0]=changedItems.get(0).influxMeasurementName();
+     }
+     model.getOpenHAB2Model().items();
+     Item iris_item=model.getOpenHAB2Model().resolveItem("iris_item").get();
+     iris_item.getStateAsString();*/
+    /* FIXME either save state of unchanged items here (if only changed items are reported) <- pull model
+             or let knowledge base pass all relevant items <- push model
+    */
+    if(this.goal==GOAL_ACTIVITY_PHONE_AND_WATCH){
+      String[] new_data = new String[12];
+      for (int i =0; i< new_data.length; i++){
+        new_data[i] ="0";
+      }
+      for(Item item: changedItems){
+        int i = 0;
+        for(Item item1: relevant_item_list){
+          if(item.getTopic().toString().equals(item1.getTopic().toString())){
+            new_data[i]=item.getStateAsString();
+          }
+          i++;
+        }
+      }
+      this.activity_result = learner.activity_predictor(new_data);
+
+    }else if(this.goal==GOAL_PREFERENCE_BRIGHTNESS_IRIS){
+      String[] new_data = new String[2];
+      for(Item item: changedItems){
+        if(root.getOpenHAB2Model().getActivityItem().equals(item))
+        {
+          String test=item.getStateAsString();
+          int index = Math.round(Float.valueOf(test));
+          new_data[0]=activites[index];
+        }
+        if(item.getID().equals("w_brightness")){
+          new_data[1]=item.getStateAsString();
+        }
+      }
+      this.preference_result=learner.preference_predictor(new_data);
+    }
+  }
+
+  @Override
+  public List<Item> getTargets() {
+    return target_item_list;
+  }
+
+  @Override
+  public List<Item> getRelevantItems() {
+    return relevant_item_list;
+  }
+
+  @Override
+  public void triggerTraining() {
+    logger.debug("Ignore trigger training call");
+  }
+
+  @Override
+  public Instant lastModelUpdate() {
+    return null;
+  }
+
+  @Override
+  public MachineLearningResult classify() {
+    switch (this.goal) {
+      case GOAL_ACTIVITY_PHONE_AND_WATCH:
+        String activityStringValue = activity_result;
+        Item activityItem = resolve(this.root.getOpenHAB2Model(), "activity");
+        //activityItem.setStateFromString(activityStringValue);
+        // FIXME how to translate activityStringValue to a number? or should activity item state better be a String?
+        for (int i=0; i< activites.length;i++){
+          if(activites[i].equals(activityStringValue)){
+            activityItem.setStateFromString(String.valueOf(i));
+          }
+        }
+        logger.debug("Classify would return activity: {}", activityStringValue);
+        ItemPreference classifiedActivity = new ItemPreferenceDouble(activityItem, 0);
+        return new MachineLearningResultImpl(classifiedActivity);
+      case GOAL_PREFERENCE_BRIGHTNESS_IRIS:
+//        String[] preference = {result[1], result[2]};
+        // FIXME what is the meaning of result[1] and result[2]
+        Item iris1 = resolve(this.root.getOpenHAB2Model(), "iris1_item");
+        int color = 0;
+        int brightness = 0;
+        if (preference_result != null){
+          color = Math.round(Float.valueOf(preference_result[0]));
+          brightness = Math.round(Float.valueOf(preference_result[1]));
+        }
+
+        ItemPreference classifiedPreference = new ItemPreferenceColor(iris1, TupleHSB.of(color, 100, brightness));
+        return new MachineLearningResultImpl(classifiedPreference);
+      default:
+        logger.error("Unknown goal value ({}) set in classify", this.goal);
+        return new EmptyMachineLearningResult();
+    }
+  }
+
+  public void initActivities(String filenameOfCsv) {
+    logger.debug(filenameOfCsv);
+    learner.activity_train(filenameOfCsv);
+  }
+
+  public void initPreferences(String filenameOfCsv) {
+    learner.preference_train(filenameOfCsv);
+  }
+
+  class EmptyMachineLearningResult implements MachineLearningResult {
+
+    @Override
+    public List<ItemPreference> getPreferences() {
+      return Collections.emptyList();
+    }
+  }
+
+}
diff --git a/feedbackloop.learner_backup/src/main/java/de/tudresden/inf/st/eraser/feedbackloop.learner_backup/MachineLearningResultImpl.java b/feedbackloop.learner_backup/src/main/java/de/tudresden/inf/st/eraser/feedbackloop.learner_backup/MachineLearningResultImpl.java
new file mode 100644
index 0000000000000000000000000000000000000000..61d483a566255d51ce424d23adb014df1eec8109
--- /dev/null
+++ b/feedbackloop.learner_backup/src/main/java/de/tudresden/inf/st/eraser/feedbackloop.learner_backup/MachineLearningResultImpl.java
@@ -0,0 +1,26 @@
+package de.tudresden.inf.st.eraser.feedbackloop.learner_backup;
+
+import de.tudresden.inf.st.eraser.jastadd.model.ItemPreference;
+import de.tudresden.inf.st.eraser.jastadd.model.MachineLearningResult;
+
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * TODO: Add description.
+ *
+ * @author rschoene - Initial contribution
+ */
+public class MachineLearningResultImpl implements MachineLearningResult {
+
+  private final ItemPreference preference;
+
+  MachineLearningResultImpl(ItemPreference preference) {
+    this.preference = preference;
+  }
+
+  @Override
+  public List<ItemPreference> getPreferences() {
+    return Collections.singletonList(preference);
+  }
+}
diff --git a/feedbackloop.learner_backup/src/main/java/de/tudresden/inf/st/eraser/feedbackloop.learner_backup/Main.java b/feedbackloop.learner_backup/src/main/java/de/tudresden/inf/st/eraser/feedbackloop.learner_backup/Main.java
new file mode 100644
index 0000000000000000000000000000000000000000..2580847d5ace8e832bde76159dad2e85974115a2
--- /dev/null
+++ b/feedbackloop.learner_backup/src/main/java/de/tudresden/inf/st/eraser/feedbackloop.learner_backup/Main.java
@@ -0,0 +1,164 @@
+package de.tudresden.inf.st.eraser.feedbackloop.learner_backup;
+
+import org.encog.util.csv.CSVFormat;
+import org.encog.util.csv.ReadCSV;
+
+import java.util.Arrays;
+
+public class Main {
+
+  public static void main(String[] args) {
+    /**
+     * new data from KB
+     * */
+    //ReaderCSV reader = new ReaderCSV("datasets/backup/activity_data.csv","preference");
+    //reader.updater();
+    Learner learner=new Learner();
+    //learner.preference_train("../datasets/backup/preference_data.csv");
+    //learner.train("datasets/backup/activity_data.csv","datasets/backup/preference_data.csv");
+    //0.5793968,1.2126632,-4.6244006,-0.030779,0.801127,0.590978,-3.1411927,-0.93373865,-0.31124622,-0.35992432,0.33746338,-0.79608154,dancing
+    /**String[] new_data = new String[12];
+    new_data[0]="0.5793968";
+    new_data[1]="1.2126632";
+    new_data[2]="-4.6244006";
+    new_data[3]="-0.030779";
+    new_data[4]="0.801127";
+    new_data[5]="0.590978";
+    new_data[6]="-3.1411927";
+    new_data[7]="-0.93373865";
+    new_data[8]="-0.31124622";
+    new_data[9]="-0.35992432";
+    new_data[10]="0.33746338";
+    new_data[11]="-0.79608154";
+
+    String result =learner.activity_predictor(new_data);
+    System.out.println(result);
+
+    List k=new ArrayList();
+
+    k.add(new_data);*/
+
+    //learner.preference_train("datasets/backup/preference_data.csv");
+    //learner.train("datasets/backup/activity_data.csv","datasets/backup/preference_data.csv");
+    //walking,medium,120,70
+    //reading,bright,180,0
+
+    //activity_validation_learner();
+    //0.10654198,8.6574335,4.414908,0.040269,0.516884,0.853285,1.2066777,-1.1444284,9.648633,1.2207031E-4,-0.055358887,0.5834961 working
+    String[] new_data = new String[12];
+    new_data[0]="0.010773907";
+    new_data[1]="8.610746";
+    new_data[2]="4.4963107";
+    new_data[3]="0.047136";
+    new_data[4]="0.515427";
+    new_data[5]="0.852877";
+    new_data[6]="0.9720459";
+    new_data[7]="-1.3694834";
+    new_data[8]="9.696517";
+    new_data[9]="-0.0056152344";
+    new_data[10]="-0.049438477";
+    new_data[11]="0.5576782";
+    //0.010773907,8.610746,4.4963107,0.047136,0.515427,0.852877,0.9720459,-1.3694834,9.696517,-0.0056152344,-0.049438477,0.5576782 working
+    //0.9999999988939648,-0.9995820798966354,-0.9999999999999997,-0.9999988062118802,-0.9974031940544938,-1.0
+
+    //String result =learner.activity_predictor(new_data);
+    //System.out.println(result);
+
+    /**String[] new_data_1 =new String[12];
+    //-2.6252422,8.619126,-2.7030537,0.552147,0.5078,0.450302,-8.1881695,-1.2641385,0.038307227,-0.34222412,0.49102783,-0.016540527,walking
+    new_data_1[0]="-2.6252422";
+    new_data_1[1]="8.619126";
+    new_data_1[2]="-2.7030537";
+    new_data_1[3]="0.552147";
+    new_data_1[4]="0.5078";
+    new_data_1[5]="0.450302";
+    new_data_1[6]="-8.1881695";
+    new_data_1[7]="-1.2641385";
+    new_data_1[8]="0.038307227";
+    new_data_1[9]="-0.34222412";
+    new_data_1[10]="0.49102783";
+    new_data_1[11]="-0.016540527";
+    String result1 =learner.activity_predictor(new_data_1);
+    System.out.println(result1);
+    /**
+     * learner.train(activity_csv_url, preference_data_url)
+     * learner.predictor get the result from predictor for new data
+     * */
+    /**String[] new_data_2 = new String[12];
+    //-6.5565214,5.717354,5.6658783,0.185591,0.464146,0.413321,-20.580557,3.8498764,-0.4261679,0.7647095,-0.4713745,0.23999023,dancing
+    new_data_2[0]="-6.5565214";
+    new_data_2[1]="5.717354";
+    new_data_2[2]="5.6658783";
+    new_data_2[3]="0.185591";
+    new_data_2[4]="0.464146";
+    new_data_2[5]="0.413321";
+    new_data_2[6]="-20.580557";
+    new_data_2[7]="3.8498764";
+    new_data_2[8]="-0.4261679";
+    new_data_2[9]="0.7647095";
+    new_data_2[10]="-0.4713745";
+    new_data_2[11]="0.23999023";
+
+    String[] new_data_3 = new String[12];
+    new_data_3[0]="-5.3881507";
+    new_data_3[1]="0.25378537";
+    new_data_3[2]="7.69257";
+    new_data_3[3]="-0.122974";
+    new_data_3[4]="0.247411";
+    new_data_3[5]="0.439031";
+    new_data_3[6]="4.9224787";
+    new_data_3[7]="-10.601525";
+    new_data_3[8]="-4.927267";
+    new_data_3[9]="0.7946167";
+    new_data_3[10]="0.35272217";
+    new_data_3[11]="0.16192627";
+    //"-5.3881507","0.25378537","7.69257","-0.122974","0.247411","0.439031","4.9224787","-10.601525","-4.927267","0.7946167","0.35272217","0.16192627","lying"
+    Learner learner=new Learner();
+    //learner.train("datasets/backup/activity_data.csv", "datasets/preference_data.csv");
+    String[] result = learner.predictor(new_data_3);
+    System.out.println("activity is:" + result[0]);
+    //System.out.println("perference is: "+ result[1]);**/
+  }
+  public static void activity_validation_learner(){
+    ReadCSV csv = new ReadCSV("datasets/backup/activity_data.csv", false, CSVFormat.DECIMAL_POINT);
+    String[] line = new String[12];
+    Learner learner=new Learner();
+    int wrong=0;
+    int right=0;
+    while(csv.next()) {
+      StringBuilder result = new StringBuilder();
+      line[0] = csv.get(0);
+      line[1] = csv.get(1);
+      line[2] = csv.get(2);
+      line[3] = csv.get(3);
+      line[4] = csv.get(4);
+      line[5] = csv.get(5);
+      line[6] = csv.get(6);
+      line[7] = csv.get(7);
+      line[8] = csv.get(8);
+      line[9] = csv.get(9);
+      line[10] = csv.get(10);
+      line[11] = csv.get(11);
+      String correct = csv.get(12);
+      String irisChosen = learner.predictor(line)[0];
+      result.append(Arrays.toString(line));
+      result.append(" -> predicted: ");
+      result.append(irisChosen);
+      result.append("(correct: ");
+      result.append(correct);
+      result.append(")");
+      if (irisChosen.equals(correct)!=true){
+        System.out.println(correct);
+        System.out.println(irisChosen);
+        ++wrong;
+      }else{
+        ++right;
+      }
+      System.out.println(result.toString());
+    }
+    System.out.println("wrong number"+wrong);
+    System.out.println("right number"+right);
+    //double validation = (double(right))/(double(wrong+right));
+    //System.out.println("%.2f"+validation);
+  }
+}
diff --git a/feedbackloop.learner_backup/src/main/java/de/tudresden/inf/st/eraser/feedbackloop.learner_backup/ReaderCSV.java b/feedbackloop.learner_backup/src/main/java/de/tudresden/inf/st/eraser/feedbackloop.learner_backup/ReaderCSV.java
new file mode 100644
index 0000000000000000000000000000000000000000..76c1ce791b00843befc9ed8b9d0f1941532ac386
--- /dev/null
+++ b/feedbackloop.learner_backup/src/main/java/de/tudresden/inf/st/eraser/feedbackloop.learner_backup/ReaderCSV.java
@@ -0,0 +1,116 @@
+package de.tudresden.inf.st.eraser.feedbackloop.learner_backup;
+
+import java.awt.*;
+import java.io.FileReader;
+import java.io.*;
+import java.util.Arrays;
+
+import com.opencsv.CSVReader;
+import org.apache.http.client.fluent.Request;
+import org.apache.http.HttpResponse;
+import org.apache.http.entity.ContentType;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.Condition;
+import java.util.concurrent.locks.ReentrantLock;
+
+public class ReaderCSV{
+
+    //read every 5 s from csv
+    //activity CSV
+    /**
+     * Col 1: smartphone acceleration x
+     * Col 2: smartphone acceleration y
+     * Col 3: smartphone acceleration z
+     * Col 4: smartphone rotation x
+     * Col 5: smartphone rotation y
+     * Col 6: smartphone rotation z
+     * Col 7: watch acceleration x
+     * Col 8: watch acceleration y
+     * Col 9: watch acceleration z
+     * Col 10: watch rotation x
+     * Col 11: watch rotation y
+     * Col 12: watch rotation z/*/
+    //preference CSV
+    /**
+     * Col 1: Activity
+     * Col 2: watch brightness range "bright, medium, dimmer, dark"*/
+
+    private static int TIME_PERIOD = 5000; //5 second update new value
+    private String csv_file_path;
+    private String csv_typ;
+    private File file;
+    private FileReader file_reader;
+    private CSVReader csv_reader;
+    private String[] next_record;
+    private static final Logger logger = LogManager.getLogger(ReaderCSV.class);
+    private static final String ERASER_ITEM_URI = "http://localhost:4567/model/items/";
+    //TODO ITEM_NAME HAS TO BE DISCUSSED
+    private static final String[] ACTIVITY_ITEM_NAME = {
+            "m_accel_x", "m_accel_y", "m_accel_z", "m_rotation_x",
+            "m_rotation_y", "m_rotation_z", "w_accel_x", "w_accel_y",
+            "w_accel_z", "w_rotation_x", "w_rotation_y", "w_rotation_z"};
+    private static final String[] PREFERENCE_ITEM_NAME = {
+           "w_brightness"
+    };
+
+    //csv_type is activity or preference
+    public ReaderCSV(String csv_file_path, String csv_type) {
+        this.csv_file_path = csv_file_path;
+        this.csv_typ = csv_type;
+    }
+
+    public void updater(){
+        file=new File(csv_file_path);
+        try {
+            file_reader =new FileReader(file);
+            csv_reader = new CSVReader(file_reader);
+            while ((next_record = csv_reader.readNext()) != null) {
+                Thread.sleep(TIME_PERIOD);
+                if (csv_typ =="activity"){
+                    String[] values = Arrays.copyOf(next_record,12);
+                    setNewValue(values, ACTIVITY_ITEM_NAME);
+
+               } else {
+                    String[] values = Arrays.copyOf(next_record,2);
+                    setNewValue(values, PREFERENCE_ITEM_NAME);
+                    }
+                }
+            }
+        catch (Exception e){
+            e.printStackTrace();
+        }
+    }
+    private void setNewValue(String[] values, String[] name){
+        if(this.csv_typ.equals("activity"))
+        {
+            int i = 0;
+            for(String value : values){
+                String uri = ERASER_ITEM_URI + name[i] + "/state";
+                logger.info("reader:",value);
+                try {
+                    HttpResponse httpResponse = Request.Put(uri)
+                            .bodyString(value, ContentType.TEXT_PLAIN)
+                            .execute().returnResponse();
+                }catch (Exception e){
+                    e.printStackTrace();
+                }
+                i+=1;
+            }
+
+        }else{
+            String uri= ERASER_ITEM_URI + "w_brightness" +"/state";
+            logger.info("reader:",values[1]);
+            try {
+                HttpResponse httpResponse = Request.Put(uri)
+                        .bodyString(values[1], ContentType.TEXT_PLAIN)
+                        .execute().returnResponse();
+            }catch (Exception e){
+                e.printStackTrace();
+            }
+
+        }
+
+    }
+}
\ No newline at end of file
diff --git a/feedbackloop.learner_backup/src/main/resources/log4j2.xml b/feedbackloop.learner_backup/src/main/resources/log4j2.xml
new file mode 100644
index 0000000000000000000000000000000000000000..867ec439d0a32dcb5f8b3e2d0c7485d7d8da418c
--- /dev/null
+++ b/feedbackloop.learner_backup/src/main/resources/log4j2.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Configuration>
+    <Appenders>
+        <Console name="Console">
+            <PatternLayout pattern="%highlight{%d{HH:mm:ss.SSS} %-5level} %c{1.} - %msg%n"/>
+        </Console>
+        <RollingFile name="RollingFile" fileName="logs/eraser.log"
+                    filePattern="logs/eraser-%i.log">
+            <PatternLayout pattern="%d{HH:mm:ss.SSS} %-5level %logger{36} - %msg%n"/>
+            <Policies>
+                <OnStartupTriggeringPolicy/>
+            </Policies>
+            <DefaultRolloverStrategy max="20"/>
+        </RollingFile>
+    </Appenders>
+    <Loggers>
+        <Root level="debug">
+            <AppenderRef ref="Console"/>
+            <AppenderRef ref="RollingFile"/>
+        </Root>
+    </Loggers>
+</Configuration>
diff --git a/feedbackloop.main/.gitignore b/feedbackloop.main/.gitignore
index 83ccc54d0286590d31587a71aaa789e780bff41e..70b583e34c3316bcd77c807e2d6b85db5e7d49f6 100644
--- a/feedbackloop.main/.gitignore
+++ b/feedbackloop.main/.gitignore
@@ -1,2 +1,3 @@
 /build/
 /bin/
+logs/
diff --git a/feedbackloop.main/build.gradle b/feedbackloop.main/build.gradle
index 2287b7b94c161ad8a3268ca9d751e1c1e2137e88..7e66daa3efddb2ef62735abc2fe6e57bab91d053 100644
--- a/feedbackloop.main/build.gradle
+++ b/feedbackloop.main/build.gradle
@@ -1,10 +1,3 @@
-repositories {
-    mavenCentral()
-}
-
-sourceCompatibility = 1.8
-
-apply plugin: 'java'
 apply plugin: 'application'
 
 dependencies {
@@ -14,11 +7,6 @@ dependencies {
     compile project(':feedbackloop.analyze')
     compile project(':feedbackloop.plan')
     compile project(':feedbackloop.execute')
-    compile group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: '2.9.8'
-    compile group: 'org.apache.logging.log4j', name: 'log4j-api', version: '2.11.1'
-    compile group: 'org.apache.logging.log4j', name: 'log4j-core', version: '2.11.1'
-    testCompile group: 'junit', name: 'junit', version: '4.12'
-    testCompile group: 'org.hamcrest', name: 'hamcrest-junit', version: '2.0.0.0'
 }
 
 run {
diff --git a/feedbackloop.main/src/main/resources/log4j2.xml b/feedbackloop.main/src/main/resources/log4j2.xml
index 0594576fac98ba859e411597c90c8e3d989378bd..867ec439d0a32dcb5f8b3e2d0c7485d7d8da418c 100644
--- a/feedbackloop.main/src/main/resources/log4j2.xml
+++ b/feedbackloop.main/src/main/resources/log4j2.xml
@@ -4,8 +4,8 @@
         <Console name="Console">
             <PatternLayout pattern="%highlight{%d{HH:mm:ss.SSS} %-5level} %c{1.} - %msg%n"/>
         </Console>
-        <RollingFile name="RollingFile" fileName="logs/jastadd-mquat.log"
-                    filePattern="logs/jastadd-mquat-%i.log">
+        <RollingFile name="RollingFile" fileName="logs/eraser.log"
+                    filePattern="logs/eraser-%i.log">
             <PatternLayout pattern="%d{HH:mm:ss.SSS} %-5level %logger{36} - %msg%n"/>
             <Policies>
                 <OnStartupTriggeringPolicy/>
diff --git a/feedbackloop.monitor/.gitignore b/feedbackloop.monitor/.gitignore
index 83ccc54d0286590d31587a71aaa789e780bff41e..70b583e34c3316bcd77c807e2d6b85db5e7d49f6 100644
--- a/feedbackloop.monitor/.gitignore
+++ b/feedbackloop.monitor/.gitignore
@@ -1,2 +1,3 @@
 /build/
 /bin/
+logs/
diff --git a/feedbackloop.monitor/build.gradle b/feedbackloop.monitor/build.gradle
index 77b9513097ddb84f3d1e003489bade769e38c4a7..e3f4c6e283e32e73a0a54ffe1f03d019c81c6d8a 100644
--- a/feedbackloop.monitor/build.gradle
+++ b/feedbackloop.monitor/build.gradle
@@ -1,20 +1,8 @@
-repositories {
-    mavenCentral()
-}
-
-sourceCompatibility = 1.8
-
-apply plugin: 'java'
 apply plugin: 'application'
 
 dependencies {
     compile project(':eraser-base')
     compile project(':feedbackloop.api')
-    compile group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: '2.9.8'
-    compile group: 'org.apache.logging.log4j', name: 'log4j-api', version: '2.11.1'
-    compile group: 'org.apache.logging.log4j', name: 'log4j-core', version: '2.11.1'
-    testCompile group: 'junit', name: 'junit', version: '4.12'
-    testCompile group: 'org.hamcrest', name: 'hamcrest-junit', version: '2.0.0.0'
 }
 
 run {
diff --git a/feedbackloop.monitor/src/main/resources/log4j2.xml b/feedbackloop.monitor/src/main/resources/log4j2.xml
index 0594576fac98ba859e411597c90c8e3d989378bd..867ec439d0a32dcb5f8b3e2d0c7485d7d8da418c 100644
--- a/feedbackloop.monitor/src/main/resources/log4j2.xml
+++ b/feedbackloop.monitor/src/main/resources/log4j2.xml
@@ -4,8 +4,8 @@
         <Console name="Console">
             <PatternLayout pattern="%highlight{%d{HH:mm:ss.SSS} %-5level} %c{1.} - %msg%n"/>
         </Console>
-        <RollingFile name="RollingFile" fileName="logs/jastadd-mquat.log"
-                    filePattern="logs/jastadd-mquat-%i.log">
+        <RollingFile name="RollingFile" fileName="logs/eraser.log"
+                    filePattern="logs/eraser-%i.log">
             <PatternLayout pattern="%d{HH:mm:ss.SSS} %-5level %logger{36} - %msg%n"/>
             <Policies>
                 <OnStartupTriggeringPolicy/>
diff --git a/feedbackloop.plan/.gitignore b/feedbackloop.plan/.gitignore
index 83ccc54d0286590d31587a71aaa789e780bff41e..70b583e34c3316bcd77c807e2d6b85db5e7d49f6 100644
--- a/feedbackloop.plan/.gitignore
+++ b/feedbackloop.plan/.gitignore
@@ -1,2 +1,3 @@
 /build/
 /bin/
+logs/
diff --git a/feedbackloop.plan/build.gradle b/feedbackloop.plan/build.gradle
index 2a9ac05f8f3beb83fab66354c598d27ca84bb4b7..13b20cf6cff29796338f0ac62dd71723fd5ff467 100644
--- a/feedbackloop.plan/build.gradle
+++ b/feedbackloop.plan/build.gradle
@@ -1,20 +1,8 @@
-repositories {
-    mavenCentral()
-}
-
-sourceCompatibility = 1.8
-
-apply plugin: 'java'
 apply plugin: 'application'
 
 dependencies {
     compile project(':eraser-base')
     compile project(':feedbackloop.api')
-    compile group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: '2.9.8'
-    compile group: 'org.apache.logging.log4j', name: 'log4j-api', version: '2.11.1'
-    compile group: 'org.apache.logging.log4j', name: 'log4j-core', version: '2.11.1'
-    testCompile group: 'junit', name: 'junit', version: '4.12'
-    testCompile group: 'org.hamcrest', name: 'hamcrest-junit', version: '2.0.0.0'
 }
 
 run {
diff --git a/feedbackloop.plan/src/main/java/de/tudresden/inf/st/eraser/feedbackloop/plan/PlanImpl.java b/feedbackloop.plan/src/main/java/de/tudresden/inf/st/eraser/feedbackloop/plan/PlanImpl.java
index a8def16ce128d05bef01a09ac79b66ec7c4e555d..e7f256b2928dfbbdd35e48b2dbf5b1797eee727d 100644
--- a/feedbackloop.plan/src/main/java/de/tudresden/inf/st/eraser/feedbackloop/plan/PlanImpl.java
+++ b/feedbackloop.plan/src/main/java/de/tudresden/inf/st/eraser/feedbackloop/plan/PlanImpl.java
@@ -2,8 +2,9 @@ package de.tudresden.inf.st.eraser.feedbackloop.plan;
 
 import de.tudresden.inf.st.eraser.feedbackloop.api.Execute;
 import de.tudresden.inf.st.eraser.feedbackloop.api.Plan;
-import de.tudresden.inf.st.eraser.jastadd.model.ItemPreference;
-import de.tudresden.inf.st.eraser.jastadd.model.Root;
+import de.tudresden.inf.st.eraser.jastadd.model.*;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
 
 import java.util.List;
 
@@ -16,6 +17,7 @@ public class PlanImpl implements Plan {
 
   private Root knowledgeBase;
   private Execute execute;
+  private static final Logger logger = LogManager.getLogger(PlanImpl.class);
 
   @Override
   public void setKnowledgeBase(Root knowledgeBase) {
@@ -33,9 +35,16 @@ public class PlanImpl implements Plan {
   }
 
   @Override
-  public void planToMatchPreferences(String activity) {
-    // TODO implement
+  public void planToMatchPreferences(Activity activity) {
+    logger.info("Plan got new activity [{}]: {}", activity.getIdentifier(), activity.getLabel());
     List<ItemPreference> preferences = knowledgeBase.currentPreferences();
+    knowledgeBase.getMachineLearningRoot().addChangeEvent(createRecognitionEvent(activity));
     informExecute(preferences);
   }
+
+  private ChangeEvent createRecognitionEvent(Activity activity) {
+    RecognitionEvent result = RecognitionEvent.createRecognitionEvent(knowledgeBase.getMachineLearningRoot().getActivityRecognition());
+    result.setActivity(activity);
+    return result;
+  }
 }
diff --git a/feedbackloop.plan/src/main/resources/log4j2.xml b/feedbackloop.plan/src/main/resources/log4j2.xml
index 0594576fac98ba859e411597c90c8e3d989378bd..867ec439d0a32dcb5f8b3e2d0c7485d7d8da418c 100644
--- a/feedbackloop.plan/src/main/resources/log4j2.xml
+++ b/feedbackloop.plan/src/main/resources/log4j2.xml
@@ -4,8 +4,8 @@
         <Console name="Console">
             <PatternLayout pattern="%highlight{%d{HH:mm:ss.SSS} %-5level} %c{1.} - %msg%n"/>
         </Console>
-        <RollingFile name="RollingFile" fileName="logs/jastadd-mquat.log"
-                    filePattern="logs/jastadd-mquat-%i.log">
+        <RollingFile name="RollingFile" fileName="logs/eraser.log"
+                    filePattern="logs/eraser-%i.log">
             <PatternLayout pattern="%d{HH:mm:ss.SSS} %-5level %logger{36} - %msg%n"/>
             <Policies>
                 <OnStartupTriggeringPolicy/>
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index fb7ef980f26942c36accd5674b80128f6cf43abe..030ec1f8a437a05e86702c10632c66340d98ba0f 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -1,5 +1,6 @@
+#Tue May 07 14:40:56 CEST 2019
 distributionBase=GRADLE_USER_HOME
 distributionPath=wrapper/dists
 zipStoreBase=GRADLE_USER_HOME
 zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.2-bin.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.2-all.zip
diff --git a/influx_test/.gitignore b/influx_test/.gitignore
index 84c048a73cc2e5dd24f807669eb99b0ce3123195..70b583e34c3316bcd77c807e2d6b85db5e7d49f6 100644
--- a/influx_test/.gitignore
+++ b/influx_test/.gitignore
@@ -1 +1,3 @@
 /build/
+/bin/
+logs/
diff --git a/influx_test/build.gradle b/influx_test/build.gradle
index fcb387f503c0960fee15af9ffbc5913d536bcb07..50fdeee4c477c0d43ad4ce73436630531a423a32 100644
--- a/influx_test/build.gradle
+++ b/influx_test/build.gradle
@@ -1,21 +1,8 @@
-repositories {
-    mavenCentral()
-}
-
-sourceCompatibility = 1.8
-
-apply plugin: 'java'
 apply plugin: 'application'
 
 dependencies {
     compile project(':eraser-base')
-    compile group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: '2.9.8'
-    compile group: 'org.apache.logging.log4j', name: 'log4j-api', version: '2.11.1'
-    compile group: 'org.apache.logging.log4j', name: 'log4j-core', version: '2.11.1'
-    compile 'org.influxdb:influxdb-java:2.14'
-
-    testCompile group: 'junit', name: 'junit', version: '4.12'
-    testCompile group: 'org.hamcrest', name: 'hamcrest-junit', version: '2.0.0.0'
+    compile group: 'org.influxdb', name: 'influxdb-java', version: '2.15'
 }
 
 run {
diff --git a/influx_test/src/main/resources/log4j2.xml b/influx_test/src/main/resources/log4j2.xml
index 0594576fac98ba859e411597c90c8e3d989378bd..867ec439d0a32dcb5f8b3e2d0c7485d7d8da418c 100644
--- a/influx_test/src/main/resources/log4j2.xml
+++ b/influx_test/src/main/resources/log4j2.xml
@@ -4,8 +4,8 @@
         <Console name="Console">
             <PatternLayout pattern="%highlight{%d{HH:mm:ss.SSS} %-5level} %c{1.} - %msg%n"/>
         </Console>
-        <RollingFile name="RollingFile" fileName="logs/jastadd-mquat.log"
-                    filePattern="logs/jastadd-mquat-%i.log">
+        <RollingFile name="RollingFile" fileName="logs/eraser.log"
+                    filePattern="logs/eraser-%i.log">
             <PatternLayout pattern="%d{HH:mm:ss.SSS} %-5level %logger{36} - %msg%n"/>
             <Policies>
                 <OnStartupTriggeringPolicy/>
diff --git a/integration/.gitignore b/integration/.gitignore
index 83ccc54d0286590d31587a71aaa789e780bff41e..70b583e34c3316bcd77c807e2d6b85db5e7d49f6 100644
--- a/integration/.gitignore
+++ b/integration/.gitignore
@@ -1,2 +1,3 @@
 /build/
 /bin/
+logs/
diff --git a/integration/build.gradle b/integration/build.gradle
index 47be63191b320dce8ea7c0673b15943a1496b634..219d9424d1b27114d05fc74ff704f6e7ce02937b 100644
--- a/integration/build.gradle
+++ b/integration/build.gradle
@@ -1,13 +1,5 @@
-
-apply plugin: 'java'
 apply plugin: 'application'
 
-sourceCompatibility = 1.8
-
-repositories {
-    mavenCentral()
-}
-
 run {
     mainClassName = 'de.tudresden.inf.st.eraser.integration.IntegrationMain'
     standardInput = System.in
@@ -19,7 +11,4 @@ run {
 dependencies {
     compile project(':eraser-base')
     compile project(':openhab-mock')
-    compile group: 'org.apache.logging.log4j', name: 'log4j-api', version: '2.11.1'
-    compile group: 'org.apache.logging.log4j', name: 'log4j-core', version: '2.11.1'
-    testCompile group: 'junit', name: 'junit', version: '4.12'
 }
diff --git a/integration/src/main/java/de/tudresden/inf/st/eraser/integration/IntegrationMain.java b/integration/src/main/java/de/tudresden/inf/st/eraser/integration/IntegrationMain.java
index a7ca2904072ea46e0a0243fd7bc47328729a55c4..3418238f3572595c4a9640369767a1d34d037920 100644
--- a/integration/src/main/java/de/tudresden/inf/st/eraser/integration/IntegrationMain.java
+++ b/integration/src/main/java/de/tudresden/inf/st/eraser/integration/IntegrationMain.java
@@ -79,17 +79,14 @@ public class IntegrationMain {
       logger.info("Start!");
       Root model = Main.importFromFile();
 //      Root model = importFromLocalFile();
-      logger.debug("Got model: {}", model.description());
+      logger.debug("Got model: {}", model.getOpenHAB2Model().description());
       MqttRoot mqttRoot = new MqttRoot();
       mqttRoot.setHostByName("localhost");
       mqttRoot.setIncomingPrefix("oh2/out/");
-      MqttTopic irisTopic = new MqttTopic();
-      irisTopic.setPart("iris1_item");
       MqttTopic irisStateTopic = new MqttTopic();
-      irisStateTopic.setPart("state");
-      irisTopic.addSubTopic(irisStateTopic);
+      irisStateTopic.setTopicString("iris1_item/state");
       Item iris = null;
-      for (Item item : model.items()) {
+      for (Item item : model.getOpenHAB2Model().items()) {
         if (item.getID().equals("iris1_item")) {
           iris = item;
           break;
@@ -99,8 +96,7 @@ public class IntegrationMain {
         logger.error("Could not find iris1. Exiting");
         return;
       }
-      irisStateTopic.setItem(iris);
-      mqttRoot.addTopic(irisTopic);
+      irisStateTopic.addItem(iris);
       model.setMqttRoot(mqttRoot);
 //      JsonSerializer.write(model, "src/main/resources/openhab2-data.json");
       JsonSerializer.write(model, "openhab2-data.json");
diff --git a/integration_test/.gitignore b/integration_test/.gitignore
new file mode 100644
index 0000000000000000000000000000000000000000..54c5c0b024dab04da2a43d4135ed67ed2a670b46
--- /dev/null
+++ b/integration_test/.gitignore
@@ -0,0 +1,2 @@
+/build/
+logs/
diff --git a/integration_test/build.gradle b/integration_test/build.gradle
new file mode 100644
index 0000000000000000000000000000000000000000..2d905f3f6d29a82267538d94376175d6556787d7
--- /dev/null
+++ b/integration_test/build.gradle
@@ -0,0 +1,24 @@
+apply plugin: 'application'
+
+dependencies {
+    testCompile project(':eraser-base')
+    testCompile group: 'org.apache.httpcomponents', name: 'httpclient', version: '4.5.8'
+    testCompile group: 'org.apache.httpcomponents', name: 'fluent-hc', version: '4.5.8'
+    testCompile group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: '2.9.8'
+}
+
+run {
+    mainClassName = 'de.tudresden.inf.st.eraser.integration_test.Main'
+    standardInput = System.in
+    if (project.hasProperty("appArgs")) {
+        args Eval.me(appArgs)
+    }
+}
+
+sourceSets {
+    main {
+        java {
+            srcDir 'src/main/java'
+        }
+    }
+}
diff --git a/feedbackloop.analyze/src/main/java/de/tudresden/inf/st/eraser/feedbackloop/analyze/Main.java b/integration_test/src/main/java/de/tudresden/inf/st/eraser/integration_test/Main.java
similarity index 50%
rename from feedbackloop.analyze/src/main/java/de/tudresden/inf/st/eraser/feedbackloop/analyze/Main.java
rename to integration_test/src/main/java/de/tudresden/inf/st/eraser/integration_test/Main.java
index df65366a4ff72363f97975383dca895555f6a8cb..c362161e99ef67c70a79278d93263cce1a91eb2f 100644
--- a/feedbackloop.analyze/src/main/java/de/tudresden/inf/st/eraser/feedbackloop/analyze/Main.java
+++ b/integration_test/src/main/java/de/tudresden/inf/st/eraser/integration_test/Main.java
@@ -1,6 +1,4 @@
-package de.tudresden.inf.st.eraser.feedbackloop.analyze;
-
-import de.tudresden.inf.st.eraser.jastadd.model.*;
+package de.tudresden.inf.st.eraser.integration_test;
 
 public class Main {
 
diff --git a/integration_test/src/main/resources/log4j2.xml b/integration_test/src/main/resources/log4j2.xml
new file mode 100644
index 0000000000000000000000000000000000000000..867ec439d0a32dcb5f8b3e2d0c7485d7d8da418c
--- /dev/null
+++ b/integration_test/src/main/resources/log4j2.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Configuration>
+    <Appenders>
+        <Console name="Console">
+            <PatternLayout pattern="%highlight{%d{HH:mm:ss.SSS} %-5level} %c{1.} - %msg%n"/>
+        </Console>
+        <RollingFile name="RollingFile" fileName="logs/eraser.log"
+                    filePattern="logs/eraser-%i.log">
+            <PatternLayout pattern="%d{HH:mm:ss.SSS} %-5level %logger{36} - %msg%n"/>
+            <Policies>
+                <OnStartupTriggeringPolicy/>
+            </Policies>
+            <DefaultRolloverStrategy max="20"/>
+        </RollingFile>
+    </Appenders>
+    <Loggers>
+        <Root level="debug">
+            <AppenderRef ref="Console"/>
+            <AppenderRef ref="RollingFile"/>
+        </Root>
+    </Loggers>
+</Configuration>
diff --git a/integration_test/src/test/java/de/tudresden/inf/st/eraser/integration_test/ItemTest.java b/integration_test/src/test/java/de/tudresden/inf/st/eraser/integration_test/ItemTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..04c36a74dc0d172456410a925b9914cfce2a23a8
--- /dev/null
+++ b/integration_test/src/test/java/de/tudresden/inf/st/eraser/integration_test/ItemTest.java
@@ -0,0 +1,150 @@
+package de.tudresden.inf.st.eraser.integration_test;
+
+import com.fasterxml.jackson.databind.DeserializationFeature;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import de.tudresden.inf.st.eraser.openhab2.data.ItemData;
+import org.apache.http.HttpResponse;
+import org.apache.http.HttpStatus;
+import org.apache.http.client.fluent.Form;
+import org.apache.http.client.fluent.Request;
+import org.apache.http.entity.ContentType;
+import org.apache.http.util.EntityUtils;
+import org.junit.BeforeClass;
+import org.junit.Ignore;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.function.Function;
+
+import static org.hamcrest.Matchers.either;
+import static org.hamcrest.Matchers.equalTo;
+import static org.junit.Assert.assertThat;
+
+/**
+ * Integration test to check openHAB-binding for eraser works well together with eraser itself.
+ *
+ * @author rschoene - Initial contribution
+ */
+@RunWith(Parameterized.class)
+@Ignore
+public class ItemTest {
+
+  private static final String OPENHAB_ITEM_URI = "http://localhost:8080/rest/items/";
+  private static final String ERASER_ITEM_URI = "http://localhost:4567/model/items/";
+
+  private static final String DIMMER_ITEM = "dimmer_item";
+  private static final String NUMBER_ITEM = "number_item";
+  private static final String SWITCH_ITEM = "switch_item";
+  private static final String COLOR_ITEM = "color_item";
+
+  @BeforeClass
+  public static void ensureItemsAreCreated() throws IOException {
+    ensureItemCreated(DIMMER_ITEM, "Dimmer");
+    ensureItemCreated(NUMBER_ITEM, "Number");
+    ensureItemCreated(SWITCH_ITEM, "Switch");
+    ensureItemCreated(COLOR_ITEM, "Color");
+  }
+
+  private static void ensureItemCreated(String name, String type) throws IOException {
+    // Create at openHAB
+    HttpResponse responseOpenHAB = Request.Put(OPENHAB_ITEM_URI + name)
+        .bodyForm(Form.form().add("type", type).add("name", name).build())
+        .execute().returnResponse();
+    assertThat(
+        responseOpenHAB.getStatusLine().getStatusCode(),
+        either(equalTo(HttpStatus.SC_CREATED))
+            .or(equalTo(HttpStatus.SC_OK)));
+
+    // Create at eraser
+    HttpResponse responseEraser = Request.Put(ERASER_ITEM_URI + name)
+        .bodyString(type + " Item: id=\"" + name + "\"", ContentType.TEXT_PLAIN)
+        .execute().returnResponse();
+    assertThat(
+        responseEraser.getStatusLine().getStatusCode(),
+        equalTo(HttpStatus.SC_CREATED));
+  }
+
+  @Test
+  public void itemAvailable() throws IOException {
+    String name = "Tradfri_2_small_tv";
+    ItemData itemData = Request.Get( OPENHAB_ITEM_URI + name )
+        .execute().handleResponse(
+            response -> retrieveResourceFromResponse(response, ItemData.class));
+    assertThat(itemData.type, equalTo("Dimmer"));
+  }
+
+  @Test
+  public void dimmerSetStateAtOpenHAB() throws IOException {
+    String name = "Tradfri_2_small_tv";
+    double newValue = 3.0;
+    // set item state with openHAB REST API
+    HttpResponse httpResponse = Request.Put(OPENHAB_ITEM_URI + name + "/state")
+        .bodyString(Double.toString(newValue), ContentType.TEXT_PLAIN)
+        .execute().returnResponse();
+    assertThat(
+        httpResponse.getStatusLine().getStatusCode(),
+        equalTo(HttpStatus.SC_ACCEPTED));
+
+    // check whether state was set correctly
+    String responseOpenHAB = Request.Get(OPENHAB_ITEM_URI + name + "/state")
+        .execute().returnContent().asString();
+    assertThat(Double.parseDouble(responseOpenHAB), equalTo(newValue));
+
+    // check whether state was updated on eraser side
+    String responseEraser = Request.Get(ERASER_ITEM_URI + name + "/state")
+        .execute().returnContent().asString();
+    assertThat(Double.parseDouble(responseEraser), equalTo(newValue));
+  }
+
+  @Test
+  public void dimmerSetStateAtEraser() throws IOException {
+    String name = "Tradfri_2_small_tv";
+    double newValue = 25.0;
+    // set item state with eraser REST API
+    String uri = ERASER_ITEM_URI + name + "/state";
+    System.out.println(uri);
+    HttpResponse httpResponse = Request.Put(uri)
+        .bodyString(Double.toString(newValue), ContentType.TEXT_PLAIN)
+        .execute().returnResponse();
+    assertThat(
+        httpResponse.getStatusLine().getStatusCode(),
+        equalTo(HttpStatus.SC_OK));
+
+    // check whether state was set correctly
+    String responseEraser = Request.Get(ERASER_ITEM_URI + name + "/state")
+        .execute().returnContent().asString();
+    assertThat(Double.parseDouble(responseEraser), equalTo(newValue));
+
+    // check whether state was updated on openHAB side
+    String responseOpenHAB = Request.Get(OPENHAB_ITEM_URI + name + "/state")
+        .execute().returnContent().asString();
+    assertThat(Double.parseDouble(responseOpenHAB), equalTo(newValue));
+  }
+
+  private static <T> T retrieveResourceFromResponse(HttpResponse response, Class<T> clazz)
+      throws IOException {
+    String jsonFromResponse = EntityUtils.toString(response.getEntity());
+    ObjectMapper mapper = new ObjectMapper()
+        .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
+    return mapper.readValue(jsonFromResponse, clazz);
+  }
+
+  private static <T> Object[] d(String name, T initialValue, Function<T, String> toString,
+                                Function<String, T> fromString) {
+    return new Object[]{name, initialValue, toString, fromString};
+  }
+
+  @Parameterized.Parameters(name= "{index}: {0}")
+  public static Iterable<Object[]> data() {
+    return Arrays.asList(
+        d(DIMMER_ITEM, 25.0, d -> Double.toString(d), Double::parseDouble),
+        d(NUMBER_ITEM, 4, i -> Integer.toString(i), Integer::parseInt),
+        d(SWITCH_ITEM, "ON", Function.identity(), Function.identity()),
+        d(COLOR_ITEM, "1,2,3", Function.identity(), Function.identity())
+    );
+  }
+
+}
diff --git a/learner_test/.gitignore b/learner_test/.gitignore
new file mode 100644
index 0000000000000000000000000000000000000000..84c048a73cc2e5dd24f807669eb99b0ce3123195
--- /dev/null
+++ b/learner_test/.gitignore
@@ -0,0 +1 @@
+/build/
diff --git a/learner_test/build.gradle b/learner_test/build.gradle
new file mode 100644
index 0000000000000000000000000000000000000000..64ba25868ab02f58ed90c006f52cfb3e9563c411
--- /dev/null
+++ b/learner_test/build.gradle
@@ -0,0 +1,35 @@
+repositories {
+    mavenCentral()
+}
+
+sourceCompatibility = 1.8
+
+apply plugin: 'java'
+apply plugin: 'application'
+
+dependencies {
+    compile project(':eraser-base')
+    compile group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: '2.8.8.1'
+    compile group: 'org.apache.logging.log4j', name: 'log4j-api', version: '2.10.0'
+    compile group: 'org.apache.logging.log4j', name: 'log4j-core', version: '2.10.0'
+    testCompile group: 'junit', name: 'junit', version: '4.12'
+    testCompile group: 'org.hamcrest', name: 'hamcrest-junit', version: '1.0.0.0'
+
+    compile 'org.encog:encog-core:3.4'
+}
+
+run {
+    mainClassName = 'de.tudresden.inf.st.eraser.learner_test.Main'
+    standardInput = System.in
+    if (project.hasProperty("appArgs")) {
+        args Eval.me(appArgs)
+    }
+}
+
+sourceSets {
+    main {
+        java {
+            srcDir 'src/main/java'
+        }
+    }
+}
diff --git a/learner_test/src/main/java/de/tudresden/inf/st/eraser/learner_test/Main.java b/learner_test/src/main/java/de/tudresden/inf/st/eraser/learner_test/Main.java
new file mode 100644
index 0000000000000000000000000000000000000000..62f00cb624f4dbae09c84bca772d09318f5c9964
--- /dev/null
+++ b/learner_test/src/main/java/de/tudresden/inf/st/eraser/learner_test/Main.java
@@ -0,0 +1,112 @@
+package de.tudresden.inf.st.eraser.learner_test;
+import org.encog.Encog;
+import org.encog.ml.MLClassification;
+import org.encog.ml.data.MLData;
+import org.encog.persist.EncogDirectoryPersistence;
+import org.encog.util.csv.CSVFormat;
+import org.encog.util.csv.ReadCSV;
+import org.encog.util.simple.EncogUtility;
+
+import org.encog.ml.data.versatile.NormalizationHelper;
+import org.encog.ml.data.versatile.VersatileMLDataSet;
+import org.encog.ml.data.versatile.columns.ColumnDefinition;
+import org.encog.ml.data.versatile.columns.ColumnType;
+import org.encog.ml.data.versatile.sources.VersatileDataSource;
+import org.encog.ml.data.versatile.sources.CSVDataSource;
+import org.encog.ml.factory.MLMethodFactory;
+import org.encog.ml.model.EncogModel;
+import org.encog.ConsoleStatusReportable;
+import org.encog.ml.MLRegression;
+import java.io.File;
+import java.util.Arrays;
+import static org.encog.persist.EncogDirectoryPersistence.*;
+
+public class Main {
+
+  public static void main(String[] args) {
+    //mapping the data into model
+    String savefile = "src/main/java/de/tudresden/inf/st/eraser/learner_test/save_model.eg";
+    String File = "src/main/java/de/tudresden/inf/st/eraser/learner_test/preference_data.csv";
+    File file = new File(File);
+    VersatileDataSource source = new CSVDataSource(file, false, CSVFormat.DECIMAL_POINT);
+    VersatileMLDataSet data = new VersatileMLDataSet(source);
+    data.defineSourceColumn("monat", 0, ColumnType.continuous);
+    data.defineSourceColumn("day", 1, ColumnType.continuous);
+    data.defineSourceColumn("hour", 2, ColumnType.continuous);
+    data.defineSourceColumn("minute", 3, ColumnType.continuous);
+    ColumnDefinition outputColumn = data.defineSourceColumn("labels", 4, ColumnType.continuous);
+    data.defineSingleOutputOthersInput(outputColumn);
+    data.analyze();
+    System.out.println("get data ");
+    EncogModel model = new EncogModel(data);
+    model.selectMethod(data, MLMethodFactory.TYPE_FEEDFORWARD);
+    //model.setReport(new ConsoleStatusReportable());
+    data.normalize();
+    NormalizationHelper helper = data.getNormHelper();
+    System.out.println(helper.toString());
+    model.holdBackValidation(0.3, true, 1001);
+    model.selectTrainingType(data);
+    MLRegression bestMethod = (MLRegression)model.crossvalidate(5, true);
+    MLClassification bestMethodtest=(MLClassification)model.crossvalidate(5,true);
+    /**System.out.println( "Training error: " + EncogUtility.calculateRegressionError(bestMethod, model.getTrainingDataset()));
+    System.out.println( "testTraining error: " + EncogUtility.calculateClassificationError(bestMethodtest, model.getTrainingDataset()));
+    System.out.println( "Validation error: " + EncogUtility.calculateRegressionError(bestMethod, model.getValidationDataset()));
+    System.out.println( "testValidation error: " + EncogUtility.calculateClassificationError(bestMethodtest, model.getValidationDataset()));
+
+    System.out.println(helper.getClass());
+    System.out.println(helper.toString());
+    System.out.println("Final model: " + bestMethod);
+    System.out.println("Final testmodel: " + bestMethodtest);**/
+    //NormalizationHelper helper = data.getNormHelper();
+
+    //test
+    String helperstr=helper.toString();
+    String [] split=helperstr.split(";");
+    String [] finalStr = split[split.length-1].replace("]","").replace("[","").
+            split(",");
+    System.out.println(helper);
+
+    // save network...
+    //to delete
+    saveObject(new File(savefile), bestMethodtest);
+    ReadCSV csv = new ReadCSV(File, false, CSVFormat.DECIMAL_POINT);
+    String[] line = new String[4];
+    MLData input = helper.allocateInputVector();
+    System.out.println("input test---------------");
+    System.out.println(input);
+    while(csv.next()) {
+      StringBuilder result = new StringBuilder();
+      line[0] = csv.get(0);
+      line[1] = csv.get(1);
+      line[2] = csv.get(2);
+      line[3] = csv.get(3);
+      String correct = csv.get(4);
+      helper.normalizeInputVector(line,input.getData(),false);
+
+      MLData output = bestMethod.compute(input);
+      System.out.println("inputs:");
+      System.out.println(input);
+      System.out.println("outputs:");
+      System.out.println(output);
+      String brightnessChosen = helper.denormalizeOutputVectorToString(output)[0];
+
+      result.append(Arrays.toString(line));
+      result.append(" -> predicted: ");
+      result.append(brightnessChosen);
+      result.append("(correct: ");
+      result.append(correct);
+      result.append(")");
+      System.out.println(result.toString());
+      break;
+
+    }
+    // Delete data file and shut down.
+    //File.delete();
+    Encog.getInstance().shutdown();
+    /**Training error: 0.299928703107046
+    testTraining error: 0.9931740614334471
+    Validation error: 0.41277024952020763
+    testValidation error: 0.992*/
+
+  }
+}
diff --git a/learner_test/src/main/java/de/tudresden/inf/st/eraser/learner_test/final_data.csv b/learner_test/src/main/java/de/tudresden/inf/st/eraser/learner_test/final_data.csv
new file mode 100644
index 0000000000000000000000000000000000000000..a1e263194d3ee4f95c64c3eff3b5123fb2246cf5
--- /dev/null
+++ b/learner_test/src/main/java/de/tudresden/inf/st/eraser/learner_test/final_data.csv
@@ -0,0 +1,418 @@
+7,20,12,13,2
+7,20,14,40,1
+7,20,14,40,2
+7,21,13,2,2
+7,21,13,2,2
+7,21,14,23,2
+7,21,14,23,2
+7,21,15,41,2
+7,21,16,54,2
+7,21,16,54,2
+7,21,17,45,3
+7,22,12,28,3
+7,22,15,35,2
+7,22,15,35,2
+7,22,18,59,3
+7,22,18,59,3
+7,23,12,32,2
+7,23,12,32,2
+7,23,16,7,2
+7,23,16,7,2
+7,23,16,7,2
+7,23,16,7,2
+7,23,16,7,2
+7,24,12,4,0
+7,24,12,4,0
+7,24,12,4,1
+7,24,14,38,2
+7,24,14,38,2
+7,24,18,54,3
+7,25,12,31,0
+7,25,12,32,1
+7,25,12,32,1
+7,25,15,6,3
+7,25,18,56,3
+7,26,13,41,2
+7,26,19,14,3
+7,27,11,39,2
+7,27,11,39,3
+7,27,11,46,3
+7,27,11,46,2
+7,27,13,8,2
+7,27,13,8,2
+7,27,13,9,2
+7,27,13,45,2
+7,27,13,45,2
+7,27,15,38,3
+7,28,12,12,2
+7,28,12,13,2
+7,28,12,41,2
+7,28,12,41,2
+7,28,12,41,2
+7,28,14,0,1
+7,28,14,0,2
+7,28,15,21,3
+7,28,18,56,3
+7,29,10,9,1
+7,29,10,9,1
+7,29,10,9,1
+7,29,11,54,0
+7,29,11,54,0
+7,29,11,54,0
+7,29,11,54,1
+7,29,14,10,2
+7,29,16,44,2
+7,29,16,44,2
+7,30,16,7,3
+7,30,18,45,3
+7,31,13,2,0
+7,31,13,2,1
+7,31,13,3,1
+7,31,13,3,1
+7,31,13,3,1
+7,31,18,39,3
+8,1,12,22,0
+8,1,12,22,1
+8,1,14,20,2
+8,1,14,20,2
+8,1,14,20,2
+8,1,15,55,3
+8,1,18,31,3
+8,1,18,37,3
+8,1,18,37,3
+8,1,19,2,3
+8,1,19,2,3
+8,1,20,5,3
+8,2,10,9,2
+8,2,10,9,1
+8,2,10,9,2
+8,2,10,9,2
+8,2,13,58,2
+8,2,13,58,2
+8,2,15,44,3
+8,2,15,44,3
+8,2,15,44,3
+8,2,17,21,3
+8,2,17,21,3
+8,2,17,21,3
+8,3,13,31,1
+8,3,13,31,2
+8,3,13,32,2
+8,3,16,43,3
+8,4,13,20,1
+8,4,13,20,2
+8,4,18,27,3
+8,5,13,37,2
+8,5,13,37,2
+8,5,18,33,3
+8,6,11,24,3
+8,6,11,24,3
+8,6,11,24,3
+8,6,13,50,3
+8,7,13,4,2
+8,7,13,4,2
+8,7,14,56,3
+8,8,12,13,2
+8,8,12,13,2
+8,8,15,51,2
+8,8,15,51,2
+8,8,15,51,3
+8,9,13,32,2
+8,9,13,32,2
+8,9,13,32,2
+8,9,15,8,2
+8,9,15,8,2
+8,9,15,8,2
+8,9,16,19,2
+8,10,11,32,0
+8,10,11,32,1
+8,10,11,32,1
+8,10,13,13,1
+8,10,13,13,1
+8,10,13,13,2
+8,10,16,42,3
+8,10,16,42,3
+8,11,14,6,2
+8,11,14,7,2
+8,11,18,54,3
+8,11,18,54,3
+8,11,18,54,3
+8,12,12,27,1
+8,12,12,27,1
+8,12,12,28,1
+8,12,13,53,2
+8,12,13,53,2
+8,12,13,53,2
+8,12,15,21,3
+8,13,13,16,1
+8,13,13,16,1
+8,13,13,16,1
+8,13,14,14,2
+8,13,14,14,2
+8,13,16,11,3
+8,13,17,18,3
+8,14,13,7,1
+8,14,13,7,1
+8,14,13,7,1
+8,14,13,7,1
+8,14,13,7,2
+8,14,13,7,2
+8,14,15,6,3
+8,15,14,5,2
+8,15,14,5,2
+8,15,14,6,2
+8,15,14,6,2
+8,15,16,41,3
+8,15,16,41,3
+8,15,17,30,3
+8,16,13,40,2
+8,16,13,40,2
+8,16,17,52,3
+8,16,17,53,3
+8,17,13,34,1
+8,17,13,35,2
+8,17,14,7,2
+8,17,19,2,3
+8,18,10,21,3
+8,18,11,14,2
+8,18,11,14,2
+8,18,11,14,2
+8,18,11,14,2
+8,18,14,25,2
+8,18,14,25,3
+8,18,14,25,2
+8,18,18,18,3
+8,18,18,19,3
+8,19,18,33,3
+8,19,18,33,3
+8,19,18,33,3
+8,19,18,33,3
+8,20,14,28,2
+8,20,14,28,2
+8,20,14,28,2
+8,20,14,28,2
+8,20,17,8,3
+8,20,18,22,3
+8,21,11,24,1
+8,21,11,24,1
+8,21,11,24,1
+8,21,15,34,3
+8,21,18,55,3
+8,22,12,3,1
+8,22,12,4,2
+8,22,12,4,2
+8,22,13,51,2
+8,22,13,51,2
+8,22,13,51,2
+8,22,18,12,3
+8,22,18,12,3
+8,22,18,12,3
+8,22,18,12,3
+8,22,18,40,3
+8,22,18,40,3
+8,23,13,42,1
+8,23,13,42,1
+8,23,17,32,3
+8,23,19,28,3
+8,23,20,27,3
+8,23,20,27,3
+8,23,21,49,3
+8,24,14,0,2
+8,24,14,0,2
+8,24,14,0,2
+8,24,14,0,2
+8,24,15,4,3
+8,24,15,4,3
+8,24,16,2,3
+8,24,16,3,3
+8,24,16,37,3
+8,24,17,9,3
+8,24,17,14,3
+8,25,13,34,1
+8,25,13,34,1
+8,25,13,34,1
+8,25,13,34,1
+8,25,13,34,1
+8,25,15,1,3
+8,25,17,58,3
+8,26,10,29,0
+8,26,10,29,0
+8,26,10,29,0
+8,26,10,29,0
+8,26,10,29,0
+8,26,16,42,3
+8,26,16,42,3
+8,26,18,41,3
+8,26,18,41,3
+8,27,13,41,2
+8,27,13,41,2
+8,27,13,41,2
+8,27,13,41,2
+8,27,17,42,3
+8,28,11,9,1
+8,28,11,9,1
+8,28,12,14,0
+8,28,12,14,1
+8,28,12,14,0
+8,28,15,3,2
+8,28,15,3,2
+8,28,16,31,3
+8,28,17,40,3
+8,29,14,44,3
+8,29,17,25,3
+8,30,12,5,0
+8,30,12,5,0
+8,30,12,5,0
+8,30,13,32,1
+8,30,13,32,1
+8,30,13,56,2
+8,30,14,23,2
+8,30,14,23,2
+8,30,14,23,2
+8,30,14,23,2
+8,30,14,41,2
+8,30,14,41,2
+8,30,14,41,2
+8,30,15,50,3
+8,30,17,0,3
+8,30,18,59,3
+8,30,18,59,3
+8,31,14,31,2
+8,31,14,31,2
+8,31,14,31,2
+8,31,17,59,3
+8,31,18,0,3
+9,1,16,13,3
+9,1,16,13,3
+9,1,16,13,3
+9,1,17,41,3
+9,2,13,44,1
+9,2,13,44,1
+9,2,13,44,1
+9,2,14,49,2
+9,2,14,49,2
+9,2,14,49,2
+9,2,16,6,3
+9,2,16,6,3
+9,2,17,2,3
+9,3,16,9,3
+9,3,17,35,3
+9,3,17,36,3
+9,4,12,57,1
+9,4,12,57,1
+9,4,15,8,3
+9,4,15,34,3
+9,4,16,26,3
+9,4,16,26,3
+9,4,18,37,3
+9,4,18,37,3
+9,4,18,37,3
+9,6,11,18,0
+9,6,11,18,0
+9,6,12,54,1
+9,6,12,54,1
+9,6,14,21,2
+9,6,14,21,2
+9,6,19,20,3
+9,7,11,50,0
+9,7,14,17,2
+9,7,14,57,3
+9,7,14,57,3
+9,7,16,56,3
+9,7,16,56,3
+9,7,16,56,3
+9,7,16,56,3
+9,7,18,38,3
+9,7,18,38,3
+9,8,11,4,2
+9,8,11,4,2
+9,8,11,13,0
+9,8,11,13,0
+9,8,11,13,0
+9,8,11,13,0
+9,8,11,13,0
+9,8,11,14,0
+9,8,11,14,1
+9,8,11,14,1
+9,8,12,1,0
+9,8,12,1,0
+9,8,12,1,0
+9,8,12,1,0
+9,8,12,1,0
+9,8,12,1,1
+9,8,12,36,0
+9,8,12,36,0
+9,8,12,36,0
+9,8,12,36,0
+9,8,12,36,0
+9,8,13,37,1
+9,8,13,37,1
+9,8,13,37,1
+9,8,14,20,2
+9,8,14,20,2
+9,8,18,20,3
+9,9,12,47,1
+9,9,12,47,2
+9,9,12,47,2
+9,9,19,5,3
+9,10,13,15,1
+9,10,13,15,1
+9,10,13,15,0
+9,10,16,49,3
+9,10,19,6,3
+9,10,21,5,3
+9,11,14,16,2
+9,11,14,16,2
+9,11,14,16,2
+9,11,18,41,3
+9,12,14,43,2
+9,12,14,43,2
+9,12,14,43,2
+9,12,16,14,3
+9,12,17,12,3
+9,12,17,12,2
+9,12,17,12,3
+9,12,17,12,2
+9,12,20,44,3
+9,13,19,52,3
+9,14,14,39,2
+9,14,14,39,2
+9,14,15,14,3
+9,14,17,29,3
+9,14,17,29,3
+9,14,17,29,3
+9,15,11,41,1
+9,15,11,41,1
+9,15,13,4,1
+9,15,14,3,1
+9,15,14,3,2
+9,16,12,36,1
+9,16,12,36,1
+9,16,12,36,1
+9,16,12,36,1
+9,16,12,48,1
+9,16,12,48,1
+9,16,13,51,1
+9,16,13,51,2
+9,16,13,51,1
+9,16,15,13,3
+9,16,15,14,3
+9,16,15,14,3
+9,17,10,27,0
+9,17,10,27,0
+9,17,11,10,0
+9,17,11,10,0
+9,17,11,10,0
+9,17,12,43,1
+9,17,12,43,1
+9,17,12,43,1
+9,17,13,32,1
+9,17,13,32,1
+9,17,14,5,1
+9,17,14,5,2
+9,17,14,6,2
+9,17,15,7,3
+9,17,15,49,3
+9,17,15,49,3
+9,17,18,12,3
+9,17,18,13,3
diff --git a/learner_test/src/main/java/de/tudresden/inf/st/eraser/learner_test/save_model.eg b/learner_test/src/main/java/de/tudresden/inf/st/eraser/learner_test/save_model.eg
new file mode 100644
index 0000000000000000000000000000000000000000..47c37ff28ef95b7b22a2df05ec050c3ffeb53a30
--- /dev/null
+++ b/learner_test/src/main/java/de/tudresden/inf/st/eraser/learner_test/save_model.eg
@@ -0,0 +1,24 @@
+encog,BasicNetwork,java,3.4.0,1,1554196571101
+[BASIC]
+[BASIC:PARAMS]
+[BASIC:NETWORK]
+beginTraining=0
+connectionLimit=0
+contextTargetOffset=0,0,0
+contextTargetSize=0,0,0
+endTraining=2
+hasContext=f
+inputCount=4
+layerCounts=1,8,5
+layerFeedCounts=1,7,4
+layerContextCount=0,0,0
+layerIndex=0,1,9
+output=0.2537517424,0.3154675575,-0.8739039638,-0.4408848221,-0.8484433638,-0.999915299,-0.6964984771,-0.208278439,1,0,0,-0.4545454545,0.3559322034,1
+outputCount=1
+weightIndex=0,8,43
+weights=0.5976774048,-0.7925906525,0.7127327881,-0.9611660362,0.8031350986,-0.7286657218,1.0990482817,-0.5985785536,-0.0783115433,0.575612931,1.1267500918,1.7184744034,0.2271044512,-1.0525796764,0.0900869671,1.1492323512,0.6141715555,-1.0455927965,-0.0925453451,0.2471651431,2.3634316872,0.3939369257,0.4607437082,-0.1435186798,0.8428535365,-0.0848896791,-0.070602589,-1.2640263565,2.4899996734,-0.2185394776,10.3421332361,-0.1650898311,-0.2750133571,-0.79680959,-0.8051139953,0.8219933747,-0.0727160299,-0.4609522002,-1.0410685492,-0.5354063412,0.3028724456,-0.6835374219,0.169591233
+biasActivation=0,1,1
+[BASIC:ACTIVATION]
+"org.encog.engine.network.activation.ActivationTANH"
+"org.encog.engine.network.activation.ActivationTANH"
+"org.encog.engine.network.activation.ActivationLinear"
diff --git a/learner_test/src/main/java/de/tudresden/inf/st/eraser/learner_test/save_model_test.eg b/learner_test/src/main/java/de/tudresden/inf/st/eraser/learner_test/save_model_test.eg
new file mode 100644
index 0000000000000000000000000000000000000000..62fe6421d95e164aa14e123b950e831e37a5f23c
--- /dev/null
+++ b/learner_test/src/main/java/de/tudresden/inf/st/eraser/learner_test/save_model_test.eg
@@ -0,0 +1,24 @@
+encog,BasicNetwork,java,3.4.0,1,1548158734516
+[BASIC]
+[BASIC:PARAMS]
+[BASIC:NETWORK]
+beginTraining=0
+connectionLimit=0
+contextTargetOffset=0,0,0
+contextTargetSize=0,0,0
+endTraining=2
+hasContext=f
+inputCount=4
+layerCounts=4,8,5
+layerFeedCounts=4,7,4
+layerContextCount=0,0,0
+layerIndex=0,4,12
+output=0.6991387348,-0.8711034513,-0.996886038,-0.832747291,-0.0935682806,-0.9996163977,0.5399150265,0.9411173394,-0.5084989975,0.4850010791,0.9999999957,1,0,-0.6666666667,-0.4545454545,0.6949152542,1
+outputCount=4
+weightIndex=0,32,67
+weights=-2.6901880743,0.6512821123,-1.2270002115,1.63124668,0.1982387305,-0.2994789552,1.5833040739,-0.9450411677,2.0541422847,-0.718279397,-1.1761952241,0.5028631512,0.0690323612,-1.496141565,-0.1955149568,-0.7453976822,-0.3691141073,0.9854755554,2.2113850088,-1.5216550292,0.9652087936,-1.3028209693,-1.3346156171,0.4142247818,1.0821207364,0.1987534858,0.6202881884,-0.2940331887,-1.4643282498,2.6960334656,-0.0167663298,-2.9907087565,0.3469960227,-0.0441249736,-2.5998575813,-0.7106361301,-0.8111809962,2.2216158678,-0.5482762437,-1.7996398291,-3.6734127565,-2.9102547958,0.4845401914,0.3760471288,-0.0124987546,0.3784047483,0.5860932613,-0.2682876707,0.7429004186,-7.559247176,-3.4421363532,1.1989747484,-2.3340717496,-1.4740773042,-0.7795788072,-1.8241693655,-0.630132295,-0.8191869009,-0.4060569987,-1.0997423162,-0.5495165849,0.1407829068,-2.2964930412,0.0798893221,-19.5271913755,2.0474187009,-0.2622671892
+biasActivation=0,1,1
+[BASIC:ACTIVATION]
+"org.encog.engine.network.activation.ActivationTANH"
+"org.encog.engine.network.activation.ActivationTANH"
+"org.encog.engine.network.activation.ActivationLinear"
diff --git a/learner_test/src/main/resources/log4j2.xml b/learner_test/src/main/resources/log4j2.xml
new file mode 100644
index 0000000000000000000000000000000000000000..0594576fac98ba859e411597c90c8e3d989378bd
--- /dev/null
+++ b/learner_test/src/main/resources/log4j2.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Configuration>
+    <Appenders>
+        <Console name="Console">
+            <PatternLayout pattern="%highlight{%d{HH:mm:ss.SSS} %-5level} %c{1.} - %msg%n"/>
+        </Console>
+        <RollingFile name="RollingFile" fileName="logs/jastadd-mquat.log"
+                    filePattern="logs/jastadd-mquat-%i.log">
+            <PatternLayout pattern="%d{HH:mm:ss.SSS} %-5level %logger{36} - %msg%n"/>
+            <Policies>
+                <OnStartupTriggeringPolicy/>
+            </Policies>
+            <DefaultRolloverStrategy max="20"/>
+        </RollingFile>
+    </Appenders>
+    <Loggers>
+        <Root level="debug">
+            <AppenderRef ref="Console"/>
+            <AppenderRef ref="RollingFile"/>
+        </Root>
+    </Loggers>
+</Configuration>
diff --git a/make-new-project.py b/make-new-project.py
index afd3a51931e3a7dc6823d47e1b9c97a65a2a2e76..635755567333e0d326ff346d5852d7d62437bbcb 100755
--- a/make-new-project.py
+++ b/make-new-project.py
@@ -1,4 +1,4 @@
-#!/usr/bin/env python3
+#!/usr/bin/env python2.7
 import argparse
 import os
 import shutil
diff --git a/ml_test/.gitignore b/ml_test/.gitignore
index 83ccc54d0286590d31587a71aaa789e780bff41e..70b583e34c3316bcd77c807e2d6b85db5e7d49f6 100644
--- a/ml_test/.gitignore
+++ b/ml_test/.gitignore
@@ -1,2 +1,3 @@
 /build/
 /bin/
+logs/
diff --git a/ml_test/build.gradle b/ml_test/build.gradle
index 241f853811b83bf6161c9e98efc0a22ec1ba64f2..a9e198eb94876643269b680a0de267e69724100f 100644
--- a/ml_test/build.gradle
+++ b/ml_test/build.gradle
@@ -1,19 +1,7 @@
-repositories {
-    mavenCentral()
-}
-
-sourceCompatibility = 1.8
-
-apply plugin: 'java'
 apply plugin: 'application'
 
 dependencies {
     compile project(':eraser-base')
-    compile group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: '2.9.8'
-    compile group: 'org.apache.logging.log4j', name: 'log4j-api', version: '2.11.1'
-    compile group: 'org.apache.logging.log4j', name: 'log4j-core', version: '2.11.1'
-    testCompile group: 'junit', name: 'junit', version: '4.12'
-    testCompile group: 'org.hamcrest', name: 'hamcrest-junit', version: '2.0.0.0'
 }
 
 run {
diff --git a/ml_test/src/main/java/de/tudresden/inf/st/eraser/ml_test/Main.java b/ml_test/src/main/java/de/tudresden/inf/st/eraser/ml_test/Main.java
index 7a83aa0cc237f02ae018b3c3c4d73a4efce33d34..59c156853782b8e58010d24393e7c14c73ffefb8 100644
--- a/ml_test/src/main/java/de/tudresden/inf/st/eraser/ml_test/Main.java
+++ b/ml_test/src/main/java/de/tudresden/inf/st/eraser/ml_test/Main.java
@@ -23,22 +23,26 @@ public class Main {
   }
 
   private static Root createModel() {
-    Root model = Root.createEmptyRoot();
+    Root root = Root.createEmptyRoot();
     Group group = new Group();
     group.setID("Group1");
-    model.addGroup(group);
+    root.getOpenHAB2Model().addGroup(group);
     Item activityItem = newItem("activity", "Recognized activity", false, 8);
     Item brightnessItem = newItem("brightness", "Measured brightness", false, 5);
     group.addItem(activityItem);
     group.addItem(brightnessItem);
-    return model;
+    return root;
   }
 
   private static NumberItem newItem(String id, String label, boolean defaultSendState, int initialState) {
     NumberItem item = new NumberItem();
     item.setID(id);
     item.setLabel(label);
-    item.setDefaultShouldSendState(defaultSendState);
+    if (defaultSendState) {
+      item.enableSendState();
+    } else {
+      item.disableSendState();
+    }
     item.setState(initialState);
     return item;
   }
@@ -66,7 +70,7 @@ public class Main {
       }
     }
     logger.info("Classification results: {}", results);
-    logger.info("Took {}ms", String.join("ms, ", times.stream().map(l -> Long.toString(l)).collect(Collectors.toList())));
+    logger.info("Took {}ms", times.stream().map(l -> Long.toString(l)).collect(Collectors.joining("ms, ")));
     logger.info("Took on average: {}ms",
         Arrays.stream(times.toArray(new Long[0])).mapToLong(l -> l).average().orElse(-1));
     logger.info("Took on median: {}ms",
@@ -74,30 +78,36 @@ public class Main {
             .skip((REPETITIONS-1)/2).limit(2-REPETITIONS%2).average().orElse(Double.NaN));
   }
 
-  /**
-   * Purpose: Create a neural network with 3 layers (2 + 10 + 1 neurons)
-   * Sigmoid function for all layers, combinator of output is identity function
-   */
-  private static void createAndTestBrightnessNetwork() {
-    /*
-   	- Helligkeit NN:
-      - arbeitet momentan mit Zonen und nicht mit einzelnen Lampen
-      - 3 Layers
-      - Input Layer hat Neuronen (Aktivitätsnummer, Wert vom Helligkeitssensor)
-      - Hidden Layer hat 10 Neuronen
-      - Output Layer hat 1 Neuron ( Helligkeitswert)
-      - Aktivierungsfunktion: Sigmoidfunktion <- selbe für alle Layers
-     */
-    Root model = createModel();
-    Item activityItem = model.resolveItem("activity").orElseThrow(
+  private static class PreparationResult {
+    OutputLayer outputLayer;
+    DoubleArrayDoubleFunction sigmoid;
+    InputNeuron activity;
+    InputNeuron brightness;
+    NeuralNetworkRoot nn;
+    HiddenNeuron[] hiddenNeurons;
+
+    PreparationResult(OutputLayer outputLayer, DoubleArrayDoubleFunction sigmoid, InputNeuron activity,
+                      InputNeuron brightness, NeuralNetworkRoot nn, HiddenNeuron[] hiddenNeurons) {
+      this.outputLayer = outputLayer;
+      this.sigmoid = sigmoid;
+      this.activity = activity;
+      this.brightness = brightness;
+      this.nn = nn;
+      this.hiddenNeurons = hiddenNeurons;
+    }
+  }
+
+  private static PreparationResult prepareNetwork() {
+    Root root = createModel();
+    Item activityItem = root.getOpenHAB2Model().resolveItem("activity").orElseThrow(
         () -> new RuntimeException("Activity not found"));
-    Item brightnessItem = model.resolveItem("brightness").orElseThrow(
+    Item brightnessItem = root.getOpenHAB2Model().resolveItem("brightness").orElseThrow(
         () -> new RuntimeException("Brightness not found"));
     NeuralNetworkRoot nn = new NeuralNetworkRoot();
 
     DoubleArrayDoubleFunction sigmoid = inputs -> Math.signum(Arrays.stream(inputs).sum());
 
-    // input layer
+    // input layer (2 neurons)
     InputNeuron activity = new InputNeuron();
     activity.setItem(activityItem);
     InputNeuron brightness = new InputNeuron();
@@ -105,31 +115,50 @@ public class Main {
     nn.addInputNeuron(activity);
     nn.addInputNeuron(brightness);
 
-    // output layer
     OutputLayer outputLayer = new OutputLayer();
-    OutputNeuron output = new OutputNeuron();
-    output.setLabel("Brightness_Output");
-    output.setActivationFormula(sigmoid);
-    outputLayer.addOutputNeuron(output);
-    // we just have one output neuron, thus use IdentityFunction
-    outputLayer.setCombinator(inputs -> inputs[0]);
     nn.setOutputLayer(outputLayer);
 
-    // hidden layer
+    // hidden layer (10 neurons)
     HiddenNeuron[] hiddenNeurons = new HiddenNeuron[10];
-    for (int i = 0; i < hiddenNeurons.length; i++) {
+    for (int hiddenIndex = 0; hiddenIndex < hiddenNeurons.length; hiddenIndex++) {
       HiddenNeuron hiddenNeuron = new HiddenNeuron();
       hiddenNeuron.setActivationFormula(sigmoid);
-      hiddenNeurons[i] = hiddenNeuron;
       nn.addHiddenNeuron(hiddenNeuron);
       activity.connectTo(hiddenNeuron, 1.0/2.0);
       brightness.connectTo(hiddenNeuron, 1.0/2.0);
-      hiddenNeuron.connectTo(output, 1.0/hiddenNeurons.length);
     }
+    root.getMachineLearningRoot().setPreferenceLearning(nn);
+
+    return new PreparationResult(outputLayer, sigmoid, activity, brightness, nn, hiddenNeurons);
+  }
+
+  /**
+   * Purpose: Create a neural network with 3 layers (2 + 10 + 1 neurons)
+   * Sigmoid function for all layers, combinator of output is identity function
+   */
+  private static void createAndTestBrightnessNetwork() {
+    /*
+   	- Helligkeit NN:
+      - arbeitet momentan mit Zonen und nicht mit einzelnen Lampen
+      - 3 Layers
+      - Input Layer hat Neuronen (Aktivitätsnummer, Wert vom Helligkeitssensor)
+      - Hidden Layer hat 10 Neuronen
+      - Output Layer hat 1 Neuron ( Helligkeitswert)
+      - Aktivierungsfunktion: Sigmoidfunktion <- selbe für alle Layers
+     */
+    PreparationResult pr = prepareNetwork();
+    OutputNeuron output = new OutputNeuron();
+    output.setLabel("Brightness_Output");
+    output.setActivationFormula(pr.sigmoid);
+    pr.outputLayer.addOutputNeuron(output);
+    // we just have one output neuron, thus use IdentityFunction
+    pr.outputLayer.setCombinator(inputs -> inputs[0]);
 
-    model.getMachineLearningRoot().setPreferenceLearning(nn);
+    for (HiddenNeuron hiddenNeuron : pr.hiddenNeurons) {
+      hiddenNeuron.connectTo(output, 1.0/pr.hiddenNeurons.length);
+    }
 
-    classifyTimed(nn, NeuralNetworkRoot::classify,
+    classifyTimed(pr.nn, NeuralNetworkRoot::classify,
         classification -> Double.toString(classification.number));
   }
 
@@ -138,52 +167,23 @@ public class Main {
    * Sigmoid function for all layers, combinator creates RGB value in hex form
    */
   private static void createAndTestColorNetwork() {
-    Root model = createModel();
-    Item activityItem = model.resolveItem("activity").orElseThrow(
-        () -> new RuntimeException("Activity not found"));
-    Item brightnessItem = model.resolveItem("brightness").orElseThrow(
-        () -> new RuntimeException("Brightness not found"));
-    NeuralNetworkRoot nn = new NeuralNetworkRoot();
-
-    DoubleArrayDoubleFunction sigmoid = inputs -> Math.signum(Arrays.stream(inputs).sum());
-
-    // input layer (2 neurons)
-    InputNeuron activity = new InputNeuron();
-    activity.setItem(activityItem);
-    InputNeuron brightness = new InputNeuron();
-    brightness.setItem(brightnessItem);
-    nn.addInputNeuron(activity);
-    nn.addInputNeuron(brightness);
-
-    // output layer (3 neurons)
-    OutputLayer outputLayer = new OutputLayer();
+    PreparationResult pr = prepareNetwork();
     for (int i = 0; i < 3; i++) {
       OutputNeuron output = new OutputNeuron();
       output.setLabel("Brightness_Output_" + i);
       output.setActivationFormula(inputs -> Arrays.stream(inputs).sum());
-      outputLayer.addOutputNeuron(output);
+      pr.outputLayer.addOutputNeuron(output);
     }
     // we have three output neurons, combine them to a double value (representing RGB)
-    outputLayer.setCombinator(inputs -> 65536 * Math.ceil(255.0 * inputs[0]) + 256 * Math.ceil(255.0 * inputs[1]) + Math.ceil(255.0 * inputs[0]));
-    nn.setOutputLayer(outputLayer);
+    pr.outputLayer.setCombinator(inputs -> 65536 * Math.ceil(255.0 * inputs[0]) + 256 * Math.ceil(255.0 * inputs[1]) + Math.ceil(255.0 * inputs[0]));
 
-    // hidden layer (10 neurons)
-    HiddenNeuron[] hiddenNeurons = new HiddenNeuron[10];
-    for (int hiddenIndex = 0; hiddenIndex < hiddenNeurons.length; hiddenIndex++) {
-      HiddenNeuron hiddenNeuron = new HiddenNeuron();
-      hiddenNeuron.setActivationFormula(sigmoid);
-      hiddenNeurons[hiddenIndex] = hiddenNeuron;
-      nn.addHiddenNeuron(hiddenNeuron);
-      activity.connectTo(hiddenNeuron, 1.0/2.0);
-      brightness.connectTo(hiddenNeuron, 1.0/2.0);
-      for (int outputIndex = 0; outputIndex < outputLayer.getNumOutputNeuron(); outputIndex++) {
-        hiddenNeuron.connectTo(outputLayer.getOutputNeuron(outputIndex), random.nextDouble() * 1.0/hiddenNeurons.length);
+    for (HiddenNeuron hiddenNeuron : pr.hiddenNeurons) {
+      for (int outputIndex = 0; outputIndex < pr.outputLayer.getNumOutputNeuron(); outputIndex++) {
+        hiddenNeuron.connectTo(pr.outputLayer.getOutputNeuron(outputIndex), random.nextDouble() * 1.0/pr.hiddenNeurons.length);
       }
     }
 
-    model.getMachineLearningRoot().setPreferenceLearning(nn);
-
-    classifyTimed(nn, NeuralNetworkRoot::classify,
+    classifyTimed(pr.nn, NeuralNetworkRoot::classify,
         classification -> Double.toHexString(classification.number));
 
 //    long before = System.nanoTime();
diff --git a/ml_test/src/main/resources/log4j2.xml b/ml_test/src/main/resources/log4j2.xml
index a5132e32502bf95c14ac0e6e50c2185325643ca7..686c2a889038bd7e7d89928939edfd09a5f15a94 100644
--- a/ml_test/src/main/resources/log4j2.xml
+++ b/ml_test/src/main/resources/log4j2.xml
@@ -4,8 +4,8 @@
         <Console name="Console">
             <PatternLayout pattern="%highlight{%d{HH:mm:ss.SSS} %-5level} %c{1.} - %msg%n"/>
         </Console>
-        <RollingFile name="RollingFile" fileName="logs/jastadd-mquat.log"
-                    filePattern="logs/jastadd-mquat-%i.log">
+        <RollingFile name="RollingFile" fileName="logs/eraser.log"
+                    filePattern="logs/eraser-%i.log">
             <PatternLayout pattern="%d{HH:mm:ss.SSS} %-5level %logger{36} - %msg%n"/>
             <Policies>
                 <OnStartupTriggeringPolicy/>
diff --git a/ml_test_boqi/.gitignore b/ml_test_boqi/.gitignore
new file mode 100644
index 0000000000000000000000000000000000000000..70b583e34c3316bcd77c807e2d6b85db5e7d49f6
--- /dev/null
+++ b/ml_test_boqi/.gitignore
@@ -0,0 +1,3 @@
+/build/
+/bin/
+logs/
diff --git a/ml_test_boqi/build.gradle b/ml_test_boqi/build.gradle
new file mode 100644
index 0000000000000000000000000000000000000000..d07b5930b7d3c75216b36bb9f5d9fa08b5d9d2c0
--- /dev/null
+++ b/ml_test_boqi/build.gradle
@@ -0,0 +1,33 @@
+repositories {
+    mavenCentral()
+}
+
+sourceCompatibility = 1.8
+
+apply plugin: 'java'
+apply plugin: 'application'
+
+dependencies {
+    compile project(':eraser-base')
+    compile group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: '2.9.8'
+    compile group: 'org.apache.logging.log4j', name: 'log4j-api', version: '2.11.1'
+    compile group: 'org.apache.logging.log4j', name: 'log4j-core', version: '2.11.1'
+    testCompile group: 'junit', name: 'junit', version: '4.12'
+    testCompile group: 'org.hamcrest', name: 'hamcrest-junit', version: '2.0.0.0'
+}
+
+run {
+    mainClassName = 'de.tudresden.inf.st.eraser.ml_test_boqi.Main'
+    standardInput = System.in
+    if (project.hasProperty("appArgs")) {
+        args Eval.me(appArgs)
+    }
+}
+
+sourceSets {
+    main {
+        java {
+            srcDir 'src/main/java'
+        }
+    }
+}
diff --git a/ml_test_boqi/src/main/java/de/tudresden/inf/st/eraser/ml_test_boqi/Main.java b/ml_test_boqi/src/main/java/de/tudresden/inf/st/eraser/ml_test_boqi/Main.java
new file mode 100644
index 0000000000000000000000000000000000000000..f42663472e3a986c82df86e4bf89d0f9ff706441
--- /dev/null
+++ b/ml_test_boqi/src/main/java/de/tudresden/inf/st/eraser/ml_test_boqi/Main.java
@@ -0,0 +1,219 @@
+package de.tudresden.inf.st.eraser.ml_test_boqi;
+
+import de.tudresden.inf.st.eraser.jastadd.model.*;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+import org.apache.commons.math3.stat.StatUtils;
+import de.tudresden.inf.st.eraser.jastadd.model.Item;
+
+
+public class Main {
+
+  private static final Logger logger = LogManager.getLogger(Main.class);
+
+  public static void main(String[] args) {
+
+    logger.info("Hello World!");
+    createAndTestBrightnessNetwork();
+  }
+  private static Root createModel() {
+    Root model = Root.createEmptyRoot();
+    Group group = new Group();
+    group.setID("Group1");
+    model.addGroup(group);
+
+    // inputs items muss normalize 1.0, 0.06666666666666665, 0.4545454545454546, -0.5593220338983051, 1(bias)
+
+    NumberItem monthItem = new NumberItem();
+    monthItem.setState(-1.0);
+    monthItem.setID("month");
+    monthItem.setLabel("datetime-month");
+
+    NumberItem dayItem = new NumberItem();
+    dayItem.setState(0.2666666666666666);
+    dayItem.setID("day");
+    dayItem.setLabel("datetime-day");
+
+    NumberItem hourItem = new NumberItem();
+    hourItem.setState(-0.6363636363636364);
+    hourItem.setID("hour");
+    hourItem.setLabel("datetime-hour");
+
+    NumberItem minuteItem = new NumberItem();
+    minuteItem.setState(-0.5593220338983051);
+    minuteItem.setID("minute");
+    minuteItem.setLabel("datetime-minute");
+
+    NumberItem biasItem = new NumberItem();
+    biasItem.setState(1);
+    biasItem.setID("bias");
+    biasItem.setLabel("bias");
+
+    group.addItem(monthItem);
+    group.addItem(dayItem);
+    group.addItem(hourItem);
+    group.addItem(minuteItem);
+    group.addItem(biasItem);
+    return model;
+  }
+  private static final int REPETITIONS = 1;
+  private static void classifyTimed(
+          NeuralNetworkRoot nn,
+          Function<NeuralNetworkRoot, DoubleNumber> classify,
+          Function<DoubleNumber, String> leafToString) {
+    List<String> results = new ArrayList<>();
+    List<Long> times = new ArrayList<>();
+    long before = System.nanoTime();
+    DoubleNumber classification = classify.apply(nn);
+    long diff = System.nanoTime() - before;
+    results.add(leafToString.apply(classification));
+    times.add(TimeUnit.NANOSECONDS.toMillis(diff));
+    logger.info("Classification results: {}", results);
+    logger.info("Took {}ms", String.join("ms, ", times.stream().map(l -> Long.toString(l)).collect(Collectors.toList())));
+    logger.info("Took on average: {}ms",
+            Arrays.stream(times.toArray(new Long[0])).mapToLong(l -> l).average().orElse(-1));
+    logger.info("Took on median: {}ms",
+            Arrays.stream(times.toArray(new Long[0])).mapToLong(l -> l).sorted()
+                    .skip((REPETITIONS - 1) / 2).limit(2 - REPETITIONS % 2).average().orElse(Double.NaN));
+  }
+
+  /**
+   * Purpose: Create a neural network with 3 layers (5 + 8 + 4 neurons)
+   */
+  private static void createAndTestBrightnessNetwork() {
+    Root model = createModel();
+    Item monthItem = model.resolveItem("month").orElseThrow(
+            () -> new RuntimeException("Month not found"));
+    Item dayItem = model.resolveItem("day").orElseThrow(
+            () -> new RuntimeException("Day not found"));
+    Item hourItem = model.resolveItem("hour").orElseThrow(
+            () -> new RuntimeException("Hour not found"));
+    Item minuteItem = model.resolveItem("minute").orElseThrow(
+            () -> new RuntimeException("Minute not found"));
+    Item biasItem = model.resolveItem("bias").orElseThrow(
+            () -> new RuntimeException("Bias not found"));
+
+    NeuralNetworkRoot nn = new NeuralNetworkRoot();
+
+    DoubleArrayDoubleFunction sigmoid = inputs -> Math.signum(Arrays.stream(inputs).sum());
+    DoubleArrayDoubleFunction tanh= inputs ->Math.tanh(Arrays.stream(inputs).sum());
+    DoubleArrayDoubleFunction function_one= inputs->function_one();
+
+    //Weights outputs from learner Module
+    ArrayList<Double> weights= new ArrayList<Double>(Arrays.asList(
+            -4.8288886204,0.6723236931,2.1451097188,-0.8551053267,-0.7858304445,4.1369566727,-3.3096691918,
+            -0.2190980261,2.6871317298,1.2272772167,-2.5292510941,-1.2860407542,-4.2280191541,1.004752063,
+            0.8345207039,0.0123185817,-0.5921808915,0.0967336988,-0.305892589,0.5572392781,-0.7190098073,
+            -1.6247354373,0.4589248822,-0.0269816271,2.2208040852,-3.6281085698,0.2204999381,4.7263701556,
+            -4.8348948698,0.231141867,8.7120706018,-1.4912707741,0.9482851705,0.1377551973,-6.6525856465,
+            -1.321197315,-2.7369948929,17.664289214,-3.1279212743,-0.8245974167,-1.4251924355,0.8370511414,
+            2.0841638143,-0.210152817,-1.9414132298,-1.7973688846,-2.1977997794,-3.6046836685,-3.3403186721,
+            -6.1556924635,-2.8952903587,-1.0773989561,0.2300429028,-0.2184650371,0.0297181797,0.5709092417,
+            1.3960358442,-3.1577981239,0.0423944625,-17.8143314027,-1.4439317172,-0.5137688896,1.0166045804,
+            0.3059149818,1.0938282764,0.6203368549,0.702449827));
+    // input layer
+    InputNeuron month = new InputNeuron();
+    month.setItem(monthItem);
+    InputNeuron day = new InputNeuron();
+    day.setItem(dayItem);
+    InputNeuron hour = new InputNeuron();
+    hour.setItem(hourItem);
+    InputNeuron minute = new InputNeuron();
+    minute.setItem(minuteItem);
+    InputNeuron bias = new InputNeuron();
+    bias.setItem(biasItem);
+
+    nn.addInputNeuron(month);
+    nn.addInputNeuron(day);
+    nn.addInputNeuron(hour);
+    nn.addInputNeuron(minute);
+    nn.addInputNeuron(bias);
+
+    // output layer
+    OutputLayer outputLayer = new OutputLayer();
+    OutputNeuron output0 = new OutputNeuron();
+    output0.setActivationFormula(tanh);
+    OutputNeuron output1 = new OutputNeuron();
+    output1.setActivationFormula(tanh);
+    OutputNeuron output2 = new OutputNeuron();
+    output2.setActivationFormula(tanh);
+    OutputNeuron output3 = new OutputNeuron();
+    output3.setActivationFormula(tanh);
+
+    outputLayer.addOutputNeuron(output0);
+    outputLayer.addOutputNeuron(output1);
+    outputLayer.addOutputNeuron(output2);
+    outputLayer.addOutputNeuron(output3);
+
+    outputLayer.setCombinator(inputs->predictor(inputs));
+    nn.setOutputLayer(outputLayer);
+
+    // hidden layer
+    HiddenNeuron[] hiddenNeurons = new HiddenNeuron[8];
+    for (int i = 0; i < (hiddenNeurons.length); i++) {
+
+      if (i==7){
+        HiddenNeuron hiddenNeuron = new HiddenNeuron();
+        hiddenNeuron.setActivationFormula(function_one);
+        hiddenNeurons[i] = hiddenNeuron;
+        nn.addHiddenNeuron(hiddenNeuron);
+        bias.connectTo(hiddenNeuron,1.0);
+        hiddenNeuron.connectTo(output0, weights.get(i));
+        hiddenNeuron.connectTo(output1, weights.get(i+8));
+        hiddenNeuron.connectTo(output2, weights.get(i+8*2));
+        hiddenNeuron.connectTo(output3, weights.get(i+8*3));
+      }
+     else{
+        HiddenNeuron hiddenNeuron = new HiddenNeuron();
+        hiddenNeuron.setActivationFormula(tanh);
+        hiddenNeurons[i] = hiddenNeuron;
+        nn.addHiddenNeuron(hiddenNeuron);
+
+        month.connectTo(hiddenNeuron, weights.get((hiddenNeurons.length*4)+i*5));
+        day.connectTo(hiddenNeuron, weights.get((hiddenNeurons.length*4+1)+i*5));
+        hour.connectTo(hiddenNeuron, weights.get((hiddenNeurons.length*4+2)+i*5));
+        minute.connectTo(hiddenNeuron, weights.get((hiddenNeurons.length*4+3)+i*5));
+        bias.connectTo(hiddenNeuron,weights.get((hiddenNeurons.length*4+4)+i*5));
+        hiddenNeuron.connectTo(output0, weights.get(i));
+        hiddenNeuron.connectTo(output1, weights.get(i+8));
+        hiddenNeuron.connectTo(output2, weights.get(i+8*2));
+        hiddenNeuron.connectTo(output3, weights.get(i+8*3));}
+    }
+
+    model.getMachineLearningRoot().setPreferenceLearning(nn);
+    System.out.println(model.prettyPrint());
+
+    classifyTimed(nn, NeuralNetworkRoot::classify,
+            classification -> Double.toString(classification.number));
+  }
+  private static double function_one() {
+    return 1.0;
+  }
+  private static double predictor(double[] inputs) {
+    int index=0;
+    double maxinput=StatUtils.max(inputs);
+    System.out.println(inputs);
+    for (int i = 0; i < inputs.length; i++)
+    {
+      if (inputs[i] == maxinput){
+        index=i;
+      }
+    }
+    //outputs from learner
+    ArrayList<Double> outputs= new ArrayList<Double>(Arrays.asList(2.0,1.0,3.0,0.0));
+    double output=outputs.get(index);
+    return output;
+  }
+}
+
+//inputs:
+//[BasicMLData:-1.0,0.2666666666666666,-0.6363636363636364,-0.5593220338983051]
+//outputs:
+//[BasicMLData:-0.9151867668336432,-0.1568555041251098,-0.9786996639280675,-0.9436628188408074]
+//[7, 20, 12, 13] -> predicted: 1(correct: 2)
\ No newline at end of file
diff --git a/ml_test_boqi/src/main/resources/log4j2.xml b/ml_test_boqi/src/main/resources/log4j2.xml
new file mode 100644
index 0000000000000000000000000000000000000000..0594576fac98ba859e411597c90c8e3d989378bd
--- /dev/null
+++ b/ml_test_boqi/src/main/resources/log4j2.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Configuration>
+    <Appenders>
+        <Console name="Console">
+            <PatternLayout pattern="%highlight{%d{HH:mm:ss.SSS} %-5level} %c{1.} - %msg%n"/>
+        </Console>
+        <RollingFile name="RollingFile" fileName="logs/jastadd-mquat.log"
+                    filePattern="logs/jastadd-mquat-%i.log">
+            <PatternLayout pattern="%d{HH:mm:ss.SSS} %-5level %logger{36} - %msg%n"/>
+            <Policies>
+                <OnStartupTriggeringPolicy/>
+            </Policies>
+            <DefaultRolloverStrategy max="20"/>
+        </RollingFile>
+    </Appenders>
+    <Loggers>
+        <Root level="debug">
+            <AppenderRef ref="Console"/>
+            <AppenderRef ref="RollingFile"/>
+        </Root>
+    </Loggers>
+</Configuration>
diff --git a/openhab-mock/.gitignore b/openhab-mock/.gitignore
index 83ccc54d0286590d31587a71aaa789e780bff41e..70b583e34c3316bcd77c807e2d6b85db5e7d49f6 100644
--- a/openhab-mock/.gitignore
+++ b/openhab-mock/.gitignore
@@ -1,2 +1,3 @@
 /build/
 /bin/
+logs/
diff --git a/openhab-mock/build.gradle b/openhab-mock/build.gradle
index 94017c3d7308df49f404a1c49203b807fa4a5ef7..d4197d59aacaebd8d31ff349c2ed78007f0bfb2d 100644
--- a/openhab-mock/build.gradle
+++ b/openhab-mock/build.gradle
@@ -1,14 +1,6 @@
-apply plugin: 'java'
 apply plugin: 'application'
 
-sourceCompatibility = 1.8
-
-repositories {
-    mavenCentral()
-}
-
 run {
-//    mainClassName = 'de.tudresden.inf.st.eraser.openhab_mock.MockMain'
     mainClassName = 'de.tudresden.inf.st.eraser.openhab_mock.HueMain'
     standardInput = System.in
     if (project.hasProperty("appArgs")) {
@@ -19,9 +11,5 @@ run {
 dependencies {
     compile project(':eraser-base')
     compile project(':commons.color')
-    compile group: 'org.apache.logging.log4j', name: 'log4j-api', version: '2.11.1'
-    compile group: 'org.apache.logging.log4j', name: 'log4j-core', version: '2.11.1'
-    compile 'com.opencsv:opencsv:3.8'
-    compile 'org.apache.commons:commons-math3:3.6.1'
-    testCompile group: 'junit', name: 'junit', version: '4.12'
+    compile group: 'com.opencsv', name: 'opencsv', version: '3.8'
 }
diff --git a/openhab-mock/src/main/resources/log4j2.xml b/openhab-mock/src/main/resources/log4j2.xml
index 89799a2f09ba34d288e610d960b3ed6348213105..18175a02521156259c8789745fb849fa893302e9 100644
--- a/openhab-mock/src/main/resources/log4j2.xml
+++ b/openhab-mock/src/main/resources/log4j2.xml
@@ -4,8 +4,8 @@
         <Console name="Console">
             <PatternLayout pattern="%highlight{%d{HH:mm:ss.SSS} %-5level} %c{1.} - %msg%n"/>
         </Console>
-        <RollingFile name="RollingFile" fileName="logs/jastadd-mquat.log"
-                    filePattern="logs/jastadd-mquat-%i.log">
+        <RollingFile name="RollingFile" fileName="logs/eraser.log"
+                    filePattern="logs/eraser-%i.log">
             <PatternLayout pattern="%d{HH:mm:ss.SSS} %-5level %logger{36} - %msg%n"/>
             <Policies>
                 <OnStartupTriggeringPolicy/>
diff --git a/org.openhab.action.machinelearn/.gitignore b/org.openhab.action.machinelearn/.gitignore
index 6ecea694e9b18e4e42bed81e7e4d44bed9301fb7..64c2056c6f34fbf226e041a575bdd73dff75df1f 100644
--- a/org.openhab.action.machinelearn/.gitignore
+++ b/org.openhab.action.machinelearn/.gitignore
@@ -1,2 +1,3 @@
 build/
 /bin/
+logs/
diff --git a/org.openhab.action.machinelearn/build.gradle b/org.openhab.action.machinelearn/build.gradle
index 12d6d1e875fee2da8bb334ebcf14e95d9e3c66af..7360d4c8b932a5805e9bd29f21960d3c67b9851e 100644
--- a/org.openhab.action.machinelearn/build.gradle
+++ b/org.openhab.action.machinelearn/build.gradle
@@ -1,35 +1,12 @@
-repositories {
-    mavenCentral()
-}
-
-sourceCompatibility = 1.8
-
-apply plugin: 'java'
-//apply plugin: 'application'
-
 dependencies {
     compile files('lib/weka.jar')
     compile project(':stub.org.openhab.core.scriptengine.action')
-//    compile 'org.apache.commons:commons-lang3:3.8.1'
-//    compile group: 'org.eclipse.jdt', name: 'org.eclipse.jdt.annotation', version: '2.0.0'
-//    compile 'org.openhab.core.library.types'
-//    compile 'org.openhab.core.scriptengine.action'
     compile group: 'org.osgi', name: 'org.osgi.framework', version: '1.9.0'
     compile group: 'org.osgi', name: 'org.osgi.service.cm', version: '1.6.0'
     compile group: 'org.osgi', name: 'org.osgi.service.component', version: '1.4.0'
     compile group: 'org.slf4j', name: 'slf4j-api', version: '1.7.25'
-    testCompile group: 'junit', name: 'junit', version: '4.12'
-    testCompile group: 'org.hamcrest', name: 'hamcrest-junit', version: '2.0.0.0'
 }
 
-//run {
-//    mainClassName = 'de.tudresden.inf.st.eraser.skywriter_hue_integration.Main'
-//    standardInput = System.in
-//    if (project.hasProperty("appArgs")) {
-//        args Eval.me(appArgs)
-//    }
-//}
-
 sourceSets {
     main {
         java {
diff --git a/org.openlicht.action.reinforcementlearning/.gitignore b/org.openlicht.action.reinforcementlearning/.gitignore
index 6ecea694e9b18e4e42bed81e7e4d44bed9301fb7..64c2056c6f34fbf226e041a575bdd73dff75df1f 100644
--- a/org.openlicht.action.reinforcementlearning/.gitignore
+++ b/org.openlicht.action.reinforcementlearning/.gitignore
@@ -1,2 +1,3 @@
 build/
 /bin/
+logs/
diff --git a/org.openlicht.action.reinforcementlearning/build.gradle b/org.openlicht.action.reinforcementlearning/build.gradle
index 54e1daf916b41766a9c4dc961e365040744ff608..572c87ea95295ba93b68e1f8bae5bed2dcc0149d 100644
--- a/org.openlicht.action.reinforcementlearning/build.gradle
+++ b/org.openlicht.action.reinforcementlearning/build.gradle
@@ -1,25 +1,11 @@
-repositories {
-    mavenCentral()
-}
-
-sourceCompatibility = 1.8
-
-apply plugin: 'java'
-//apply plugin: 'application'
-
 dependencies {
     compile files('lib/encog-core-3.4.jar')
     compile project(':stub.org.openhab.core.scriptengine.action')
-    compile 'org.apache.commons:commons-lang3:3.8.1'
     compile group: 'org.eclipse.jdt', name: 'org.eclipse.jdt.annotation', version: '2.2.200'
-//    compile 'org.openhab.core.library.types'
-//    compile 'org.openhab.core.scriptengine.action'
     compile group: 'org.osgi', name: 'org.osgi.framework', version: '1.9.0'
     compile group: 'org.osgi', name: 'org.osgi.service.cm', version: '1.6.0'
     compile group: 'org.osgi', name: 'org.osgi.service.component', version: '1.4.0'
     compile group: 'org.slf4j', name: 'slf4j-api', version: '1.7.25'
-    testCompile group: 'junit', name: 'junit', version: '4.12'
-    testCompile group: 'org.hamcrest', name: 'hamcrest-junit', version: '2.0.0.0'
 }
 
 sourceSets {
diff --git a/project-template/.gitignore b/project-template/.gitignore
index 84c048a73cc2e5dd24f807669eb99b0ce3123195..54c5c0b024dab04da2a43d4135ed67ed2a670b46 100644
--- a/project-template/.gitignore
+++ b/project-template/.gitignore
@@ -1 +1,2 @@
 /build/
+logs/
diff --git a/project-template/build.gradle b/project-template/build.gradle
index 9cb094bf6e4b76641f50f823f9f526b8c8ba7657..3bae16225ca01a6fea919d36be3a383c6a62507f 100644
--- a/project-template/build.gradle
+++ b/project-template/build.gradle
@@ -1,19 +1,7 @@
-repositories {
-    mavenCentral()
-}
-
-sourceCompatibility = 1.8
-
-apply plugin: 'java'
 apply plugin: 'application'
 
 dependencies {
     compile project(':eraser-base')
-    compile group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: '2.9.8'
-    compile group: 'org.apache.logging.log4j', name: 'log4j-api', version: '2.11.1'
-    compile group: 'org.apache.logging.log4j', name: 'log4j-core', version: '2.11.1'
-    testCompile group: 'junit', name: 'junit', version: '4.12'
-    testCompile group: 'org.hamcrest', name: 'hamcrest-junit', version: '2.0.0.0'
 }
 
 run {
diff --git a/project-template/src/main/resources/log4j2.xml b/project-template/src/main/resources/log4j2.xml
index 0594576fac98ba859e411597c90c8e3d989378bd..867ec439d0a32dcb5f8b3e2d0c7485d7d8da418c 100644
--- a/project-template/src/main/resources/log4j2.xml
+++ b/project-template/src/main/resources/log4j2.xml
@@ -4,8 +4,8 @@
         <Console name="Console">
             <PatternLayout pattern="%highlight{%d{HH:mm:ss.SSS} %-5level} %c{1.} - %msg%n"/>
         </Console>
-        <RollingFile name="RollingFile" fileName="logs/jastadd-mquat.log"
-                    filePattern="logs/jastadd-mquat-%i.log">
+        <RollingFile name="RollingFile" fileName="logs/eraser.log"
+                    filePattern="logs/eraser-%i.log">
             <PatternLayout pattern="%d{HH:mm:ss.SSS} %-5level %logger{36} - %msg%n"/>
             <Policies>
                 <OnStartupTriggeringPolicy/>
diff --git a/project-template/src/test/java/de/tudresden/inf/st/eraser/projectName/ATest.java b/project-template/src/test/java/de/tudresden/inf/st/eraser/projectName/ATest.java
index 59db3af92a329827411cb89a18dc3ea4ab833020..50f834da9ece7f0ba9ba3ed51c2adedbfbe318eb 100644
--- a/project-template/src/test/java/de/tudresden/inf/st/eraser/projectName/ATest.java
+++ b/project-template/src/test/java/de/tudresden/inf/st/eraser/projectName/ATest.java
@@ -17,6 +17,6 @@ public class ATest {
 
   @Test
   public void test1() {
-    fail();
+
   }
 }
diff --git a/ragdoc-view b/ragdoc-view
new file mode 160000
index 0000000000000000000000000000000000000000..4bb8afb1bc3dc6b346e04e72c5896568b2e9b192
--- /dev/null
+++ b/ragdoc-view
@@ -0,0 +1 @@
+Subproject commit 4bb8afb1bc3dc6b346e04e72c5896568b2e9b192
diff --git a/settings.gradle b/settings.gradle
index 44b66ee077cceb9a26f2cfed97b319d4a30741a3..0a118ec458b406dfae388bf1218c2abd4bfd356b 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -3,6 +3,7 @@ rootProject.name = 'eraser'
 include ':eraser-base'
 include 'openhab-mock'
 include 'integration'
+include ':benchmark'
 include ':commons.color'
 include ':skywriter-hue-integration'
 include ':org.openhab.action.machinelearn'
@@ -19,3 +20,5 @@ include ':feedbackloop.learner'
 include ':influx_test'
 include ':eraser.spark'
 include ':eraser.starter'
+include ':feedbackloop.learner_backup'
+include ':datasets'
diff --git a/skywriter-hue-integration/.gitignore b/skywriter-hue-integration/.gitignore
index 83ccc54d0286590d31587a71aaa789e780bff41e..70b583e34c3316bcd77c807e2d6b85db5e7d49f6 100644
--- a/skywriter-hue-integration/.gitignore
+++ b/skywriter-hue-integration/.gitignore
@@ -1,2 +1,3 @@
 /build/
 /bin/
+logs/
diff --git a/skywriter-hue-integration/build.gradle b/skywriter-hue-integration/build.gradle
index ab64e3d71e5b0b3c4e5939b2ffa387718e05a242..410a39b582bd8ea9316289b95c1f7f0083de6576 100644
--- a/skywriter-hue-integration/build.gradle
+++ b/skywriter-hue-integration/build.gradle
@@ -1,20 +1,8 @@
-repositories {
-    mavenCentral()
-}
-
-sourceCompatibility = 1.8
-
-apply plugin: 'java'
 apply plugin: 'application'
 
 dependencies {
     compile project(':eraser-base')
     compile project(':commons.color')
-    compile group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: '2.9.8'
-    compile group: 'org.apache.logging.log4j', name: 'log4j-api', version: '2.11.1'
-    compile group: 'org.apache.logging.log4j', name: 'log4j-core', version: '2.11.1'
-    testCompile group: 'junit', name: 'junit', version: '4.12'
-    testCompile group: 'org.hamcrest', name: 'hamcrest-junit', version: '2.0.0.0'
 }
 
 run {
diff --git a/skywriter-hue-integration/src/main/java/de/tudresden/inf/st/eraser/skywriter_hue_integration/Main.java b/skywriter-hue-integration/src/main/java/de/tudresden/inf/st/eraser/skywriter_hue_integration/Main.java
index 19d08a1372d4bff1d308f036b50c64fe05106346..3f64c8cdd3c530d8a287a0d3ce0d140f449c5921 100644
--- a/skywriter-hue-integration/src/main/java/de/tudresden/inf/st/eraser/skywriter_hue_integration/Main.java
+++ b/skywriter-hue-integration/src/main/java/de/tudresden/inf/st/eraser/skywriter_hue_integration/Main.java
@@ -3,9 +3,7 @@ package de.tudresden.inf.st.eraser.skywriter_hue_integration;
 import beaver.Parser;
 import de.tudresden.inf.st.eraser.commons.color.ColorUtils;
 import de.tudresden.inf.st.eraser.commons.color.ColorUtils.RGBvalues;
-import de.tudresden.inf.st.eraser.jastadd.model.Item;
-import de.tudresden.inf.st.eraser.jastadd.model.Root;
-import de.tudresden.inf.st.eraser.jastadd.model.Rule;
+import de.tudresden.inf.st.eraser.jastadd.model.*;
 import de.tudresden.inf.st.eraser.openhab2.mqtt.MQTTUpdater;
 import de.tudresden.inf.st.eraser.util.ParserUtils;
 import org.apache.logging.log4j.LogManager;
@@ -38,7 +36,8 @@ public class Main {
   @SuppressWarnings("ResultOfMethodCallIgnored")
   public static void main(String[] args) throws IOException, Parser.Exception {
     // use openHAB-eraser-connection to update hue (automatically done)
-    Root model = ParserUtils.load("skywriter-hue.eraser", Main.class);
+    Root root = ParserUtils.load("skywriter-hue.eraser", Main.class);
+    OpenHAB2Model model = root.getOpenHAB2Model();
     Item irisItem = model.resolveItem("iris1_item").orElseThrow(() ->
         new NoSuchElementException("Iris1_item not found"));
     Item skywriter1_x = model.resolveItem("skywriter1_x").orElseThrow(() ->
@@ -47,20 +46,23 @@ public class Main {
         new NoSuchElementException("Skywriter1 y not found"));
     Item skywriter1_xyz = model.resolveItem("skywriter1_xyz").orElseThrow(() ->
         new NoSuchElementException("Skywriter1 xyz not found"));
-    System.out.println(model.prettyPrint());
+    System.out.println(root.prettyPrint());
     // define rule to switch color, based on xyz-to-rgb-to-hsb mapping
     Rule mapXYtoIrisState = new Rule();
-    model.addRule(mapXYtoIrisState);
-    mapXYtoIrisState.addEventFor(skywriter1_xyz, "x,y, or z changed");
+    root.addRule(mapXYtoIrisState);
+    mapXYtoIrisState.activateFor(skywriter1_xyz);
 //    mapXYtoIrisState.addEventFor(skywriter1_y, "y changed");
-    mapXYtoIrisState.setCondition(root -> true); // always fire
-    mapXYtoIrisState.setAction(root -> updateStateRGB(skywriter1_xyz, irisItem));
+    SetStateFromItemsAction action = new SetStateFromItemsAction();
+    action.addSourceItem(skywriter1_xyz);
+    action.setCombinator(items -> updateStateRGB(items.iterator().next()));
+    action.setAffectedItem(irisItem);
+    mapXYtoIrisState.addAction(action);
 
     // get mqtt message directly from skywriter, not via binding (which is not working atm)
     Lock abortLock = new ReentrantLock();
     Condition abortCondition = abortLock.newCondition();
     Thread readFromOpenHABThread = new Thread(() -> {
-      try (MQTTUpdater updater = new MQTTUpdater(model)) {
+      try (MQTTUpdater updater = new MQTTUpdater(root)) {
         updater.start();
         if (!updater.waitUntilReady(3, TimeUnit.SECONDS)) {
           logger.error("openHAB reader not ready. Aborting.");
@@ -80,7 +82,7 @@ public class Main {
       }
     });
     if (USE_READ_FROM_OPENHAB || SEND_TO_OPENHAB) {
-      model.getMqttRoot().setHostByName(MQTT_HOST);
+      root.getMqttRoot().setHostByName(MQTT_HOST);
     }
     if (USE_READ_FROM_OPENHAB) {
       readFromOpenHABThread.start();
@@ -120,7 +122,7 @@ public class Main {
     }
 
     // wait for user to press enter
-    System.out.println("Press [Enter] to quit updating the model.");
+    System.out.println("Press [Enter] to quit updating the root.");
     System.in.read();
 
     // and then cancel the threads
@@ -152,7 +154,7 @@ public class Main {
     irisItem.setStateFromString(String.format("%d,%d,%d", hsb.hue, hsb.saturation, hsb.brightness));
   }
 
-  private static void updateStateRGB(Item skywriter1_xyz, Item irisItem) {
+  private static String updateStateRGB(Item skywriter1_xyz) {
     String[] tokens = skywriter1_xyz.getStateAsString().split(",");
     int[] rgbArray = new int[3];
     for (int i = 0; i < 3; i++) {
@@ -161,6 +163,6 @@ public class Main {
     }
     ColorUtils.HSBvalues255 hsb = ColorUtils.convertRGBtoHSB(RGBvalues.of(rgbArray))
         .toIntegral().ensureBounds();
-    irisItem.setStateFromString(String.format("%d,%d,%d", hsb.hue, hsb.saturation, hsb.brightness));
+    return String.format("%d,%d,%d", hsb.hue, hsb.saturation, hsb.brightness);
   }
 }
diff --git a/skywriter-hue-integration/src/main/resources/log4j2.xml b/skywriter-hue-integration/src/main/resources/log4j2.xml
index 5c534092d64e9c1834c2ba20208c057e2b56be16..5d1091ea995c881e5985a0cfc925a952ff50d0bc 100644
--- a/skywriter-hue-integration/src/main/resources/log4j2.xml
+++ b/skywriter-hue-integration/src/main/resources/log4j2.xml
@@ -4,8 +4,8 @@
         <Console name="Console">
             <PatternLayout pattern="%highlight{%d{HH:mm:ss.SSS} %-5level} %c{1.} - %msg%n"/>
         </Console>
-        <RollingFile name="RollingFile" fileName="logs/jastadd-mquat.log"
-                    filePattern="logs/jastadd-mquat-%i.log">
+        <RollingFile name="RollingFile" fileName="logs/eraser.log"
+                    filePattern="logs/eraser-%i.log">
             <PatternLayout pattern="%d{HH:mm:ss.SSS} %-5level %logger{36} - %msg%n"/>
             <Policies>
                 <OnStartupTriggeringPolicy/>
diff --git a/skywriter-hue-integration/src/main/resources/smart_arrive.eraser b/skywriter-hue-integration/src/main/resources/smart_arrive.eraser
new file mode 100644
index 0000000000000000000000000000000000000000..44b67cf94d3bb8da666cc38743f008552041a152
--- /dev/null
+++ b/skywriter-hue-integration/src/main/resources/smart_arrive.eraser
@@ -0,0 +1,44 @@
+ThingType: id="hue:bridge" label="Hue Bridge" description="The hue bridge represents the Philips hue bridge." parameters=[] channelTypes=[] ;
+ThingType: id="hue:0200" label="Color Light" description="A dimmable light with changeable colors." ;
+ThingType: id="hue:0210" label="Extended Color Light" description="A dimmable light with changeable colors and tunable color temperature." parameters=[] channelTypes=[];
+ThingType: id="skywriter-hat" label="SkyWriterHAT" description="SkyWriterHAT Gesture Recognition" parameters=["brokername"] channelTypes=["flick-type", "skywriter-x", "skywriter-y"] ;
+
+Thing: id="hue:bridge:0017880adcf4" label="Philips hue (10.8.0.160)" type="hue:bridge" channels=[] ;
+Thing: id="hue:0210:0017880adcf4:1" label="Wohnzimmer" type="hue:0210" channels=["hue:0210:0017880adcf4:1:color_temperature", "hue:0210:0017880adcf4:1:effect"] ;
+Thing: id="hue:0200:0017880adcf4:5" label="Hue iris 1" type="hue:0200" channels=["hue:0210:0017880adcf4:4:color", "hue:0210:0017880adcf4:4:color_temperature", "hue:0210:0017880adcf4:4:alert", "hue:0210:0017880adcf4:4:effect"] ;
+Thing: id="hue:0210:0017880adcf4:4" label="Hue go 1" type="hue:0210" channels=["hue:0200:0017880adcf4:5:color", "hue:0200:0017880adcf4:5:alert", "hue:0200:0017880adcf4:5:effect"] ;
+Thing: id="skywriter1" label="Our skywriter" type="skywriter-hat" channels=["skywriter1-flick", "skywriter1-x", "skywriter1-y"] ;
+
+ChannelType: id="hue:color" label="Color" description="The color channel allows to control the color of a light. It is also possible to dim values and switch the light on and off." itemType="Color" category="ColorLight" ;
+ChannelType: id="hue:alert" label="Alert" description="The alert channel allows a temporary change to the bulb’s state." itemType="String" category="Unknown" ;
+ChannelType: id="hue:effect" label="Color Loop" description="The effect channel allows putting the bulb in a color looping mode." itemType="Switch" category="ColorLight" ;
+ChannelType: id="hue:color_temperature" label="Color Temperature" description="The color temperature channel allows to set the color temperature of a light from 0 (cold) to 100 (warm)." itemType="Dimmer" category="ColorLight" ;
+ChannelType: id="flick-type" itemType="String" label="Last Flick" description="Last Flick detected (and its direction)" category="Motion" readOnly ;
+ChannelType: id="skywriter-x" itemType="String" label="Current X coordinate" description="Current X coordinate" category="Motion" readOnly ;
+ChannelType: id="skywriter-y" itemType="String" label="Current Y coordinate" description="Current Y coordinate" category="Motion" readOnly ;
+
+Channel: id="hue:0210:0017880adcf4:1:color_temperature" type="hue:color_temperature" links=[] ;
+Channel: id="hue:0210:0017880adcf4:1:effect" type="hue:effect" links=[] ;
+Channel: id="hue:0210:0017880adcf4:4:color" type="hue:color" links=[] ;
+Channel: id="hue:0210:0017880adcf4:4:color_temperature" type="hue:color_temperature" links=[] ;
+Channel: id="hue:0210:0017880adcf4:4:alert" type="hue:alert" links=[] ;
+Channel: id="hue:0210:0017880adcf4:4:effect" type="hue:effect" links=[] ;
+Channel: id="hue:0200:0017880adcf4:5:color" type="hue:color" links=["iris1_item"] ;
+Channel: id="hue:0200:0017880adcf4:5:alert" type="hue:alert" links=[] ;
+Channel: id="hue:0200:0017880adcf4:5:effect" type="hue:color" links=[] ;
+Channel: id="skywriter1-flick" type="flick-type" links=[];
+Channel: id="skywriter1-x" type="skywriter-x" links=["skywriter1_x"];
+Channel: id="skywriter1-y" type="skywriter-y" links=["skywriter1_y"];
+
+Item: id="iris1_item" label="Iris 1" state="121,88,68" topic="iris1_item/state";
+Item: id="skywriter1_x" label="X" state="0" topic="skywriter/x";
+Item: id="skywriter1_y" label="Y" state="0" topic="skywriter/y";
+//Item: id="skywriter1_xyz" label="Y" state="0" topic="skywriter/xyz";
+
+Group: id="Lights" items=["iris1_item"];
+Group: id="Skywriter" items=["skywriter1_x", "skywriter1_y"];
+
+Parameter: id="brokername" label="Broker Name" description="Name of the broker as defined in the &lt;broker&gt;.url in services/mqtt.cfg. See the MQTT Binding for more information on how to configure MQTT broker connections." type="Text" context="service" default="mosquitto" required ;
+
+// MqttHost is set programmatically
+Mqtt: incoming="oh2/out/" outgoing="oh2/in/" ;
diff --git a/stub.org.openhab.core.scriptengine.action/.gitignore b/stub.org.openhab.core.scriptengine.action/.gitignore
index 83ccc54d0286590d31587a71aaa789e780bff41e..70b583e34c3316bcd77c807e2d6b85db5e7d49f6 100644
--- a/stub.org.openhab.core.scriptengine.action/.gitignore
+++ b/stub.org.openhab.core.scriptengine.action/.gitignore
@@ -1,2 +1,3 @@
 /build/
 /bin/
+logs/
diff --git a/stub.org.openhab.core.scriptengine.action/build.gradle b/stub.org.openhab.core.scriptengine.action/build.gradle
index e2b54c7a70609c315c5b529678bdfb6cc2d8b461..128c3aa9c93c8dc9dd4aff8a245930bfb21b555a 100644
--- a/stub.org.openhab.core.scriptengine.action/build.gradle
+++ b/stub.org.openhab.core.scriptengine.action/build.gradle
@@ -1,18 +1,3 @@
-repositories {
-    mavenCentral()
-}
-
-sourceCompatibility = 1.8
-
-apply plugin: 'java'
-
-dependencies {
-    compile group: 'org.apache.logging.log4j', name: 'log4j-api', version: '2.11.1'
-    compile group: 'org.apache.logging.log4j', name: 'log4j-core', version: '2.11.1'
-    testCompile group: 'junit', name: 'junit', version: '4.12'
-    testCompile group: 'org.hamcrest', name: 'hamcrest-junit', version: '2.0.0.0'
-}
-
 sourceSets {
     main {
         java {
diff --git a/stub.org.openhab.core.scriptengine.action/src/main/resources/log4j2.xml b/stub.org.openhab.core.scriptengine.action/src/main/resources/log4j2.xml
index 0594576fac98ba859e411597c90c8e3d989378bd..867ec439d0a32dcb5f8b3e2d0c7485d7d8da418c 100644
--- a/stub.org.openhab.core.scriptengine.action/src/main/resources/log4j2.xml
+++ b/stub.org.openhab.core.scriptengine.action/src/main/resources/log4j2.xml
@@ -4,8 +4,8 @@
         <Console name="Console">
             <PatternLayout pattern="%highlight{%d{HH:mm:ss.SSS} %-5level} %c{1.} - %msg%n"/>
         </Console>
-        <RollingFile name="RollingFile" fileName="logs/jastadd-mquat.log"
-                    filePattern="logs/jastadd-mquat-%i.log">
+        <RollingFile name="RollingFile" fileName="logs/eraser.log"
+                    filePattern="logs/eraser-%i.log">
             <PatternLayout pattern="%d{HH:mm:ss.SSS} %-5level %logger{36} - %msg%n"/>
             <Policies>
                 <OnStartupTriggeringPolicy/>