Преглед изворни кода

Version 1.1 - added unitId, fixed bugs, etc

Lukas Cerny пре 5 година
родитељ
комит
3ef76c15e7
55 измењених фајлова са 812 додато и 748 уклоњено
  1. 2 1
      .gitignore
  2. 19 1
      README.md
  3. 1 1
      build.gradle
  4. 60 0
      config/analyzer_init.sql
  5. 3 3
      config/config.yaml
  6. 2 0
      doc/architecture.svg
  7. 1 0
      doc/architecture.xml
  8. 2 5
      src/main/java/cz/senslog/analyzer/analysis/AnalyzerComponent.java
  9. 3 3
      src/main/java/cz/senslog/analyzer/analysis/AnalyzerInfo.java
  10. 24 49
      src/main/java/cz/senslog/analyzer/analysis/AnalyzerModule.java
  11. 4 2
      src/main/java/cz/senslog/analyzer/analysis/Checker.java
  12. 3 4
      src/main/java/cz/senslog/analyzer/analysis/ObservationAnalyzer.java
  13. 0 35
      src/main/java/cz/senslog/analyzer/analysis/ThresholdRule.java
  14. 46 44
      src/main/java/cz/senslog/analyzer/analysis/module/CollectorHandler.java
  15. 24 12
      src/main/java/cz/senslog/analyzer/analysis/module/FilterHandler.java
  16. 19 41
      src/main/java/cz/senslog/analyzer/analysis/module/HandlersModule.java
  17. 20 15
      src/main/java/cz/senslog/analyzer/analysis/module/ThresholdHandler.java
  18. 2 2
      src/main/java/cz/senslog/analyzer/app/Application.java
  19. 5 1
      src/main/java/cz/senslog/analyzer/app/Configuration.java
  20. 0 6
      src/main/java/cz/senslog/analyzer/app/Constants.java
  21. 2 2
      src/main/java/cz/senslog/analyzer/core/EventBusModule.java
  22. 2 2
      src/main/java/cz/senslog/analyzer/core/api/BlockingHandler.java
  23. 2 2
      src/main/java/cz/senslog/analyzer/core/api/Builder.java
  24. 4 4
      src/main/java/cz/senslog/analyzer/core/api/ContextBuilder.java
  25. 1 1
      src/main/java/cz/senslog/analyzer/core/api/DataFinisher.java
  26. 1 1
      src/main/java/cz/senslog/analyzer/core/api/Handler.java
  27. 11 4
      src/main/java/cz/senslog/analyzer/core/api/HandlerContext.java
  28. 6 5
      src/main/java/cz/senslog/analyzer/core/api/HandlerInvoker.java
  29. 1 1
      src/main/java/cz/senslog/analyzer/core/api/HandlerRunnable.java
  30. 2 2
      src/main/java/cz/senslog/analyzer/core/api/NextInvokerBuilder.java
  31. 7 0
      src/main/java/cz/senslog/analyzer/domain/AggregationType.java
  32. 13 0
      src/main/java/cz/senslog/analyzer/domain/AttributeValue.java
  33. 19 21
      src/main/java/cz/senslog/analyzer/domain/Data.java
  34. 45 32
      src/main/java/cz/senslog/analyzer/domain/DoubleStatistics.java
  35. 0 22
      src/main/java/cz/senslog/analyzer/domain/FilteredSensor.java
  36. 54 5
      src/main/java/cz/senslog/analyzer/domain/Group.java
  37. 1 1
      src/main/java/cz/senslog/analyzer/domain/GroupBy.java
  38. 4 5
      src/main/java/cz/senslog/analyzer/domain/Observation.java
  39. 31 13
      src/main/java/cz/senslog/analyzer/domain/Sensor.java
  40. 38 18
      src/main/java/cz/senslog/analyzer/domain/Threshold.java
  41. 9 9
      src/main/java/cz/senslog/analyzer/domain/Timestamp.java
  42. 0 29
      src/main/java/cz/senslog/analyzer/persistence/AttributeCode.java
  43. 1 1
      src/main/java/cz/senslog/analyzer/persistence/ConnectionModule.java
  44. 4 4
      src/main/java/cz/senslog/analyzer/persistence/RepositoryModule.java
  45. 0 27
      src/main/java/cz/senslog/analyzer/persistence/SensorIdConverter.java
  46. 56 0
      src/main/java/cz/senslog/analyzer/persistence/repository/SensLogRepository.java
  47. 0 45
      src/main/java/cz/senslog/analyzer/persistence/repository/SenslogRepository.java
  48. 61 47
      src/main/java/cz/senslog/analyzer/persistence/repository/StatisticsConfigRepository.java
  49. 129 101
      src/main/java/cz/senslog/analyzer/persistence/repository/StatisticsRepository.java
  50. 7 7
      src/main/java/cz/senslog/analyzer/provider/ProviderConfig.java
  51. 2 2
      src/main/java/cz/senslog/analyzer/provider/ScheduleDatabaseProviderModule.java
  52. 31 10
      src/main/java/cz/senslog/analyzer/provider/ScheduledDatabaseProvider.java
  53. 27 73
      src/main/java/cz/senslog/analyzer/server/handler/StatisticsHandler.java
  54. 1 1
      src/main/java/cz/senslog/analyzer/server/vertx/VertxServer.java
  55. 0 26
      src/test/java/cz/senslog/analyzer/persistence/SensorIdConverterTest.java

+ 2 - 1
.gitignore

@@ -6,4 +6,5 @@ target
 .gradle
 build
 gradle
-
+*.log
+.DS_Store

+ 19 - 1
README.md

@@ -1,2 +1,20 @@
-# analyzer
+Spusteni programu s konfiguraci
+
+java -jar analyzer-1.0.jar -cf config.yaml > /dev/null &
+
+
+Ukazka dotazu na API
+
+http://127.0.0.1:9090/statistics?
+group_id=<GROUP_ID>&
+from=<FROM>&
+to=<TO>&
+groupBy=<ENUM_GROUP>
+
+
+GROUP_ID -> long number
+FROM -> yyyy-MM-dd HH:mm:ss+HH
+TO -> yyyy-MM-dd HH:mm:ss+HH
+GROUP -> [DAY, MONTH, YEAR]
+
 

+ 1 - 1
build.gradle

@@ -3,7 +3,7 @@ plugins {
 }
 
 group 'cz.senslog'
-version '1.0-SNAPSHOT'
+version '1.1-SNAPSHOT'
 
 sourceCompatibility = 1.8
 

+ 60 - 0
config/analyzer_init.sql

@@ -0,0 +1,60 @@
+
+\connect senslog1
+
+create schema if not exists statistics; 
+
+alter schema statistics OWNER TO senslog;
+
+/* table with calculated values */
+create table statistics.records (
+	id SERIAL PRIMARY KEY NOT NULL,
+	group_id bigint not null,
+	value_attribute text not null,      -- [MIN, MAX, SUM, COUNT]
+	record_value double precision not null,
+	time_interval integer not null,     -- interval in seconds
+	time_stamp timestamptz not null,
+    created timestamptz DEFAULT CURRENT_TIMESTAMP,
+    UNIQUE (group_id, value_attribute, time_stamp)
+);
+
+alter table statistics.records OWNER TO senslog;
+
+/* time interval to analyse sensor groups */
+create table statistics.groups_interval (
+    id SERIAL PRIMARY KEY NOT NULL,
+	time_interval integer not null,     -- interval in seconds
+    persistence boolean not null,
+    aggregation_type text not null      -- ['DOUBLE']
+);
+
+alter table statistics.groups_interval OWNER TO senslog;
+
+/* list of unit-sensor pair to run analyses */
+create table statistics.sensors (
+    id SERIAL PRIMARY KEY NOT NULL,
+    unit_id bigint not null,
+	sensor_id bigint not null,
+	CONSTRAINT fk_unit_sensor FOREIGN KEY (unit_id, sensor_id) REFERENCES public.units_to_sensors(unit_id, sensor_id)
+);
+
+alter table statistics.sensors OWNER TO senslog;
+
+/* thresholds to be checked */
+create table statistics.thresholds (
+    id SERIAL PRIMARY KEY NOT NULL,
+	group_id integer not null,
+	mode varchar(10) not null,      -- [le, lt, ge, gt, ne, eq]
+	property varchar(10) not null,  -- [MIN, MAX, AVG, VAL]
+	threshold_value double precision not null
+);
+
+alter table statistics.thresholds OWNER TO senslog;
+
+create table statistics.sensor_to_group (
+    id SERIAL PRIMARY KEY NOT NULL,
+    sensor_id integer REFERENCES statistics.sensors(id),
+    group_id integer REFERENCES statistics.groups_interval(id),
+    created timestamptz DEFAULT CURRENT_TIMESTAMP
+);
+
+alter table statistics.sensor_to_group OWNER TO senslog;

+ 3 - 3
config/config.yaml

@@ -1,11 +1,11 @@
 database:
-  url: "jdbc:postgresql://localhost:5432/postgres"
+  url: "jdbc:postgresql://localhost:5432/senslog1"
   username: "postgres"
-  password: "postgres"
+  password: "root"
   connectionPoolSize: 6
 
 scheduler:
-  initDate: 2020-01-30T15:00:00.000
+  initDate: "2020-01-30T14:00:00.00+02:00"
   period: 100
 
 server:

Разлика између датотеке није приказан због своје велике величине
+ 2 - 0
doc/architecture.svg


Разлика између датотеке није приказан због своје велике величине
+ 1 - 0
doc/architecture.xml


+ 2 - 5
src/main/java/cz/senslog/analyzer/analysis/AnalyzerComponent.java

@@ -9,9 +9,6 @@ import javax.inject.Singleton;
 @Component(modules = AnalyzerModule.class)
 public interface AnalyzerComponent {
 
-    @Named("advancedAnalyzer")
-    Analyzer createAdvancedAnalyzer();
-
-    @Named("simpleAnalyzer")
-    Analyzer createSimpleAnalyzer();
+    @Named("statisticsAnalyzer")
+    Analyzer createNewAnalyzer();
 }

+ 3 - 3
src/main/java/cz/senslog/analyzer/analysis/AnalyzerInfo.java

@@ -13,11 +13,11 @@ public class AnalyzerInfo {
 
     static class HandlerInfo {
 
-        private String name;
+        private final String name;
 
-        private Status status;
+        private final Status status;
 
-        private String configHash;
+        private final String configHash;
 
         public HandlerInfo(String name, Status status, String configHash) {
             this.name = name;

+ 24 - 49
src/main/java/cz/senslog/analyzer/analysis/AnalyzerModule.java

@@ -10,8 +10,11 @@ import cz.senslog.analyzer.core.api.HandlerInvoker;
 import cz.senslog.analyzer.domain.DoubleStatistics;
 import cz.senslog.analyzer.domain.Observation;
 
+import cz.senslog.analyzer.persistence.repository.StatisticsRepository;
 import dagger.Module;
 import dagger.Provides;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
 
 import javax.inject.Named;
 
@@ -26,63 +29,35 @@ import static cz.senslog.analyzer.core.api.HandlerInvoker.cancelInvoker;
 })
 public class AnalyzerModule {
 
-    @Provides @Named("advancedAnalyzer")
-    Analyzer provideAdvancedAnalyzer (
+    private static final Logger logger = LogManager.getLogger(AnalyzerModule.class);
+
+    @Provides @Named("statisticsAnalyzer")
+    Analyzer provideSimpleAnalyzer (
             @Named("sensorFilterHandler") FilterHandler<Observation> sensorFilterHandler,
-            @Named("sensorThresholdHandler") ThresholdHandler<Observation> thresholdHandler,
-            @Named("observationCollector") BlockingHandler<Observation, DoubleStatistics> observationCollector,
-            @Named("groupFilterHandler") FilterHandler<DoubleStatistics> groupFilterHandler,
+            @Named("sensorThresholdHandler") ThresholdHandler<Observation> sensorThresholdHandler,
+            @Named("aggregationCollectorHandler") BlockingHandler<Observation, DoubleStatistics> aggregateCollectorHandler,
             @Named("groupThresholdHandler") ThresholdHandler<DoubleStatistics> groupThresholdHandler,
-            @Named("statisticsCollector") BlockingHandler<DoubleStatistics, DoubleStatistics> statisticsCollector,
             EventBus eventBus
     ) {
-        HandlerInvoker<Observation> invoker = HandlerInvoker.create()
-                .handler(sensorFilterHandler)
-                .nextHandlerInvoker(HandlerInvoker.create()
-                        .handler(thresholdHandler)
-                        .nextHandlerInvoker(HandlerInvoker.create()
-                                .blockingHandler(observationCollector, eventBus::save)
-                                .nextHandlerInvoker(HandlerInvoker.create()
-                                        .handler(groupFilterHandler)
-                                        .nextHandlerInvoker(HandlerInvoker.create()
-                                                .handler(groupThresholdHandler)
-                                                .nextHandlerInvoker(HandlerInvoker.create()
-                                                        .blockingHandler(statisticsCollector, eventBus::save)
-                                                        .nextHandlerInvoker(cancelInvoker())
-                                                        .eventBus(eventBus)
-                                                        .build()
-                                                ).eventBus(eventBus)
-                                                .build()
-                                        ).eventBus(eventBus)
-                                        .build()
-                                ).eventBus(eventBus)
-                                .build()
-                        ).eventBus(eventBus)
-                        .build()
-                ).eventBus(eventBus)
-                .build();
+        // filter & group mapping ===>> check sensor value ====>> aggregate by groups ====>> check group statistics
 
-        return new ObservationAnalyzer(invoker);
-    }
+        HandlerInvoker<DoubleStatistics> groupThreshold = HandlerInvoker.create()
+                .handler(groupThresholdHandler).nextHandlerInvoker(cancelInvoker())
+                .eventBus(eventBus).build();
 
-    @Provides @Named("simpleAnalyzer")
-    Analyzer provideSimpleAnalyzer (
-            @Named("sensorFilterHandler") FilterHandler<Observation> sensorFilterHandler,
-            @Named("sensorThresholdHandler") ThresholdHandler<Observation> thresholdHandler,
-            @Named("observationCollector") BlockingHandler<Observation, DoubleStatistics> observationCollector,
-            EventBus eventBus
-    ) {
-        HandlerInvoker<Observation> obsColl = HandlerInvoker.create()
-                .blockingHandler(observationCollector, eventBus::save)
-                .nextHandlerInvoker(cancelInvoker()).eventBus(eventBus)
-                .build();
+        HandlerInvoker<Observation> aggregateCollector = HandlerInvoker.create()
+                .blockingHandler(aggregateCollectorHandler, eventBus::save)
+                .nextHandlerInvoker(groupThreshold)
+                .eventBus(eventBus).build();
 
-        HandlerInvoker<Observation> obsThs = HandlerInvoker.create().handler(thresholdHandler)
-                .nextHandlerInvoker(obsColl).eventBus(eventBus).build();
+        HandlerInvoker<Observation> sensorThreshold = HandlerInvoker.create()
+                .handler(sensorThresholdHandler).nextHandlerInvoker(aggregateCollector)
+                .eventBus(eventBus).build();
 
-        HandlerInvoker<Observation> obsFlt = HandlerInvoker.create().handler(sensorFilterHandler)
-                .nextHandlerInvoker(obsThs).eventBus(eventBus).build();
+        HandlerInvoker<Observation> sensorFilter = HandlerInvoker.create()
+                .handler(sensorFilterHandler).nextHandlerInvoker(sensorThreshold)
+                .eventBus(eventBus).build();
 
-        return new ObservationAnalyzer(obsFlt);
+        return new ObservationAnalyzer(sensorFilter);
     }
 }

+ 4 - 2
src/main/java/cz/senslog/analyzer/analysis/Checker.java

@@ -1,12 +1,14 @@
 package cz.senslog.analyzer.analysis;
 
+import cz.senslog.analyzer.domain.Threshold;
+
 import java.util.HashMap;
 import java.util.Map;
 import java.util.function.BiFunction;
 
 public class Checker {
 
-    private static Map<String, BiFunction<Double, Double, Boolean>> functions;
+    private static final Map<String, BiFunction<Double, Double, Boolean>> functions;
 
     static {
         functions = new HashMap<>(6);
@@ -23,7 +25,7 @@ public class Checker {
         return functions.getOrDefault(mode, (val, ths) -> false).apply(value, threshold);
     }
 
-    public static boolean checkThresholdValue(ThresholdRule rule, Double value) {
+    public static boolean checkThresholdValue(Threshold.Rule rule, Double value) {
         if (rule == null || value == null) return false;
         return checkThresholdValue(rule.getMode(), value, rule.getValue());
     }

+ 3 - 4
src/main/java/cz/senslog/analyzer/analysis/ObservationAnalyzer.java

@@ -1,6 +1,5 @@
 package cz.senslog.analyzer.analysis;
 
-
 import cz.senslog.analyzer.core.api.HandlerInvoker;
 import cz.senslog.analyzer.domain.Observation;
 import org.apache.logging.log4j.LogManager;
@@ -10,7 +9,7 @@ import java.util.List;
 
 public class ObservationAnalyzer implements Analyzer {
 
-    private static Logger logger = LogManager.getLogger(ObservationAnalyzer.class);
+    private static final Logger logger = LogManager.getLogger(ObservationAnalyzer.class);
 
     private final HandlerInvoker<Observation> invoker;
 
@@ -20,9 +19,9 @@ public class ObservationAnalyzer implements Analyzer {
 
     @Override
     public void accept(List<Observation> observations) {
-        logger.info("Processing new {} observations.", observations.size());
+        logger.info("Processing of the new {} observations.", observations.size());
         invoker.accept(observations);
-        logger.info("Processing of {} observations was finished.", observations.size());
+        logger.info("The process of processing new observations is finished.");
     }
 
     @Override

+ 0 - 35
src/main/java/cz/senslog/analyzer/analysis/ThresholdRule.java

@@ -1,35 +0,0 @@
-package cz.senslog.analyzer.analysis;
-
-public class ThresholdRule {
-
-    private final String mode;
-    private final String property;
-    private final Double value;
-
-    public ThresholdRule(String mode, String property, Double value) {
-        this.mode = mode;
-        this.property = property;
-        this.value = value;
-    }
-
-    public String getMode() {
-        return mode;
-    }
-
-    public String getProperty() {
-        return property;
-    }
-
-    public Double getValue() {
-        return value;
-    }
-
-    @Override
-    public String toString() {
-        return "ThresholdRule{" +
-                "mode='" + mode + '\'' +
-                ", property='" + property + '\'' +
-                ", value=" + value +
-                '}';
-    }
-}

+ 46 - 44
src/main/java/cz/senslog/analyzer/analysis/module/CollectorHandler.java

@@ -4,29 +4,36 @@ import cz.senslog.analyzer.core.api.BlockingHandler;
 import cz.senslog.analyzer.core.api.DataFinisher;
 import cz.senslog.analyzer.core.api.HandlerContext;
 import cz.senslog.analyzer.domain.*;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
 
 import java.util.*;
 import java.util.function.Consumer;
 
-import static java.util.Collections.emptyList;
+import static cz.senslog.common.json.BasicJson.objectToJson;
 import static java.util.Collections.singletonList;
 
-public abstract class CollectorHandler<I extends Data<?>> extends BlockingHandler<I, DoubleStatistics> {
+public abstract class CollectorHandler<I extends Data<?, ?>> extends BlockingHandler<I, DoubleStatistics> {
+
+    private static final Logger logger = LogManager.getLogger(CollectorHandler.class);
 
     private static class CollectedStatistics {
         private final Timestamp startTime;
         private final Timestamp endTime;
         private final DoubleStatistics summaryStatistics;
 
-        private CollectedStatistics(Sensor sensor, Timestamp startTime, Long interval) {
+        private CollectedStatistics(Group group, Timestamp startTime) {
             this.startTime = startTime;
-            this.endTime = Timestamp.of(startTime.get().plusSeconds(interval));
-            this.summaryStatistics = new DoubleStatistics(sensor, startTime, interval);
+            this.endTime = Timestamp.of(startTime.get().plusSeconds(group.getInterval()));
+            this.summaryStatistics = DoubleStatistics.init(group, startTime);
         }
     }
 
-    private Map<Long, List<Long>> groupsInterval;
-    private Map<Long, Map<Long, List<CollectedStatistics>>> collectedStatistics;
+    /** Map of saved statistics group by group_id and interval (Map<group_id, Statistics>).*/
+    private final Map<Group, List<CollectedStatistics>> collectedStatistics;
+
+    /** Map of saved groups (Map<group_id, Group>). */
+    private Map<Long, Group> groupsGroupById;
 
     public CollectorHandler() {
         this.collectedStatistics = new HashMap<>();
@@ -34,31 +41,27 @@ public abstract class CollectorHandler<I extends Data<?>> extends BlockingHandle
 
     protected abstract List<Group> loadGroups();
     protected abstract Consumer<I> collectData(DoubleStatistics statistics);
-
+    protected abstract long getGroupId(I data);
 
     @Override
     public void init() {
         List<Group> groups = loadGroups();
-        groupsInterval = new HashMap<>(groups.size());
+        groupsGroupById = new HashMap<>(groups.size());
         for (Group group : groups) {
-            groupsInterval.computeIfAbsent(group.getId(), k -> new ArrayList<>())
-                    .add(group.getInterval());
+            groupsGroupById.put(group.getId(), group);
         }
     }
 
     @Override
     public void finish(DataFinisher<DoubleStatistics> finisher, Timestamp edgeDateTime) {
-        Map<Long, Map<Long, List<CollectedStatistics>>> collectedStatistics = getCollectedStatistics();
         List<DoubleStatistics> finishedData = new ArrayList<>();
-        for (Map<Long, List<CollectedStatistics>> intervals : collectedStatistics.values()) {
-            for (List<CollectedStatistics> statistics : intervals.values()) {
-                Iterator<CollectedStatistics> statisticsIterator = statistics.iterator();
-                while (statisticsIterator.hasNext()) {
-                    CollectedStatistics statistic = statisticsIterator.next();
-                    if (statistic.endTime.isBefore(edgeDateTime)) {
-                        finishedData.add(statistic.summaryStatistics);
-                        statisticsIterator.remove();
-                    }
+        for (List<CollectedStatistics> statistics : collectedStatistics.values()) {
+            Iterator<CollectedStatistics> statisticsIterator = statistics.iterator();
+            while (statisticsIterator.hasNext()) {
+                CollectedStatistics statistic = statisticsIterator.next();
+                if (statistic.endTime.isBefore(edgeDateTime)) {
+                    finishedData.add(statistic.summaryStatistics);
+                    statisticsIterator.remove();
                 }
             }
         }
@@ -68,37 +71,36 @@ public abstract class CollectorHandler<I extends Data<?>> extends BlockingHandle
     @Override
     public void handle(HandlerContext<I, DoubleStatistics> context) {
         I data = context.data();
-        Sensor sensor = data.getSensor();
+        long groupId = getGroupId(context.data());
         Timestamp timestamp = data.getTimestamp();
+        Group group = getGroupByGroupId(groupId);
 
-        List<Long> intervals = groupsInterval.getOrDefault(sensor.getSensorId(), emptyList());
-        Map<Long, List<CollectedStatistics>> sensorStatistics = getCollectedStatistics()
-                .computeIfAbsent(sensor.getSensorId(), k -> new HashMap<>());
+        if (group.getInterval() <= 0) { return; }
 
-        for (Long interval : intervals) {
+        List<CollectedStatistics> groupStatistics = getCollectedStatisticsByGroup(group);
 
-            List<CollectedStatistics> statistics = sensorStatistics.computeIfAbsent(interval, k ->
-                            new ArrayList<>(singletonList(new CollectedStatistics(sensor, timestamp, interval))));
-
-            boolean founded = false; // TODO refactor
-            for (CollectedStatistics st : statistics) {
-                if (timestamp.isEqual(st.startTime) ||
-                        (timestamp.isAfter(st.startTime) && timestamp.isBefore(st.endTime))
-                ) {
-                    collectData(st.summaryStatistics).accept(data);
-                    founded = true;
-                }
-            }
-
-            if (!founded) {
-                CollectedStatistics st = new CollectedStatistics(sensor, timestamp, interval);
+        boolean newDataAccepted = false;
+        for (CollectedStatistics st : groupStatistics) {
+            if (timestamp.isEqual(st.startTime) ||
+                    (timestamp.isAfter(st.startTime) && timestamp.isBefore(st.endTime))
+            ) {
                 collectData(st.summaryStatistics).accept(data);
-                statistics.add(st);
+                newDataAccepted = true;
             }
         }
+
+        if (!newDataAccepted) {
+            CollectedStatistics st = new CollectedStatistics(group, timestamp);
+            collectData(st.summaryStatistics).accept(data);
+            groupStatistics.add(st);
+        }
+    }
+
+    private List<CollectedStatistics> getCollectedStatisticsByGroup(Group group) {
+        return collectedStatistics.computeIfAbsent(group, k -> new ArrayList<>());
     }
 
-    private Map<Long, Map<Long, List<CollectedStatistics>>> getCollectedStatistics() {
-        return collectedStatistics;
+    private Group getGroupByGroupId(long groupId) {
+        return groupsGroupById.getOrDefault(groupId, Group.empty());
     }
 }

+ 24 - 12
src/main/java/cz/senslog/analyzer/analysis/module/FilterHandler.java

@@ -3,34 +3,46 @@ package cz.senslog.analyzer.analysis.module;
 import cz.senslog.analyzer.core.api.Handler;
 import cz.senslog.analyzer.core.api.HandlerContext;
 import cz.senslog.analyzer.domain.Data;
-import cz.senslog.analyzer.domain.FilteredSensor;
 import cz.senslog.analyzer.domain.Sensor;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
 
 import java.util.*;
 
 import static java.util.Collections.emptySet;
 
-public abstract class FilterHandler<T extends Data<?>> extends Handler<T, T> {
+public abstract class FilterHandler<T extends Data<?, ?>> extends Handler<T, T> {
 
-    private Map<Long, Set<Long>> sensorMapping;
+    private static final Logger logger = LogManager.getLogger(FilterHandler.class);
 
-    protected abstract List<FilteredSensor> loadMapping();
+    /** Map of sensors and theirs groups where they are assigned (Map<Sensor, Set<group_is>>). */
+    private Map<Sensor, Set<Long>> sensorMapping;
+
+    protected abstract List<Sensor> loadMapping();
     protected abstract T newData(Sensor sensor, T data);
+    protected abstract Sensor getSensor(T data);
 
     @Override
     public void init() {
-        List<FilteredSensor> sensors = loadMapping();
+        List<Sensor> sensors = loadMapping();
         sensorMapping = new HashMap<>(sensors.size());
-        for (FilteredSensor sensor : sensors) {
-            sensorMapping.computeIfAbsent(sensor.getId(), k -> new HashSet<>())
-                    .add(sensor.getNewId());
+        for (Sensor sensor : sensors) {
+            sensorMapping.computeIfAbsent(sensor, k -> new HashSet<>())
+                    .add(sensor.getGroupId());
         }
     }
 
     @Override
     public void handle(HandlerContext<T, T> context) {
-        Sensor sensor = context.data().getSensor();
-        sensorMapping.getOrDefault(sensor.getSensorId(), emptySet()).forEach(newId ->
-                context.next(newData(new Sensor(newId), context.data())));
+        Sensor sensor = getSensor(context.data());
+        Set<Long> sensorGroupsIds = getGroupIdsBySensor(sensor);
+
+        for (long groupId : sensorGroupsIds) {
+            context.next(newData(new Sensor(sensor, groupId), context.data()));
+        }
+    }
+
+    private Set<Long> getGroupIdsBySensor(Sensor sensor) {
+        return sensorMapping.getOrDefault(sensor, emptySet());
     }
-}
+}

+ 19 - 41
src/main/java/cz/senslog/analyzer/analysis/module/HandlersModule.java

@@ -6,81 +6,59 @@ import cz.senslog.analyzer.persistence.RepositoryModule;
 import cz.senslog.analyzer.persistence.repository.StatisticsConfigRepository;
 import dagger.Module;
 import dagger.Provides;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
 
 import javax.inject.Named;
 import javax.inject.Singleton;
 import java.util.*;
 import java.util.function.Consumer;
 
-import static cz.senslog.analyzer.app.Constants.MINIMAL_INTERVAL;
-import static java.util.stream.Collectors.toList;
-
 @Module(includes = RepositoryModule.class)
 public class HandlersModule {
 
+    private static final Logger logger = LogManager.getLogger(HandlersModule.class);
+
     @Provides @Singleton @Named("sensorFilterHandler")
     public FilterHandler<Observation> provideFilterHandler(StatisticsConfigRepository repository) {
+        logger.info("Creating a new instance for the handler '{}'.", "sensorFilterHandler");
         return new FilterHandler<Observation>() {
-            @Override protected List<FilteredSensor> loadMapping() { return repository.getAllAvailableSensors(); }
+            @Override protected List<Sensor> loadMapping() { return repository.getAllAvailableSensors(); }
             @Override protected Observation newData(Sensor sensor, Observation data) {
-                return new Observation(sensor, data.getValue(), data.getTimestamp());
-            }
-        };
-    }
-    
-    @Provides @Singleton @Named("groupFilterHandler")
-    public FilterHandler<DoubleStatistics> provideGroupFilterHandler(StatisticsConfigRepository repository) {
-        return new FilterHandler<DoubleStatistics>() {
-            @Override protected List<FilteredSensor> loadMapping() {
-                return repository.getAllGroupsMapping();
-            }
-            @Override protected DoubleStatistics newData(Sensor sensor, DoubleStatistics data) {
-                return new DoubleStatistics(sensor, data.getValue(), data.getTimestamp(), data.getInterval());
-            }
+                return new Observation(sensor, data.getValue(), data.getTimestamp()); }
+            @Override protected Sensor getSensor(Observation data) { return data.getSource(); }
         };
     }
 
     @Provides @Singleton @Named("sensorThresholdHandler")
     public ThresholdHandler<Observation> provideSensorThresholdHandler(StatisticsConfigRepository repository) {
+        logger.info("Creating a new instance for the handler '{}'.", "sensorThresholdHandler");
         return new ThresholdHandler<Observation>() {
-            @Override protected List<Threshold> loadThresholdValues() {
-                return repository.getCurrentThresholdsValue();
-            }
+            @Override protected List<Threshold> loadThresholdValues() { return repository.getCurrentThresholdsValue(); }
+            @Override protected long getGroupId(Observation data) { return data.getSource().getGroupId(); }
         };
     }
 
     @Provides @Singleton @Named("groupThresholdHandler")
     public ThresholdHandler<DoubleStatistics> provideGroupThresholdHandler(StatisticsConfigRepository repository) {
+        logger.info("Creating a new instance for the handler '{}'.", "groupThresholdHandler");
         return new ThresholdHandler<DoubleStatistics>() {
-            @Override protected List<Threshold> loadThresholdValues() {
-                return repository.getIntervalThresholdsValue();
-            }
+            @Override protected List<Threshold> loadThresholdValues() { return repository.getIntervalThresholdsValue(); }
+            @Override protected long getGroupId(DoubleStatistics data) { return data.getSource().getId(); }
         };
     }
 
-    @Provides @Singleton @Named("observationCollector")
+    @Provides @Singleton @Named("aggregationCollectorHandler")
     public BlockingHandler<Observation, DoubleStatistics> provideObservationCollector(StatisticsConfigRepository repository) {
+        logger.info("Creating a new instance for the handler '{}'.", "aggregationCollectorHandler");
         return new CollectorHandler<Observation>() {
             @Override protected List<Group> loadGroups() {
-                return repository.getAllAvailableSensors().stream()
-                        .map(s -> new Group(s.getId(), MINIMAL_INTERVAL))
-                        .collect(toList());
+                return repository.getGroupInfos();
             }
             @Override protected Consumer<Observation> collectData(DoubleStatistics statistics) {
-                return val -> statistics.accept(val.getValue());
-            }
-        };
-    }
-
-    @Provides @Singleton @Named("statisticsCollector")
-    public BlockingHandler<DoubleStatistics, DoubleStatistics> provideStatisticsCollector(StatisticsConfigRepository repository) {
-        return new CollectorHandler<DoubleStatistics>() {
-            @Override protected List<Group> loadGroups() {
-                return repository.getAllIntervalGroups();
-            }
-            @Override protected Consumer<DoubleStatistics> collectData(DoubleStatistics statistics) {
-                return val -> statistics.accept(val.getValue());
+                return val -> statistics.accept(val.getSource(), val.getValue());
             }
+            @Override protected long getGroupId(Observation data) { return data.getSource().getGroupId(); }
         };
     }
 }

+ 20 - 15
src/main/java/cz/senslog/analyzer/analysis/module/ThresholdHandler.java

@@ -1,47 +1,52 @@
 package cz.senslog.analyzer.analysis.module;
 
-import cz.senslog.analyzer.analysis.*;
-import cz.senslog.analyzer.domain.Alert;
+import cz.senslog.analyzer.domain.*;
 import cz.senslog.analyzer.core.api.Handler;
 import cz.senslog.analyzer.core.api.HandlerContext;
-import cz.senslog.analyzer.domain.Data;
-import cz.senslog.analyzer.domain.Sensor;
-import cz.senslog.analyzer.domain.Threshold;
-import cz.senslog.analyzer.domain.ValidationResult;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
 
 import java.util.*;
 
 import static java.util.Collections.emptyList;
 
-public abstract class ThresholdHandler<T extends Data<?>> extends Handler<T, T> {
+public abstract class ThresholdHandler<T extends Data<?, ?>> extends Handler<T, T> {
 
-    private Map<Long, List<ThresholdRule>> thresholdRules;
+    private static final Logger logger = LogManager.getLogger(ThresholdHandler.class);
+
+    /** Map of thresholds rules order by group_id (Map<group_id, List<ThresholdRule>>). */
+    private Map<Long, List<Threshold.Rule>> thresholdRules;
 
     protected abstract List<Threshold> loadThresholdValues();
+    protected abstract long getGroupId(T data);
 
     @Override
     public void init() {
         List<Threshold> thresholds = loadThresholdValues();
         thresholdRules = new HashMap<>(thresholds.size());
         for (Threshold threshold : thresholds) {
-            thresholdRules.computeIfAbsent(threshold.getId(), k -> new ArrayList<>())
-                    .add(new ThresholdRule(threshold.getMode(), threshold.getProperty(), threshold.getValue()));
+            thresholdRules.computeIfAbsent(threshold.getGroupId(), k -> new ArrayList<>())
+                    .add(threshold.getRule());
         }
     }
 
     @Override
     public void handle(HandlerContext<T, T> context) {
         T data = context.data();
-        Sensor sensor = data.getSensor();
-        List<ThresholdRule> rules = thresholdRules.getOrDefault(sensor.getSensorId(), emptyList());
+        long groupId = getGroupId(context.data());
+        List<Threshold.Rule> rules = getThresholdRulesByGroupId(groupId);
         ValidationResult validateResult = data.validate(rules);
 
         if (validateResult.isNotValid()) {
-            context.eventBus().send(Alert
-                    .create(String.format("Data %s does not match the rules:\n %s", data, validateResult.getMessages()))
-            );
+            String message = String.format("Group(%s) failed at %s in %s.",
+                    groupId, data.getTimestamp(), validateResult.getMessages());
+            context.eventBus().send(Alert.create(message));
         }
 
         context.next(data);
     }
+
+    private List<Threshold.Rule> getThresholdRulesByGroupId(long groupId) {
+        return thresholdRules.getOrDefault(groupId, emptyList());
+    }
 }

+ 2 - 2
src/main/java/cz/senslog/analyzer/app/Application.java

@@ -29,7 +29,7 @@ public class Application extends Thread {
     private static final long START_TIMESTAMP;
     private static final RuntimeMXBean RUNTIME_MX_BEAN;
 
-    private static Logger logger = LogManager.getLogger(Application.class);
+    private static final Logger logger = LogManager.getLogger(Application.class);
 
     private final Parameters params;
 
@@ -88,7 +88,7 @@ public class Application extends Thread {
 
         Analyzer analyzer = DaggerAnalyzerComponent.builder()
                 .connectionModule(connectionModule)
-                .build().createSimpleAnalyzer();
+                .build().createNewAnalyzer();
 
         DataProvider dataProvider = DaggerDataProviderComponent.builder()
                 .connectionModule(connectionModule).build()

+ 5 - 1
src/main/java/cz/senslog/analyzer/app/Configuration.java

@@ -15,10 +15,13 @@ import java.nio.file.Files;
 import java.nio.file.Path;
 import java.nio.file.Paths;
 import java.time.LocalDateTime;
+import java.time.OffsetDateTime;
+import java.time.format.DateTimeFormatter;
 import java.util.Date;
 import java.util.Map;
 
 import static java.time.ZoneOffset.UTC;
+import static java.time.format.DateTimeFormatter.ISO_OFFSET_DATE_TIME;
 
 public class Configuration {
     private static Logger logger = LogManager.getLogger(Configuration.class);
@@ -81,7 +84,8 @@ public class Configuration {
         );
 
         Map<String, Object> schedulerConfigMap = (Map<String, Object>)schedulerMap;
-        LocalDateTime initDate = ((Date)schedulerConfigMap.get("initDate")).toInstant().atZone(UTC).toLocalDateTime();
+        String initDateString = (String) schedulerConfigMap.get("initDate");
+        OffsetDateTime initDate = initDateString != null ? OffsetDateTime.parse(initDateString) : OffsetDateTime.now();
         ProviderConfig providerConfig = ProviderConfig.config()
                 .startDateTime(initDate)
                 .period((Integer)schedulerConfigMap.get("period"))

+ 0 - 6
src/main/java/cz/senslog/analyzer/app/Constants.java

@@ -1,6 +0,0 @@
-package cz.senslog.analyzer.app;
-
-public class Constants {
-
-    public static final long MINIMAL_INTERVAL = 60;
-}

+ 2 - 2
src/main/java/cz/senslog/analyzer/core/EventBusModule.java

@@ -3,7 +3,7 @@ package cz.senslog.analyzer.core;
 import cz.senslog.analyzer.domain.Alert;
 import cz.senslog.analyzer.domain.DoubleStatistics;
 import cz.senslog.analyzer.persistence.RepositoryModule;
-import cz.senslog.analyzer.persistence.repository.SenslogRepository;
+import cz.senslog.analyzer.persistence.repository.SensLogRepository;
 import cz.senslog.analyzer.persistence.repository.StatisticsRepository;
 import dagger.Module;
 import dagger.Provides;
@@ -17,7 +17,7 @@ public class EventBusModule {
     @Provides @Singleton
     public EventBus provideEventBus(
             StatisticsRepository statisticsRepository,
-            SenslogRepository senslogRepository
+            SensLogRepository senslogRepository
     ) {
         return new EventBus() {
 

+ 2 - 2
src/main/java/cz/senslog/analyzer/core/api/BlockingHandler.java

@@ -3,7 +3,7 @@ package cz.senslog.analyzer.core.api;
 import cz.senslog.analyzer.domain.Data;
 import cz.senslog.analyzer.domain.Timestamp;
 
-public abstract class BlockingHandler<T extends Data<?>, N extends Data<?>> extends HandlerRunnable<T, N> {
+public abstract class BlockingHandler<T extends Data<?, ?>, N extends Data<?, ?>> extends HandlerRunnable<T, N> {
 
     private DataFinisher<N> finisher;
 
@@ -24,4 +24,4 @@ public abstract class BlockingHandler<T extends Data<?>, N extends Data<?>> exte
             handle(context);
         }
     }
-}
+}

+ 2 - 2
src/main/java/cz/senslog/analyzer/core/api/Builder.java

@@ -4,7 +4,7 @@ import cz.senslog.analyzer.core.EventBus;
 import cz.senslog.analyzer.domain.Data;
 
 public interface Builder {
-    <T extends Data<?>> HandlerInvoker<T> build();
+    <T extends Data<?, ?>> HandlerInvoker<T> build();
 
     class BuilderImpl implements Builder {
 
@@ -19,7 +19,7 @@ public interface Builder {
         }
 
         @Override
-        public <T extends Data<?>> HandlerInvoker<T> build() {
+        public <T extends Data<?, ?>> HandlerInvoker<T> build() {
             return new HandlerInvoker<>(new HandlerContext(handler, invoker, eventBus));
         }
     }

+ 4 - 4
src/main/java/cz/senslog/analyzer/core/api/ContextBuilder.java

@@ -4,20 +4,20 @@ import cz.senslog.analyzer.domain.Data;
 
 public interface ContextBuilder {
 
-    <A extends Data<?>, B extends Data<?>> NextInvokerBuilder<B> handler(Handler<A, B> handler);
+    <A extends Data<?, ?>, B extends Data<?, ?>> NextInvokerBuilder<B> handler(Handler<A, B> handler);
 
-    <A extends Data<?>, B extends Data<?>> NextInvokerBuilder<B> blockingHandler(BlockingHandler<A, B> handler, DataFinisher<B> finisher);
+    <A extends Data<?, ?>, B extends Data<?, ?>> NextInvokerBuilder<B> blockingHandler(BlockingHandler<A, B> handler, DataFinisher<B> finisher);
 
     class ContextBuilderImpl implements ContextBuilder {
 
         @Override
-        public <A extends Data<?>, B extends Data<?>> NextInvokerBuilder<B> handler(Handler<A, B> handler) {
+        public <A extends Data<?, ?>, B extends Data<?, ?>> NextInvokerBuilder<B> handler(Handler<A, B> handler) {
             handler.init();
             return new NextInvokerBuilder.NextInvokerBuilderImpl<>(handler);
         }
 
         @Override
-        public <A extends Data<?>, B extends Data<?>> NextInvokerBuilder<B> blockingHandler(BlockingHandler<A, B> handler, DataFinisher<B> finisher) {
+        public <A extends Data<?, ?>, B extends Data<?, ?>> NextInvokerBuilder<B> blockingHandler(BlockingHandler<A, B> handler, DataFinisher<B> finisher) {
             handler.init(finisher);
             return new NextInvokerBuilder.NextInvokerBuilderImpl<>(handler);
         }

+ 1 - 1
src/main/java/cz/senslog/analyzer/core/api/DataFinisher.java

@@ -5,6 +5,6 @@ import cz.senslog.analyzer.domain.Data;
 import java.util.List;
 
 @FunctionalInterface
-public interface DataFinisher<T extends Data<?>> {
+public interface DataFinisher<T extends Data<?, ?>> {
     void finish(List<T> data);
 }

+ 1 - 1
src/main/java/cz/senslog/analyzer/core/api/Handler.java

@@ -2,7 +2,7 @@ package cz.senslog.analyzer.core.api;
 
 import cz.senslog.analyzer.domain.Data;
 
-public abstract class Handler<T extends Data<?>, N extends Data<?>> extends HandlerRunnable<T, N> {
+public abstract class Handler<T extends Data<?, ?>, N extends Data<?, ?>> extends HandlerRunnable<T, N> {
 
     public abstract void init();
     public abstract void handle(HandlerContext<T, N> context);

+ 11 - 4
src/main/java/cz/senslog/analyzer/core/api/HandlerContext.java

@@ -2,12 +2,18 @@ package cz.senslog.analyzer.core.api;
 
 import cz.senslog.analyzer.core.EventBus;
 import cz.senslog.analyzer.domain.Data;
+import cz.senslog.analyzer.domain.Threshold;
 import cz.senslog.analyzer.domain.Timestamp;
+import cz.senslog.analyzer.domain.ValidationResult;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
 
 import java.util.*;
 
 
-public class HandlerContext<D extends Data<?>, N extends Data<?>> {
+public class HandlerContext<D extends Data<?, ?>, N extends Data<?, ?>> {
+
+    private static final Logger logger = LogManager.getLogger(HandlerContext.class);
 
     private Queue<D> inputQueue;
     private List<N> collectedList;
@@ -21,7 +27,6 @@ public class HandlerContext<D extends Data<?>, N extends Data<?>> {
         this.handler = handler;
         this.nextInvoker = nextInvoker;
         this.eventBus = eventBus;
-        this.collectedList = new ArrayList<>();
     }
 
     public D data() {
@@ -48,8 +53,9 @@ public class HandlerContext<D extends Data<?>, N extends Data<?>> {
         Timestamp timestamp = dataList.isEmpty() ? Timestamp.now() : dataList.get(dataList.size() - 1).getTimestamp();
 
         inputQueue = new LinkedList<>(dataList);
-        dataList = Collections.emptyList();
+        collectedList = new ArrayList<>(dataList.size());
         inputQueue.add(D.empty(timestamp));
+        dataList = Collections.emptyList();
 
         do {
             handler.run(this);
@@ -57,9 +63,10 @@ public class HandlerContext<D extends Data<?>, N extends Data<?>> {
         } while (!inputQueue.isEmpty());
 
         nextInvoker.accept(collectedList);
+        collectedList = Collections.emptyList();
     }
 
     public EventBus eventBus() {
         return eventBus;
     }
-}
+}

+ 6 - 5
src/main/java/cz/senslog/analyzer/core/api/HandlerInvoker.java

@@ -3,19 +3,20 @@ package cz.senslog.analyzer.core.api;
 
 import cz.senslog.analyzer.domain.Data;
 
+import java.util.ArrayList;
 import java.util.List;
 
-public class HandlerInvoker<T extends Data<?>> {
+public class HandlerInvoker<T extends Data<?, ?>> {
 
-    public static <T extends Data<?>> HandlerInvoker<T> cancelInvoker() {
+    public static <T extends Data<?, ?>> HandlerInvoker<T> cancelInvoker() {
         return new HandlerInvoker<T>(new HandlerContext(null, null, null) {
             @Override final protected void accept(List dataList) { }
         });
     }
 
-    private final HandlerContext<T, ? extends Data<?>> handlerContext;
+    private final HandlerContext<T, ? extends Data<?, ?>> handlerContext;
 
-    protected HandlerInvoker(HandlerContext<T, ? extends Data<?>> handlerContext) {
+    protected HandlerInvoker(HandlerContext<T, ? extends Data<?, ?>> handlerContext) {
         this.handlerContext = handlerContext;
     }
 
@@ -24,6 +25,6 @@ public class HandlerInvoker<T extends Data<?>> {
     }
 
     public void accept(List<T> observations) {
-        handlerContext.accept(observations);
+        handlerContext.accept(new ArrayList<>(observations));
     }
 }

+ 1 - 1
src/main/java/cz/senslog/analyzer/core/api/HandlerRunnable.java

@@ -2,7 +2,7 @@ package cz.senslog.analyzer.core.api;
 
 import cz.senslog.analyzer.domain.Data;
 
-public abstract class HandlerRunnable<D extends Data<?>, N extends Data<?>> {
+public abstract class HandlerRunnable<D extends Data<?, ?>, N extends Data<?, ?>> {
 
     protected abstract void run(HandlerContext<D, N> context);
 }

+ 2 - 2
src/main/java/cz/senslog/analyzer/core/api/NextInvokerBuilder.java

@@ -3,11 +3,11 @@ package cz.senslog.analyzer.core.api;
 
 import cz.senslog.analyzer.domain.Data;
 
-public interface NextInvokerBuilder<T extends Data<?>> {
+public interface NextInvokerBuilder<T extends Data<?, ?>> {
 
     ContextBusBuilder nextHandlerInvoker(HandlerInvoker<T> handlerInvoker);
 
-    class NextInvokerBuilderImpl<T extends Data<?>> implements NextInvokerBuilder<T> {
+    class NextInvokerBuilderImpl<T extends Data<?, ?>> implements NextInvokerBuilder<T> {
 
         private final HandlerRunnable<?, T> handler;
 

+ 7 - 0
src/main/java/cz/senslog/analyzer/domain/AggregationType.java

@@ -0,0 +1,7 @@
+package cz.senslog.analyzer.domain;
+
+public enum  AggregationType {
+    DOUBLE,
+
+    ;
+}

+ 13 - 0
src/main/java/cz/senslog/analyzer/domain/AttributeValue.java

@@ -0,0 +1,13 @@
+package cz.senslog.analyzer.domain;
+
+public enum AttributeValue {
+
+    MIN,
+    MAX,
+    SUM,
+    COUNT,
+    AVG,
+    VAL,
+
+    ;
+}

+ 19 - 21
src/main/java/cz/senslog/analyzer/domain/Data.java

@@ -1,49 +1,46 @@
 package cz.senslog.analyzer.domain;
 
-import cz.senslog.analyzer.analysis.ThresholdRule;
-
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
+import java.util.*;
 import java.util.function.Supplier;
 
 import static cz.senslog.analyzer.analysis.Checker.checkThresholdValue;
 
-public abstract class Data<T> {
+public abstract class Data<S, V> {
     private final Map<String, Supplier<Double>> mapping;
 
-    private final Sensor sensor;
+    private final S source;
     private final Timestamp timestamp;
 
-    public static <T extends Data<?>> T empty(Timestamp timestamp) {
-        return (T) new Data<T>(new Sensor(-1L), timestamp) {
-            @Override public T getValue() { return null;}
-            @Override public ValidationResult validate(List<ThresholdRule> rules) { return new ValidationResult(); }
+    @SuppressWarnings("unchecked")
+    public static <T extends Data<?, ?>> T empty(Timestamp timestamp) {
+        return (T) new Data<Object, Object>(null, timestamp) {
+            @Override public Object getValue() { return null; }
+            @Override public ValidationResult validate(List<Threshold.Rule> rules) {return new ValidationResult();}
             @Override public String toString() { return "Data.empty"; }
         };
     }
 
-    protected Data(Sensor sensor, Timestamp timestamp) {
-        this.sensor = sensor;
+    protected Data(S source, Timestamp timestamp) {
+        this.source = source;
         this.timestamp = timestamp;
         this.mapping = new HashMap<>();
     }
 
-    public abstract T getValue();
+    public abstract V getValue();
 
-    final protected void addMapping(String property, Supplier<Double> getter) {
-        mapping.put(property, getter);
+    final protected void addMapping(AttributeValue property, Supplier<Double> getter) {
+        mapping.put(property.name(), getter);
     }
 
-    public ValidationResult validate(List<ThresholdRule> rules) {
+    public ValidationResult validate(List<Threshold.Rule> rules) {
         if (rules == null || rules.isEmpty()) { return new ValidationResult(); }
 
         ValidationResult result = new ValidationResult();
-        for (ThresholdRule rule : rules) {
+        for (Threshold.Rule rule : rules) {
             Double value = mapping.getOrDefault(rule.getProperty(), () -> null).get();
             if (checkThresholdValue(rule, value)) {
                 result.addMessage(String
-                        .format("%s %s %s", value, rule.getMode(), rule.getValue())
+                        .format("%s(%s) %s %s", rule.getProperty(), value, rule.getMode(), rule.getValue())
                 );
             }
         }
@@ -51,8 +48,9 @@ public abstract class Data<T> {
         return result;
     }
 
-    public Sensor getSensor() {
-        return sensor;
+
+    public S getSource() {
+        return source;
     }
 
     public Timestamp getTimestamp() {

+ 45 - 32
src/main/java/cz/senslog/analyzer/domain/DoubleStatistics.java

@@ -6,7 +6,7 @@ import java.util.stream.DoubleStream;
 import static cz.senslog.common.json.BasicJson.objectToJson;
 
 
-public class DoubleStatistics extends Data<DoubleStatistics> {
+public class DoubleStatistics extends Data<Group, DoubleStatistics> {
 
     private long count;
     private double sum;
@@ -14,20 +14,22 @@ public class DoubleStatistics extends Data<DoubleStatistics> {
     private double simpleSum;
     private double min = 1.0D / 0.0;
     private double max = -1.0D / 0.0;
-    private final long interval;
 
-    public DoubleStatistics(Sensor sensor, Timestamp timestamp, long interval) {
-        super(sensor, timestamp);
-        this.interval = interval;
+    public static DoubleStatistics init(Group group, Timestamp timestamp) {
+        return new DoubleStatistics(group, timestamp);
     }
 
-    public DoubleStatistics(Sensor sensor, DoubleStatistics statistics, Timestamp timestamp, long interval) {
-        this(sensor, statistics.count, statistics.min, statistics.max, statistics.sum, timestamp, interval);
+    private DoubleStatistics(Group group, Timestamp timestamp) {
+        super(group, timestamp);
+        initMapping();
     }
 
-    public DoubleStatistics(Sensor sensor, long count, double min, double max, double sum, Timestamp timestamp, long interval) {
-        super(sensor, timestamp);
-        this.interval = interval;
+    public DoubleStatistics(Group group, DoubleStatistics statistics, Timestamp timestamp) {
+        this(group, statistics.count, statistics.min, statistics.max, statistics.sum, timestamp);
+    }
+
+    public DoubleStatistics(Group group, long count, double min, double max, double sum, Timestamp timestamp) {
+        super(group, timestamp);
         if (count < 0L) {
             throw new IllegalArgumentException("Negative count value");
         } else {
@@ -49,10 +51,13 @@ public class DoubleStatistics extends Data<DoubleStatistics> {
                 this.max = max;
             }
         }
+        initMapping();
+    }
 
-        addMapping("min", this::getMin);
-        addMapping("max", this::getMax);
-        addMapping("avg", this::getAverage);
+    private void initMapping() {
+        addMapping(AttributeValue.MIN, this::getMin);
+        addMapping(AttributeValue.MAX, this::getMax);
+        addMapping(AttributeValue.AVG, this::getAverage);
     }
 
     private void sumWithCompensation(double value) {
@@ -83,30 +88,38 @@ public class DoubleStatistics extends Data<DoubleStatistics> {
         return this.getCount() > 0L ? this.getSum() / (double)this.getCount() : 0.0D;
     }
 
-    public long getInterval() {
-        return interval;
-    }
-
     @Override
     public DoubleStatistics getValue() {
         return this;
     }
 
-    public void accept(DoubleStatistics other) {
-        this.count += other.count;
-        this.simpleSum += other.simpleSum;
-        this.sumWithCompensation(other.sum);
-        this.sumWithCompensation(other.sumCompensation);
-        this.min = Math.min(this.min, other.min);
-        this.max = Math.max(this.max, other.max);
+    private boolean acceptSensor(Sensor sensor) {
+        return getSource().getSensors().contains(sensor);
     }
 
-    public void accept(double value) {
-        ++this.count;
-        this.simpleSum += value;
-        this.sumWithCompensation(value);
-        this.min = Math.min(this.min, value);
-        this.max = Math.max(this.max, value);
+    public boolean accept(Sensor sensor, DoubleStatistics other) {
+        if (acceptSensor(sensor)) {
+            this.count += other.count;
+            this.simpleSum += other.simpleSum;
+            this.sumWithCompensation(other.sum);
+            this.sumWithCompensation(other.sumCompensation);
+            this.min = Math.min(this.min, other.min);
+            this.max = Math.max(this.max, other.max);
+            return true;
+        }
+        return false;
+    }
+
+    public boolean accept(Sensor sensor, double value) {
+        if (acceptSensor(sensor)) {
+            ++this.count;
+            this.simpleSum += value;
+            this.sumWithCompensation(value);
+            this.min = Math.min(this.min, value);
+            this.max = Math.max(this.max, value);
+            return true;
+        }
+        return false;
     }
 
     @Override
@@ -114,7 +127,7 @@ public class DoubleStatistics extends Data<DoubleStatistics> {
         if (this == o) return true;
         if (o == null || getClass() != o.getClass()) return false;
         DoubleStatistics that = (DoubleStatistics) o;
-        return getSensor().equals(that.getSensor()) &&
+        return getSource().equals(that.getSource()) &&
                 getMin() == that.getMin() &&
                 getMax() == that.getMax() &&
                 getSum() == that.getSum() &&
@@ -124,7 +137,7 @@ public class DoubleStatistics extends Data<DoubleStatistics> {
 
     @Override
     public int hashCode() {
-        return Objects.hash(getSensor(), getMin(), getMax(), getAverage(), getSum(),getCount());
+        return Objects.hash(getSource(), getMin(), getMax(), getAverage(), getSum(),getCount());
     }
 
     @Override

+ 0 - 22
src/main/java/cz/senslog/analyzer/domain/FilteredSensor.java

@@ -1,22 +0,0 @@
-package cz.senslog.analyzer.domain;
-
-
-public class FilteredSensor {
-
-    private final Long id;
-
-    private final Long newId;
-
-    public FilteredSensor(Long id, Long newId) {
-        this.id = id;
-        this.newId = newId;
-    }
-
-    public Long getId() {
-        return id;
-    }
-
-    public Long getNewId() {
-        return newId;
-    }
-}

+ 54 - 5
src/main/java/cz/senslog/analyzer/domain/Group.java

@@ -1,21 +1,70 @@
 package cz.senslog.analyzer.domain;
 
+import java.util.Objects;
+import java.util.Set;
+
+import static cz.senslog.common.json.BasicJson.objectToJson;
+import static java.util.Collections.emptySet;
+
 public class Group {
 
-    private final Long id;
+    private final long id;
+    private final long interval;
+    private final boolean persistence;
+    private final AggregationType aggregationType;
+    private final Set<Sensor> sensors;
 
-    private final Long interval;
+    public static Group empty() {
+        return new Group(-1, 0, false, null, emptySet());
+    }
 
-    public Group(Long id, Long interval) {
+    public Group(long id, long interval, boolean persistence, AggregationType aggregationType, Set<Sensor> sensors) {
         this.id = id;
         this.interval = interval;
+        this.persistence = persistence;
+        this.aggregationType = aggregationType;
+        this.sensors = sensors;
+    }
+
+    public Group(Group group, Set<Sensor> sensors) {
+        this(group.id, group.interval, group.persistence, group.aggregationType, sensors);
     }
 
-    public Long getId() {
+    public long getId() {
         return id;
     }
 
-    public Long getInterval() {
+    public long getInterval() {
         return interval;
     }
+
+    public boolean isPersistence() {
+        return persistence;
+    }
+
+    public AggregationType getAggregationType() {
+        return aggregationType;
+    }
+
+    public Set<Sensor> getSensors() {
+        return sensors;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+        Group group = (Group) o;
+        return id == group.id;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(id);
+    }
+
+    @Override
+    public String toString() {
+        return objectToJson(this);
+    }
 }

+ 1 - 1
src/main/java/cz/senslog/analyzer/domain/GroupBy.java

@@ -1,7 +1,7 @@
 package cz.senslog.analyzer.domain;
 
 public enum  GroupBy {
-    HOUR, DAY, WEEK, MONTH, YEAR;
+    DAY, MONTH, YEAR;
 
     public static GroupBy parse(String groupBy) {
         return valueOf(groupBy);

+ 4 - 5
src/main/java/cz/senslog/analyzer/domain/Observation.java

@@ -1,17 +1,16 @@
 package cz.senslog.analyzer.domain;
 
-
 import static cz.senslog.common.json.BasicJson.objectToJson;
 
-public class Observation extends Data<Double> {
+public class Observation extends Data<Sensor, Double> {
 
-    private final Double value;
+    private final double value;
 
-    public Observation(Sensor sensor, Double value, Timestamp timestamp) {
+    public Observation(Sensor sensor, double value, Timestamp timestamp) {
         super(sensor, timestamp);
         this.value = value;
 
-        addMapping("val", this::getValue);
+        addMapping(AttributeValue.VAL, this::getValue);
     }
 
     public Double getValue() {

+ 31 - 13
src/main/java/cz/senslog/analyzer/domain/Sensor.java

@@ -1,37 +1,55 @@
 package cz.senslog.analyzer.domain;
 
-import java.util.Objects;
 
-import static cz.senslog.common.json.BasicJson.objectToJson;
+import java.util.Objects;
 
 public class Sensor {
 
-    private final Long sensorId;
+    private final long unitId;
+    private final long sensorId;
+    private final long groupId;
+
+    public static Sensor empty() {
+        return new Sensor(-1, -1, -1);
+    }
+
+    public Sensor(long unitId, long  sensorId) {
+        this(unitId, sensorId, -1);
+    }
+
+    public Sensor(Sensor sensor, long groupId) {
+        this(sensor.getUnitId(), sensor.getSensorId(), groupId);
+    }
 
-    public Sensor(Long sensorId) {
+    public Sensor(long unitId, long  sensorId, long groupId) {
+        this.unitId = unitId;
         this.sensorId = sensorId;
+        this.groupId = groupId;
     }
 
-    public Long getSensorId() {
+    public long getSensorId() {
         return sensorId;
     }
 
+    public long getUnitId() {
+        return unitId;
+    }
+
+    public long getGroupId() {
+        return groupId;
+    }
 
     @Override
     public boolean equals(Object o) {
         if (this == o) return true;
         if (o == null || getClass() != o.getClass()) return false;
-        Sensor that = (Sensor) o;
-        return sensorId.equals(that.sensorId);
+        Sensor sensor = (Sensor) o;
+        return unitId == sensor.unitId &&
+                sensorId == sensor.sensorId;
     }
 
     @Override
     public int hashCode() {
-        return Objects.hash(sensorId);
-    }
-
-    @Override
-    public String toString() {
-        return objectToJson(this);
+        return Objects.hash(unitId, sensorId);
     }
 }

+ 38 - 18
src/main/java/cz/senslog/analyzer/domain/Threshold.java

@@ -1,32 +1,52 @@
 package cz.senslog.analyzer.domain;
 
+import static cz.senslog.common.json.BasicJson.objectToJson;
+
 public class Threshold {
 
-    private final Long id;
-    private final String property;
-    private final String mode;
-    private final Double value;
+    public static class Rule {
 
-    public Threshold(Long id, String property, String mode, Double value) {
-        this.id = id;
-        this.property = property;
-        this.mode = mode;
-        this.value = value;
-    }
+        private final String mode;
+        private final String property;
+        private final Double value;
+
+        public Rule(String mode, String property, Double value) {
+            this.mode = mode;
+            this.property = property;
+            this.value = value;
+        }
+
+        public String getMode() {
+            return mode;
+        }
 
-    public Long getId() {
-        return id;
+        public String getProperty() {
+            return property;
+        }
+
+        public Double getValue() {
+            return value;
+        }
+
+        @Override
+        public String toString() {
+            return objectToJson(this);
+        }
     }
 
-    public String getProperty() {
-        return property;
+    private final long groupId;
+    private final Rule rule;
+
+    public Threshold(long groupId, String property, String mode, Double value) {
+        this.groupId = groupId;
+        this.rule = new Rule(mode,property, value);
     }
 
-    public String getMode() {
-        return mode;
+    public Long getGroupId() {
+        return groupId;
     }
 
-    public Double getValue() {
-        return value;
+    public Rule getRule() {
+        return rule;
     }
 }

+ 9 - 9
src/main/java/cz/senslog/analyzer/domain/Timestamp.java

@@ -1,7 +1,7 @@
 package cz.senslog.analyzer.domain;
 
 import java.time.Instant;
-import java.time.ZonedDateTime;
+import java.time.OffsetDateTime;
 import java.time.format.DateTimeFormatter;
 import java.time.format.DateTimeFormatterBuilder;
 import java.util.Objects;
@@ -23,25 +23,25 @@ public class Timestamp {
             .toFormatter();
 
 
-    private final ZonedDateTime value;
+    private final OffsetDateTime value;
 
     public static Timestamp parse(String value) {
-        return of(ZonedDateTime.parse(value, DATE_TIME_FORMATTER));
+        return of(OffsetDateTime.parse(value, DATE_TIME_FORMATTER));
     }
 
     public static Timestamp now() {
-        return of(ZonedDateTime.now());
+        return of(OffsetDateTime.now());
     }
 
-    public static Timestamp of(ZonedDateTime value) {
+    public static Timestamp of(OffsetDateTime value) {
         return new Timestamp(value);
     }
 
-    private Timestamp(ZonedDateTime value) {
+    private Timestamp(OffsetDateTime value) {
         this.value = value;
     }
 
-    public ZonedDateTime get() {
+    public OffsetDateTime get() {
         return value;
     }
 
@@ -77,8 +77,8 @@ public class Timestamp {
     public boolean equals(Object o) {
         if (this == o) return true;
         if (o == null || getClass() != o.getClass()) return false;
-        Timestamp timestamp = (Timestamp) o;
-        return Objects.equals(value, timestamp.value);
+        Timestamp that = (Timestamp) o;
+        return value.equals(that.value);
     }
 
     @Override

+ 0 - 29
src/main/java/cz/senslog/analyzer/persistence/AttributeCode.java

@@ -1,29 +0,0 @@
-package cz.senslog.analyzer.persistence;
-
-public enum AttributeCode {
-
-    MIN     (0001),
-    MAX     (0002),
-    SUM     (0003),
-    COUNT   (0004),
-
-    ;
-
-    private final int code;
-    AttributeCode(int code) {
-        this.code = code;
-    }
-
-    public int getCode() {
-        return code;
-    }
-
-    public static AttributeCode valueOf(int code) {
-        for (AttributeCode value : values()) {
-            if (value.code == code) {
-                return value;
-            }
-        }
-        return null;
-    }
-}

+ 1 - 1
src/main/java/cz/senslog/analyzer/persistence/ConnectionModule.java

@@ -16,7 +16,7 @@ import javax.inject.Singleton;
 @Module
 public class ConnectionModule {
 
-    private static Logger logger = LogManager.getLogger(ConnectionModule.class);
+    private static final Logger logger = LogManager.getLogger(ConnectionModule.class);
 
     private final DatabaseConfig config;
 

+ 4 - 4
src/main/java/cz/senslog/analyzer/persistence/RepositoryModule.java

@@ -1,6 +1,6 @@
 package cz.senslog.analyzer.persistence;
 
-import cz.senslog.analyzer.persistence.repository.SenslogRepository;
+import cz.senslog.analyzer.persistence.repository.SensLogRepository;
 import cz.senslog.analyzer.persistence.repository.StatisticsConfigRepository;
 import cz.senslog.analyzer.persistence.repository.StatisticsRepository;
 import dagger.Module;
@@ -17,9 +17,9 @@ public class RepositoryModule {
     private static Logger logger = LogManager.getLogger(RepositoryModule.class);
 
     @Provides @Singleton
-    public SenslogRepository provideObservationRepository(Connection<Jdbi> connection) {
-        logger.info("Creating a new instance of {}.", SenslogRepository.class);
-        return new SenslogRepository(connection);
+    public SensLogRepository provideObservationRepository(Connection<Jdbi> connection) {
+        logger.info("Creating a new instance of {}.", SensLogRepository.class);
+        return new SensLogRepository(connection);
     }
 
     @Provides @Singleton

+ 0 - 27
src/main/java/cz/senslog/analyzer/persistence/SensorIdConverter.java

@@ -1,27 +0,0 @@
-package cz.senslog.analyzer.persistence;
-
-import cz.senslog.analyzer.domain.Sensor;
-import cz.senslog.common.util.Tuple;
-
-public class SensorIdConverter {
-
-    public static long encodeId(Sensor sensor, AttributeCode property) {
-        long sensorId = sensor.getSensorId();
-        int code = property.getCode();
-        // return (sensorId << 4) + code;
-        return sensorId * 10000 + code;
-    }
-
-    public static Tuple<Sensor, AttributeCode> decodeId(long id) {
-        int code = (int) (id % 10000);
-        long sensorId = id / 10000;
-        /*
-        int code = (int)id & 0b1111;
-        long sensorId = id >> 4;
-        */
-
-        AttributeCode property = AttributeCode.valueOf(code);
-        Sensor sensor = new Sensor(sensorId);
-        return Tuple.of(sensor, property);
-    }
-}

+ 56 - 0
src/main/java/cz/senslog/analyzer/persistence/repository/SensLogRepository.java

@@ -0,0 +1,56 @@
+package cz.senslog.analyzer.persistence.repository;
+
+import cz.senslog.analyzer.domain.Alert;
+import cz.senslog.analyzer.domain.Observation;
+import cz.senslog.analyzer.domain.Sensor;
+import cz.senslog.analyzer.domain.Timestamp;
+import cz.senslog.analyzer.persistence.Connection;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.jdbi.v3.core.Jdbi;
+
+import javax.inject.Inject;
+import java.time.OffsetDateTime;
+import java.util.List;
+
+public class SensLogRepository {
+
+    private static final Logger logger = LogManager.getLogger(SensLogRepository.class);
+
+    private final Jdbi jdbi;
+
+    @Inject
+    public SensLogRepository(Connection<Jdbi> connection) {
+        this.jdbi = connection.get();
+    }
+
+    public List<Observation> getObservationsFromTime(OffsetDateTime timestamp) {
+        return jdbi.withHandle(h -> h.createQuery(
+                "SELECT unit_id, sensor_id, observed_value, time_stamp FROM public.observations " +
+                        "WHERE time_stamp >= :timestamp ORDER BY time_stamp LIMIT 100"
+                )
+                    .bind("timestamp", timestamp)
+                    .map((rs, ctx) -> new Observation(
+                            new Sensor(
+                                    rs.getLong("unit_id"),
+                                    rs.getLong("sensor_id")
+                            ),
+                            rs.getDouble("observed_value"),
+                            Timestamp.parse(rs.getString("time_stamp"))
+                    )).list()
+        );
+    }
+
+    public void saveAlert(Alert alert) {
+        String message = alert.getMessage();
+        String shortMsg = message.length() >= 100 ? message.substring(0, 99) : message;
+        try {
+            jdbi.<Exception>useHandle(h -> h.inTransaction(t -> t.execute(
+                    "INSERT INTO public.alerts(alert_id, alert_description) SELECT coalesce(MAX(alert_id),0)+1, ? FROM public.alerts;", shortMsg)
+            ));
+        } catch (Exception e) {
+            logger.error("Can not persist this data: {}.", shortMsg);
+            logger.error(e.getMessage());
+        }
+    }
+}

+ 0 - 45
src/main/java/cz/senslog/analyzer/persistence/repository/SenslogRepository.java

@@ -1,45 +0,0 @@
-package cz.senslog.analyzer.persistence.repository;
-
-import cz.senslog.analyzer.domain.Alert;
-import cz.senslog.analyzer.domain.Observation;
-import cz.senslog.analyzer.domain.Sensor;
-import cz.senslog.analyzer.domain.Timestamp;
-import cz.senslog.analyzer.persistence.Connection;
-import org.jdbi.v3.core.Jdbi;
-
-import javax.inject.Inject;
-import java.time.Instant;
-import java.util.List;
-
-public class SenslogRepository {
-
-    private final Jdbi jdbi;
-
-    @Inject
-    public SenslogRepository(Connection<Jdbi> connection) {
-        this.jdbi = connection.get();
-    }
-
-    public List<Observation> getObservationsFromTime(Instant timestamp) {
-        return jdbi.withHandle(h -> h.createQuery(
-                "SELECT sensor_id as sensorId, observed_value as value, time_stamp as timestamp " +
-                        "FROM public.observations " +
-                        "WHERE time_stamp >= :timestamp " +
-                        "ORDER BY time_stamp LIMIT 100"
-                )
-                    .bind("timestamp", timestamp)
-                    .map((rs, ctx) -> new Observation(
-                            new Sensor(rs.getLong("sensorId")),
-                            rs.getDouble("value"),
-                            Timestamp.parse(rs.getString("timestamp"))
-                    )).list()
-        );
-    }
-
-    public void saveAlert(Alert alert) {
-        String msg = alert.getMessage().substring(0, 99);
-        jdbi.useHandle(h -> h.inTransaction(t -> t.execute(
-                "INSERT INTO public.alerts(alert_id, alert_description) SELECT coalesce(MAX(alert_id),0)+1, ? FROM public.alerts;", msg)
-        ));
-    }
-}

+ 61 - 47
src/main/java/cz/senslog/analyzer/persistence/repository/StatisticsConfigRepository.java

@@ -1,13 +1,19 @@
 package cz.senslog.analyzer.persistence.repository;
 
-import cz.senslog.analyzer.domain.FilteredSensor;
+import cz.senslog.analyzer.domain.AggregationType;
 import cz.senslog.analyzer.domain.Group;
+import cz.senslog.analyzer.domain.Sensor;
 import cz.senslog.analyzer.domain.Threshold;
 import cz.senslog.analyzer.persistence.Connection;
 import org.jdbi.v3.core.Jdbi;
 
 import javax.inject.Inject;
+import java.util.AbstractMap;
 import java.util.List;
+import java.util.Map;
+
+import static java.util.Collections.emptySet;
+import static java.util.stream.Collectors.*;
 
 public class StatisticsConfigRepository {
 
@@ -19,75 +25,83 @@ public class StatisticsConfigRepository {
     }
 
 
-    public List<FilteredSensor> getAllAvailableSensors() {
+    public List<Sensor> getAllAvailableSensors() {
         return jdbi.withHandle(h -> h.createQuery(
-                        "SELECT DISTINCT sensor_id AS id FROM statistics.sensors"
+                "SELECT s.unit_id AS unit_id, s.sensor_id AS sensor_id, g.id as group_id " +
+                        "FROM statistics.sensors AS s " +
+                        "JOIN statistics.sensor_to_group AS sg ON sg.sensor_id = s.id " +
+                        "JOIN statistics.groups_interval AS g ON sg.group_id = g.id"
                 )
-                        .map((rs, ctx) -> {
-                            Long id = rs.getLong("id");
-                            return new FilteredSensor(id, id);
-                        }).list()
+                        .map((rs, ctx) -> new Sensor(
+                                rs.getLong("unit_id"),
+                                rs.getLong("sensor_id"),
+                                rs.getLong("group_id")
+                        )).list()
         );
     }
 
-    public List<FilteredSensor> getAllGroupsMapping() {
+    public List<Group> getGroupInfos() {
         return jdbi.withHandle(h -> h.createQuery(
-                    "SELECT s.sensor_id AS id, s.group_id AS new_id " +
-                        "FROM statistics.sensors AS s " +
-                        "JOIN statistics.groups AS g ON g.id = s.group_id " +
-                        "WHERE g.interval IS NOT NULL"
-                )
-                        .map((rs, ctx) -> new FilteredSensor(
-                                rs.getLong("id"),
-                                rs.getLong("new_id")
-                        )).list()
+                "SELECT " +
+                        "g.id AS group_id, " +
+                        "s.sensor_id AS sensor_id, " +
+                        "s.unit_id AS unit_id, " +
+                        "g.time_interval AS time_interval, " +
+                        "g.persistence AS persistence, " +
+                        "g.aggregation_type AS aggregation_type " +
+                        "FROM statistics.groups_interval AS g " +
+                        "JOIN statistics.sensor_to_group AS sg ON sg.group_id = g.id " +
+                        "JOIN statistics.sensors AS s ON s.id = sg.sensor_id " +
+                        "WHERE g.time_interval != 0"
+        )
+                .map((rs, ctx) -> {
+                    long groupId = rs.getLong("group_id");
+                    Sensor sensor = new Sensor(
+                            rs.getLong("unit_id"),
+                            rs.getLong("sensor_id"),
+                            groupId
+                    );
+                    Group group = new Group(
+                            groupId,
+                            rs.getLong("time_interval"),
+                            rs.getBoolean("persistence"),
+                            AggregationType.valueOf(rs.getString("aggregation_type")),
+                            emptySet()
+                    );
+                    return new AbstractMap.SimpleEntry<>(group, sensor);
+                }).collect(groupingBy(Map.Entry::getKey, mapping(Map.Entry::getValue, toSet())))
+                .entrySet().stream().map(entry -> new Group(entry.getKey(), entry.getValue())).collect(toList())
         );
     }
 
     public List<Threshold> getCurrentThresholdsValue() {
         return jdbi.withHandle(h -> h.createQuery(
-                    "SELECT s.sensor_id AS id, ths.property AS property, ths.mode AS mode, ths.value AS value " +
-                        "FROM statistics.sensors AS s " +
-                        "JOIN statistics.groups AS g ON g.id = s.group_id " +
-                        "JOIN statistics.thresholds AS ths ON ths.group_id = g.id " +
-                        "WHERE g.interval IS NULL"
+                "SELECT t.group_id AS group_id, t.mode AS mode, t.property AS property, t.threshold_value AS threshold_value " +
+                        "FROM statistics.thresholds AS t " +
+                        "JOIN statistics.groups_interval AS g ON g.id = t.group_id " +
+                        "WHERE g.time_interval = 0"
                 )
                         .map((rs, ctx) -> new Threshold(
-                                rs.getLong("id"),
+                                rs.getLong("group_id"),
                                 rs.getString("property"),
                                 rs.getString("mode"),
-                                rs.getDouble("value")
+                                rs.getDouble("threshold_value")
                         )).list()
         );
     }
 
-    public List<Group> getAllIntervalGroups() {
-        return jdbi.withHandle(h -> h.createQuery(
-                    "SELECT id, interval " +
-                        "FROM statistics.groups " +
-                        "WHERE interval IS NOT NULL"
-                )
-                        .map((rs, ctx) -> new Group(
-                                rs.getLong("id"),
-                                rs.getLong("interval")
-                        )).list()
-        );
-    }
-
-
-
     public List<Threshold> getIntervalThresholdsValue() {
         return jdbi.withHandle(h -> h.createQuery(
-                    "SELECT ths.group_id AS id, ths.property AS property, ths.mode AS mode, ths.value AS value " +
-                        "FROM statistics.thresholds AS ths " +
-                        "JOIN statistics.groups AS g ON g.id = ths.group_id " +
-                        "WHERE g.interval IS NOT NULL"
+                "SELECT t.group_id AS group_id, t.mode AS mode, t.property AS property, t.threshold_value AS threshold_value " +
+                        "FROM statistics.thresholds AS t " +
+                        "JOIN statistics.groups_interval AS g ON g.id = t.group_id " +
+                        "WHERE g.time_interval != 0"
                 )
                         .map((rs, ctx) -> new Threshold(
-                            rs.getLong("id"),
-                            rs.getString("property"),
-                            rs.getString("mode"),
-                            rs.getDouble("value")
+                                rs.getLong("group_id"),
+                                rs.getString("property"),
+                                rs.getString("mode"),
+                                rs.getDouble("threshold_value")
                         )).list()
         );
     }

+ 129 - 101
src/main/java/cz/senslog/analyzer/persistence/repository/StatisticsRepository.java

@@ -1,30 +1,26 @@
 package cz.senslog.analyzer.persistence.repository;
 
-import cz.senslog.analyzer.domain.DoubleStatistics;
-import cz.senslog.analyzer.domain.Observation;
-import cz.senslog.analyzer.domain.Sensor;
-import cz.senslog.analyzer.domain.Timestamp;
-import cz.senslog.analyzer.persistence.AttributeCode;
+import cz.senslog.analyzer.domain.*;
 import cz.senslog.analyzer.persistence.Connection;
 import cz.senslog.common.util.TimeRange;
 import cz.senslog.common.util.Tuple;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
 import org.jdbi.v3.core.Jdbi;
 import org.jdbi.v3.core.statement.PreparedBatch;
 
 import javax.inject.Inject;
 import java.time.Instant;
-import java.time.LocalDateTime;
 import java.util.*;
-import java.util.stream.Stream;
-
-import static cz.senslog.analyzer.persistence.AttributeCode.*;
-import static cz.senslog.analyzer.persistence.SensorIdConverter.decodeId;
-import static cz.senslog.analyzer.persistence.SensorIdConverter.encodeId;
 
+import static cz.senslog.analyzer.domain.AggregationType.DOUBLE;
+import static java.util.Collections.singletonList;
 import static java.util.stream.Collectors.*;
 
 public class StatisticsRepository {
 
+    private static final Logger logger = LogManager.getLogger(StatisticsRepository.class);
+
     private final Jdbi jdbi;
 
     @Inject
@@ -33,116 +29,148 @@ public class StatisticsRepository {
     }
 
     public void save(DoubleStatistics statistics) {
-        save(Collections.singletonList(statistics));
+        try {
+            saveStatisticsBatch(singletonList(statistics));
+        } catch (Exception e) {
+            logger.error("Can not persist this data: {}.", statistics);
+            logger.error(e.getMessage());
+        }
     }
 
     public void save(List<DoubleStatistics> statistics) {
-        jdbi.withHandle(h -> h.inTransaction(t -> {
-
-           PreparedBatch batch = t.prepareBatch("INSERT INTO statistics.records(sensor_attribute, value, interval, timestamp) " +
-                   "VALUES(:id, :value, :interval, :timestamp)");
-
-            statistics.forEach(st -> batch
-                        .bind("id", encodeId(st.getSensor(), MIN))
-                        .bind("value", st.getMin())
-                        .bind("interval", st.getInterval())
-                        .bind("timestamp", st.getTimestamp().get())
-                    .add()
-                        .bind("id", encodeId(st.getSensor(), MAX))
-                        .bind("value", st.getMax())
-                        .bind("interval", st.getInterval())
-                        .bind("timestamp", st.getTimestamp().get())
-                    .add()
-                        .bind("id", encodeId(st.getSensor(), SUM))
-                        .bind("value", st.getSum())
-                        .bind("interval", st.getInterval())
-                        .bind("timestamp", st.getTimestamp().get())
-                    .add()
-                        .bind("id", encodeId(st.getSensor(), COUNT))
-                        .bind("value", Long.valueOf(st.getCount()).doubleValue())
-                        .bind("interval", st.getInterval())
-                        .bind("timestamp", st.getTimestamp().get())
-                    .add()
+        try {
+            saveStatisticsBatch(statistics);
+        } catch (Exception e) {
+            logger.warn(e.getMessage());
+            statistics.forEach(this::save);
+        }
+    }
+
+    private void saveStatisticsBatch(List<DoubleStatistics> statistics) throws Exception {
+            jdbi.<int[], Exception>withHandle(h -> h.inTransaction(t -> {
+                PreparedBatch batch = t.prepareBatch(
+                        "INSERT INTO statistics.records(group_id, value_attribute, record_value, time_interval, time_stamp) " +
+                                "VALUES(:group_id, :value_attribute, :recorded_value, :time_interval, :time_stamp)"
                 );
 
-           return batch.execute();
-        }));
+                statistics.stream().filter(st -> st.getSource().isPersistence()).forEach(st -> {
+                    long groupId = st.getSource().getId();
+                    long interval = st.getSource().getInterval();
+                    batch
+                            .bind("group_id", groupId)
+                            .bind("value_attribute", AttributeValue.MIN)
+                            .bind("recorded_value", st.getMin())
+                            .bind("time_interval", interval)
+                            .bind("time_stamp", st.getTimestamp().get())
+                            .add()
+                            .bind("group_id", groupId)
+                            .bind("value_attribute", AttributeValue.MAX)
+                            .bind("recorded_value", st.getMax())
+                            .bind("time_interval", interval)
+                            .bind("time_stamp", st.getTimestamp().get())
+                            .add()
+                            .bind("group_id", groupId)
+                            .bind("value_attribute", AttributeValue.SUM)
+                            .bind("recorded_value", st.getSum())
+                            .bind("time_interval", interval)
+                            .bind("time_stamp", st.getTimestamp().get())
+                            .add()
+                            .bind("group_id", groupId)
+                            .bind("value_attribute", AttributeValue.COUNT)
+                            .bind("recorded_value", Long.valueOf(st.getCount()).doubleValue())
+                            .bind("time_interval", interval)
+                            .bind("time_stamp", st.getTimestamp().get())
+                            .add();
+                });
+                return batch.execute();
+            }));
     }
 
-    public List<DoubleStatistics> getByTimeRange(Sensor sensor, TimeRange<Instant> timeRange) {
-        Map<Tuple<Timestamp, Integer>, List<Tuple<AttributeCode, Double>>> result =
-                jdbi.withHandle(h -> h.createQuery(
-                        "SELECT r.sensor_attribute as id, r.value as value, r.interval as interval, r.timestamp as timestamp " +
-                                "FROM statistics.records AS r " +
-                                "WHERE (r.sensor_attribute = :id_min " +
-                                            "OR r.sensor_attribute = :id_max " +
-                                            "OR r.sensor_attribute = :id_sum " +
-                                            "OR r.sensor_attribute = :id_count) " +
-                                    "AND timestamp >= :time_from " +
-                                    "AND (r.timestamp + r.interval * interval '1 second') < :time_to"
+    public List<DoubleStatistics> getByTimeRange(long groupId, TimeRange<Instant> timeRange) {
+
+        class RawRecord {
+            long recordId, groupId, sensorId, unitId;
+            double value; int interval;
+            AttributeValue attribute;
+            Timestamp timestamp;
+            AggregationType aggregationType;
+        }
+
+        List<RawRecord> rawRecords = jdbi.withHandle(h -> h.createQuery(
+                "SELECT r.id AS record_id," +
+                        "g.id AS group_id," +
+                        "s.sensor_id AS sensor_id," +
+                        "s.unit_id AS unit_id," +
+                        "r.value_attribute AS attribute," +
+                        "r.record_value AS value," +
+                        "r.time_interval AS interval," +
+                        "r.time_stamp AS time_stamp," +
+                        "g.aggregation_type AS aggregation_type " +
+                        "FROM statistics.records AS r " +
+                        "JOIN statistics.groups_interval AS g ON g.id = r.group_id " +
+                        "JOIN statistics.sensor_to_group AS sg ON sg.group_id = r.group_id " +
+                        "JOIN statistics.sensors AS s ON s.id = sg.sensor_id " +
+                        "WHERE r.time_stamp >= :time_from " +
+                        "AND (r.time_stamp + r.time_interval * interval '1 second') < :time_to " +
+                        "AND r.created >= sg.created " +
+                        "ORDER BY r.created"
                 )
-                    .bind("id_min", encodeId(sensor, MIN))
-                    .bind("id_max", encodeId(sensor, MAX))
-                    .bind("id_sum", encodeId(sensor, SUM))
-                    .bind("id_count", encodeId(sensor, COUNT))
+                    .bind("group_id", groupId)
                     .bind("time_from", timeRange.getFrom())
                     .bind("time_to", timeRange.getTo())
                 .map((rs, ctx) -> {
-                    AttributeCode code = decodeId(rs.getLong("id")).getItem2();
-                    Double value = rs.getDouble("value");
-                    Tuple<AttributeCode, Double> val = Tuple.of(code, value);
+                    RawRecord r = new RawRecord();
+                    r.recordId = rs.getLong("record_id");
+                    r.groupId = rs.getLong("group_id");
+                    r.sensorId = rs.getLong("sensor_id");
+                    r.unitId = rs.getLong("unit_id");
+                    r.attribute = AttributeValue.valueOf(rs.getString("attribute"));
+                    r.value = rs.getDouble("value");
+                    r.interval = rs.getInt("interval");
+                    r.timestamp = Timestamp.parse(rs.getString("time_stamp"));
+                    r.aggregationType = AggregationType.valueOf(rs.getString("aggregation_type"));
+                    return r;
+                }).stream().filter(r -> r.aggregationType.equals(DOUBLE))
+                    .collect(toList())
+        );
 
-                    Timestamp timestamp = Timestamp.parse(rs.getString("timestamp"));
-                    Integer interval = rs.getInt("interval");
-                    Tuple<Timestamp, Integer> key = Tuple.of(timestamp, interval);
+        Set<Sensor> sensors = new HashSet<>();
 
-                    return new AbstractMap.SimpleEntry<>(key, val);
-                }).collect(groupingBy(Map.Entry::getKey, mapping(Map.Entry::getValue, toList()))));
+        Map<Long, RawRecord> districtRecords = new HashMap<>();
+        for (RawRecord rawRecord : rawRecords) {
+            sensors.add(new Sensor(rawRecord.sensorId, rawRecord.unitId, rawRecord.groupId));
+            districtRecords.put(rawRecord.recordId, rawRecord);
+        }
 
-        // TODO refactor
-        return result.entrySet().stream().filter(e -> e.getValue().size() == 4).flatMap(recordEntry -> {
-            Tuple<Timestamp, Integer> key = recordEntry.getKey();
-            Timestamp timestamp = key.getItem1();
-            Integer interval = key.getItem2();
+        Map<Tuple<Timestamp, Integer>, List<RawRecord>> groupedRecords = new HashMap<>();
+        for (RawRecord record : districtRecords.values()) {
+            groupedRecords.computeIfAbsent(Tuple.of(record.timestamp, record.interval), k -> new ArrayList<>())
+                    .add(record);
+        }
+
+        List<DoubleStatistics> statistics = new ArrayList<>(groupedRecords.size());
+        for (Map.Entry<Tuple<Timestamp, Integer>, List<RawRecord>> stEntry : groupedRecords.entrySet()) {
+            Timestamp timestamp = stEntry.getKey().getItem1();
+            int interval = stEntry.getKey().getItem2();
+            List<RawRecord> records = stEntry.getValue();
 
             double min=0, max=0, sum=0; long count=0;
             boolean unknown = false;
-            for (Tuple<AttributeCode, Double> valEntry : recordEntry.getValue()) {
-                switch (valEntry.getItem1()) {
-                    case MAX: max = valEntry.getItem2(); break;
-                    case MIN: min = valEntry.getItem2(); break;
-                    case SUM: sum = valEntry.getItem2(); break;
-                    case COUNT: count = valEntry.getItem2().longValue(); break;
+            for (RawRecord r : records) {
+                switch (r.attribute) {
+                    case MAX: max = r.value; break;
+                    case MIN: min = r.value; break;
+                    case SUM: sum = r.value; break;
+                    case COUNT: count = (long) r.value; break;
                     default: unknown = true;
                 }
             }
+            if (!unknown) {
+                Group group = new Group(groupId, interval, true, DOUBLE, sensors);
+                statistics.add(new DoubleStatistics(group, count, min, max, sum, timestamp));
+            }
+        }
 
-            return unknown ? Stream.empty() : Stream.of(new DoubleStatistics(sensor, count, min, max, sum, timestamp, interval));
-        }).collect(toList());
-    }
-
-    public List<Observation> getRawByTimeRange(Sensor sensor, TimeRange<Instant> timeRange) {
-        return jdbi.withHandle(h -> h.createQuery("SELECT r.sensor_attribute as id, r.value as value, r.timestamp as timestamp " +
-                        "FROM statistics.records AS r " +
-                        "WHERE (r.sensor_attribute = :id_min " +
-                        "OR r.sensor_attribute = :id_max " +
-                        "OR r.sensor_attribute = :id_sum " +
-                        "OR r.sensor_attribute = :id_count) " +
-                        "AND timestamp >= :time_from " +
-                        "AND (r.timestamp + r.interval * interval '1 second') < :time_to"
-                )
-                    .bind("id_min", encodeId(sensor, MIN))
-                    .bind("id_max", encodeId(sensor, MAX))
-                    .bind("id_sum", encodeId(sensor, SUM))
-                    .bind("id_count", encodeId(sensor, COUNT))
-                    .bind("time_from", timeRange.getFrom())
-                    .bind("time_to", timeRange.getTo())
-                    .map((rs, ctx) -> new Observation(
-                            new Sensor(rs.getLong("id")),
-                            rs.getDouble("value"),
-                            Timestamp.parse(rs.getString("timestamp"))
-                        )
-                    ).list()
-        );
+        return statistics;
     }
 }

+ 7 - 7
src/main/java/cz/senslog/analyzer/provider/ProviderConfig.java

@@ -1,13 +1,13 @@
 package cz.senslog.analyzer.provider;
 
-import java.time.LocalDateTime;
+import java.time.OffsetDateTime;
 
 public class ProviderConfig {
 
     public interface ConfigurationBuilder {
 
         ConfigurationBuilder period(int period);
-        ConfigurationBuilder startDateTime(LocalDateTime startDateTime);
+        ConfigurationBuilder startDateTime(OffsetDateTime startDateTime);
 
         ProviderConfig get();
     }
@@ -16,7 +16,7 @@ public class ProviderConfig {
         return new ConfigurationBuilder() {
 
             private int period;
-            private LocalDateTime startDateTime;
+            private OffsetDateTime startDateTime;
 
             @Override
             public ConfigurationBuilder period(int period) {
@@ -25,7 +25,7 @@ public class ProviderConfig {
             }
 
             @Override
-            public ConfigurationBuilder startDateTime(LocalDateTime startDateTime) {
+            public ConfigurationBuilder startDateTime(OffsetDateTime startDateTime) {
                 this.startDateTime = startDateTime;
                 return this;
             }
@@ -38,9 +38,9 @@ public class ProviderConfig {
     }
 
     private final int period;
-    private final LocalDateTime startDateTime;
+    private final OffsetDateTime startDateTime;
 
-    private ProviderConfig(int period, LocalDateTime startDateTime) {
+    private ProviderConfig(int period, OffsetDateTime startDateTime) {
         this.period = period;
         this.startDateTime = startDateTime;
     }
@@ -49,7 +49,7 @@ public class ProviderConfig {
         return period;
     }
 
-    public LocalDateTime getStartDateTime() {
+    public OffsetDateTime getStartDateTime() {
         return startDateTime;
     }
 }

+ 2 - 2
src/main/java/cz/senslog/analyzer/provider/ScheduleDatabaseProviderModule.java

@@ -1,7 +1,7 @@
 package cz.senslog.analyzer.provider;
 
 import cz.senslog.analyzer.persistence.RepositoryModule;
-import cz.senslog.analyzer.persistence.repository.SenslogRepository;
+import cz.senslog.analyzer.persistence.repository.SensLogRepository;
 import dagger.Module;
 import dagger.Provides;
 
@@ -14,7 +14,7 @@ public class ScheduleDatabaseProviderModule {
     }
 
     @Provides
-    public ScheduledDatabaseProvider provideScheduleDatabaseProvider(SenslogRepository repository) {
+    public ScheduledDatabaseProvider provideScheduleDatabaseProvider(SensLogRepository repository) {
         return new ScheduledDatabaseProvider(repository);
     }
 }

+ 31 - 10
src/main/java/cz/senslog/analyzer/provider/ScheduledDatabaseProvider.java

@@ -2,23 +2,25 @@ package cz.senslog.analyzer.provider;
 
 import cz.senslog.analyzer.analysis.Analyzer;
 import cz.senslog.analyzer.domain.Observation;
-import cz.senslog.analyzer.persistence.repository.SenslogRepository;
+import cz.senslog.analyzer.domain.Timestamp;
+import cz.senslog.analyzer.persistence.repository.SensLogRepository;
 import cz.senslog.common.util.schedule.Scheduler;
 import org.apache.logging.log4j.LogManager;
 import org.apache.logging.log4j.Logger;
 
 import javax.inject.Inject;
 import java.time.*;
+import java.util.ArrayList;
 import java.util.List;
 
 public class ScheduledDatabaseProvider extends DataProvider {
 
-    private static Logger logger = LogManager.getLogger(ScheduledDatabaseProvider.class);
+    private static final Logger logger = LogManager.getLogger(ScheduledDatabaseProvider.class);
 
-    private final SenslogRepository repository;
+    private final SensLogRepository repository;
 
     @Inject
-    public ScheduledDatabaseProvider(SenslogRepository repository) {
+    public ScheduledDatabaseProvider(SensLogRepository repository) {
         this.repository = repository;
     }
 
@@ -33,15 +35,15 @@ public class ScheduledDatabaseProvider extends DataProvider {
 
     private static class AnalyzerTask implements Runnable {
 
-        private final SenslogRepository repository;
+        private final SensLogRepository repository;
         private final Analyzer analyzer;
 
-        private Instant timestamp;
+        private OffsetDateTime timestamp;
 
-        private AnalyzerTask(Analyzer analyzer, SenslogRepository repository, LocalDateTime startDateTime) {
+        private AnalyzerTask(Analyzer analyzer, SensLogRepository repository, OffsetDateTime startDateTime) {
             this.analyzer = analyzer;
             this.repository = repository;
-            this.timestamp = startDateTime.toInstant(OffsetDateTime.now(ZoneId.systemDefault()).getOffset());
+            this.timestamp = startDateTime;
         }
 
         @Override
@@ -49,8 +51,27 @@ public class ScheduledDatabaseProvider extends DataProvider {
             List<Observation> observations = repository.getObservationsFromTime(timestamp);
 
             if (!observations.isEmpty()) {
-                Observation lObs = observations.get(observations.size() - 1);
-                timestamp = lObs.getTimestamp().get().toInstant().plusSeconds(1);
+                Observation observationToDelete = observations.get(observations.size() - 1);
+                Timestamp timestampToDelete = observationToDelete.getTimestamp();
+
+                int originalCount = observations.size();
+                List<Observation> deletedObservations = new ArrayList<>();
+                for (int i = observations.size(); i-- > 0;) {
+                    Observation currentObservation = observations.get(i);
+                    Timestamp currentTimestamp = currentObservation.getTimestamp();
+                    if (currentTimestamp.equals(timestampToDelete)) {
+                        deletedObservations.add(currentObservation);
+                        observations.remove(i);
+                    }
+                }
+
+                if (originalCount == deletedObservations.size()) {
+                    observations = deletedObservations;
+                }
+
+                Observation lastObservation = observations.get(observations.size() - 1);
+                OffsetDateTime lastTimestamp = lastObservation.getTimestamp().get();
+                timestamp = lastTimestamp.plusSeconds(1);
             }
 
             analyzer.accept(observations);

+ 27 - 73
src/main/java/cz/senslog/analyzer/server/handler/StatisticsHandler.java

@@ -8,20 +8,20 @@ import cz.senslog.analyzer.domain.*;
 import cz.senslog.analyzer.persistence.repository.StatisticsRepository;
 import cz.senslog.analyzer.server.vertx.VertxAbstractHandler;
 import cz.senslog.common.util.TimeRange;
+import io.vertx.core.MultiMap;
 import io.vertx.core.http.HttpServerResponse;
 import io.vertx.ext.web.RoutingContext;
 
 import javax.inject.Inject;
 import java.time.Instant;
-import java.time.LocalDate;
 import java.time.format.DateTimeFormatter;
 import java.util.*;
 
+import static cz.senslog.analyzer.domain.AttributeValue.*;
 import static cz.senslog.common.http.HttpContentType.APPLICATION_JSON;
 import static cz.senslog.common.http.HttpHeader.CONTENT_TYPE;
-import static java.util.Collections.*;
+import static java.time.format.DateTimeFormatter.ofPattern;
 
-// TODO test statistic endpoint
 public class StatisticsHandler extends VertxAbstractHandler {
 
     private final StatisticsRepository repository;
@@ -29,22 +29,13 @@ public class StatisticsHandler extends VertxAbstractHandler {
     private static final Gson gson = new GsonBuilder()
             .registerTypeAdapter(DoubleStatistics.class, (JsonSerializer<DoubleStatistics>) (src, typeOfSrc, context1) -> {
                 JsonObject js = new JsonObject();
-                // js.addProperty("sensor_id", src.getSensor().getSensorId());
-                js.addProperty("time", src.getTimestamp().timeFormat());
+//                js.addProperty("time", src.getTimestamp().timeFormat());
                 // js.addProperty("date", src.getTimestamp().dateFormat());
-                // js.addProperty("timestamp", src.getTimestamp().format());
+                 js.addProperty("timestamp", src.getTimestamp().format());
 
-                // js.addProperty("interval", src.getInterval());
-                js.addProperty("min", src.getMin());
-                js.addProperty("max", src.getMax());
-                js.addProperty("avg", src.getAverage());
-                return js;
-            })
-            .registerTypeAdapter(Observation.class, (JsonSerializer<Observation>) (src, typeOfSrc, context1) -> {
-                JsonObject js = new JsonObject();
-                js.addProperty("sensor_id", src.getSensor().getSensorId());
-                js.addProperty("timestamp", src.getTimestamp().format());
-                js.addProperty("value", String.format("%.2f", src.getValue()));
+                js.addProperty(MIN.name().toLowerCase(), src.getMin());
+                js.addProperty(MAX.name().toLowerCase(), src.getMax());
+                js.addProperty(AVG.name().toLowerCase(), src.getAverage());
                 return js;
             })
             .create();
@@ -58,37 +49,17 @@ public class StatisticsHandler extends VertxAbstractHandler {
     @Override
     public void get(RoutingContext context) throws RuntimeException {
         HttpServerResponse response = context.response();
+        MultiMap params = context.request().params();
 
-        Sensor sensor = new Sensor(Long.parseLong(context.request().getParam("sensor_id")));
-        Timestamp from = Timestamp.parse(context.request().getParam("from"));
-        Timestamp to = Timestamp.parse(context.request().getParam("to"));
-        Interval interval = Interval.parse(context.request().getParam("interval"));
-        GroupBy groupBy = GroupBy.parse(context.request().getParam("groupBy"));
+        long groupId = Long.parseLong(params.get("group_id"));
+        Timestamp from = Timestamp.parse(params.get("from"));
+        Timestamp to = Timestamp.parse(params.get("to"));
+        GroupBy groupBy = params.contains("groupBy") ? GroupBy.parse(params.get("groupBy")) : null;
         TimeRange<Instant> timeRange = TimeRange.of(from.toInstant(), to.toInstant());
 
-        if ((interval != Interval.HOUR && groupBy == GroupBy.DAY) || (interval == Interval.HOUR && groupBy != GroupBy.DAY)) {
-            throw new RuntimeException(String.format(
-                    "Option '%s' and '%s' is not allowed. Combination of '%s' and '%s' is allowed only.",
-                    interval, groupBy, Interval.HOUR, GroupBy.DAY
-            ));
-        }
-
-        String format = context.request().getParam("format").toLowerCase();
-
         Object result;
-        switch (format) {
-            case "pretty": {
-                List<DoubleStatistics> statistics = repository.getByTimeRange(sensor, timeRange);
-                result = MergeStatistics
-                        .init(sensor, interval, groupBy).merge(statistics);
-            } break;
-            case "raw": {
-                result = repository.getRawByTimeRange(sensor, timeRange);
-            } break;
-            default: {
-                result = Collections.emptyList();
-            }
-        }
+        List<DoubleStatistics> statistics = repository.getByTimeRange(groupId, timeRange);
+        result = groupBy == null ? statistics : MergeStatistics.merge(groupBy, statistics);
 
         response.putHeader(CONTENT_TYPE, APPLICATION_JSON);
         response.end(gson.toJson(result));
@@ -96,39 +67,22 @@ public class StatisticsHandler extends VertxAbstractHandler {
 
     private static class MergeStatistics {
 
-        private final Interval interval;
-        private final GroupBy groupBy;
+        public static Map<String, List<DoubleStatistics>> merge(GroupBy groupBy, List<DoubleStatistics> statistics) {
+            Map<String, List<DoubleStatistics>> result = new HashMap<>();
+            DateTimeFormatter formatter = null;
 
-        private static MergeStatistics init(Sensor sensor, Interval interval, GroupBy groupBy){
-            return new MergeStatistics(sensor, interval, groupBy);
-        }
-
-        private MergeStatistics(Sensor sensor, Interval interval, GroupBy groupBy) {
-            this.interval = interval;
-            this.groupBy = groupBy;
-        }
-
-        public Map<String, List<DoubleStatistics>> merge(List<DoubleStatistics> statistics) {
-
-            Map<String, List<DoubleStatistics>> result = emptyMap();
-
-            if (groupBy == GroupBy.DAY) {
-                DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
-                Map<LocalDate, List<DoubleStatistics>> groupedResults = new HashMap<>();
-
-                for (DoubleStatistics st : statistics) {
-                    groupedResults.computeIfAbsent(st.getTimestamp().get().toLocalDate(), k -> new ArrayList<>())
-                            .add(st);
-                }
+            switch (groupBy) {
+                case DAY:   formatter = ofPattern("yyyy-MM-dd"); break;
+                case MONTH: formatter = ofPattern("yyyy-MM");    break;
+                case YEAR:  formatter = ofPattern("yyyy");       break;
+            }
 
-                result = new HashMap<>(groupedResults.size());
-                for (Map.Entry<LocalDate, List<DoubleStatistics>> entry : groupedResults.entrySet()) {
-                    result.put(entry.getKey().format(formatter), entry.getValue());
-                }
+            for (DoubleStatistics st : statistics) {
+                String time = st.getTimestamp().get().format(formatter);
+                result.computeIfAbsent(time, k -> new ArrayList<>()).add(st);
             }
 
             return result;
         }
     }
-}
-
+}

+ 1 - 1
src/main/java/cz/senslog/analyzer/server/vertx/VertxServer.java

@@ -14,7 +14,7 @@ import javax.inject.Inject;
 
 public class VertxServer extends AbstractVerticle implements Server {
 
-    private static Logger logger = LogManager.getLogger(VertxServer.class);
+    private static final Logger logger = LogManager.getLogger(VertxServer.class);
 
     private final InfoHandler infoHandler;
     private final StatisticsHandler statisticsHandler;

+ 0 - 26
src/test/java/cz/senslog/analyzer/persistence/SensorIdConverterTest.java

@@ -1,26 +0,0 @@
-package cz.senslog.analyzer.persistence;
-
-import cz.senslog.analyzer.domain.Sensor;
-import cz.senslog.common.util.Tuple;
-import org.junit.jupiter.api.Test;
-
-import static cz.senslog.analyzer.persistence.SensorIdConverter.decodeId;
-import static cz.senslog.analyzer.persistence.SensorIdConverter.encodeId;
-import static org.junit.jupiter.api.Assertions.*;
-
-class SensorIdConverterTest {
-
-    @Test
-    void encodePersistenceId_true() {
-        assertEquals(10001, encodeId(new Sensor(1L), AttributeCode.MIN));
-        assertEquals(280004, encodeId(new Sensor(28L), AttributeCode.COUNT));
-
-    }
-
-    @Test
-    void decodePersistenceId_true() {
-        Tuple<Sensor, AttributeCode> id = decodeId(280004);
-        assertEquals(28L, id.getItem1().getSensorId());
-        assertEquals(AttributeCode.COUNT, id.getItem2());
-    }
-}

Неке датотеке нису приказане због велике количине промена