Procházet zdrojové kódy

added HTML table, refactored code

Lukas Cerny před 4 roky
rodič
revize
0624a5db72
33 změnil soubory, kde provedl 695 přidání a 171 odebrání
  1. 1 1
      .gitignore
  2. 17 17
      config/foodie_senslog.yaml
  3. 8 4
      src/main/java/cz/senslog/watchdog/app/Application.java
  4. 10 10
      src/main/java/cz/senslog/watchdog/app/Watcher.java
  5. 6 5
      src/main/java/cz/senslog/watchdog/config/MonitoredObjectsConfig.java
  6. 33 29
      src/main/java/cz/senslog/watchdog/domain/ObservationInfo.java
  7. 24 0
      src/main/java/cz/senslog/watchdog/domain/Report.java
  8. 1 2
      src/main/java/cz/senslog/watchdog/domain/SimpleReport.java
  9. 44 0
      src/main/java/cz/senslog/watchdog/domain/Source.java
  10. 1 1
      src/main/java/cz/senslog/watchdog/domain/StatusReport.java
  11. 9 6
      src/main/java/cz/senslog/watchdog/messagebroker/ConsoleMessageBroker.java
  12. 2 0
      src/main/java/cz/senslog/watchdog/messagebroker/MessageBroker.java
  13. 2 0
      src/main/java/cz/senslog/watchdog/messagebroker/MessageStatus.java
  14. 0 27
      src/main/java/cz/senslog/watchdog/messagebroker/Report.java
  15. 36 36
      src/main/java/cz/senslog/watchdog/messagebroker/email/EmailMessageBroker.java
  16. 7 0
      src/main/java/cz/senslog/watchdog/messagebroker/writer/AbstractWriter.java
  17. 65 0
      src/main/java/cz/senslog/watchdog/messagebroker/writer/DelimiterTableWriter.java
  18. 112 0
      src/main/java/cz/senslog/watchdog/messagebroker/writer/HtmlTableWriter.java
  19. 4 0
      src/main/java/cz/senslog/watchdog/messagebroker/writer/TableHeaderWriter.java
  20. 9 0
      src/main/java/cz/senslog/watchdog/messagebroker/writer/TableRowWriter.java
  21. 9 0
      src/main/java/cz/senslog/watchdog/messagebroker/writer/TableWriter.java
  22. 10 4
      src/main/java/cz/senslog/watchdog/provider/Record.java
  23. 1 1
      src/main/java/cz/senslog/watchdog/provider/database/DatabaseDataProvider.java
  24. 1 1
      src/main/java/cz/senslog/watchdog/provider/database/SensLogRepository.java
  25. 3 1
      src/main/java/cz/senslog/watchdog/provider/ws/WebServiceDataProvider.java
  26. 137 0
      src/main/java/cz/senslog/watchdog/util/DateTrunc.java
  27. 14 0
      src/main/java/cz/senslog/watchdog/util/TimeConverter.java
  28. 28 22
      src/main/java/cz/senslog/watchdog/util/schedule/ScheduleTask.java
  29. 9 2
      src/main/java/cz/senslog/watchdog/util/schedule/Scheduler.java
  30. 27 2
      src/main/java/cz/senslog/watchdog/util/schedule/SchedulerBuilderImpl.java
  31. 14 0
      src/test/java/cz/senslog/watchdog/messagebroker/email/EmailMessageBrokerTest.java
  32. 31 0
      src/test/java/cz/senslog/watchdog/messagebroker/writer/HtmlTableWriterTest.java
  33. 20 0
      src/test/java/cz/senslog/watchdog/util/schedule/SchedulerTest.java

+ 1 - 1
.gitignore

@@ -1,4 +1,4 @@
-.idea
+.idea/
 *.iml
 bin
 logs

+ 17 - 17
config/foodie_senslog.yaml

@@ -6,43 +6,43 @@ dataProvider:
     user: "kynsperk"
 
 messageBroker:
-  type: CONSOLE # EMAIL, CONSOLE (for testing), WHATSAPP (not yet), TELEGRAM (not yet)
+  type: EMAIL # EMAIL, CONSOLE (for testing), WHATSAPP (not yet), TELEGRAM (not yet)
   vanishPeriod: 1 # number of days when already send report could be sent again
   config:
-#    smtpHost: "mail.lesprojekt.cz"
-#    smtpPort: 465
-#    authUsername: "watchdog@senslog.org"
-#    authPassword: "5jspdD"
-#    senderEmail: "watchdog@senslog.org"
-#    recipientEmail: "luc.cerny@gmail.com" # "kepka@ccss.cz"
-#    subject: "[Watchdog] Report Foodie SensLog (CZ)"
+    smtpHost: "mail.lesprojekt.cz"
+    smtpPort: 465
+    authUsername: "watchdog@senslog.org"
+    authPassword: "5jspdD"
+    senderEmail: "watchdog@senslog.org"
+    recipientEmail: "luc.cerny@gmail.com" # "kepka@ccss.cz"
+    subject: "[Watchdog] Report Foodie SensLog (CZ)"
 
 monitoredObjects:
   1305167562293765:
-    interval: 86400 # 1 day
+    interval: 3600 # 86400 # 1 day
     sensors: [340340092, 360200000, 410130092]
 
   1305167562275270:
-    interval: 86400 # 1 day
+    interval: 3600 # 86400 # 1 day
     sensors: [340240003, 360200000, 410090003]
 
   1305167562292824:
-    interval: 86400 # 1 day
+    interval: 3600 # 86400 # 1 day
 
   1305167549144045:
-    interval: 86400 # 1 day
+    interval: 3600 # 86400 # 1 day
 
   1305167549158198:
-    interval: 86400 # 1 day
+    interval: 3600 # 86400 # 1 day
 
   1305167549167050:
-    interval: 86400 # 1 day
+    interval: 3600 # 86400 # 1 day
 
   1305167549173046:
-    interval: 86400 # 1 day
+    interval: 3600 # 86400 # 1 day
 
   1305167549167886:
-    interval: 86400 # 1 day
+    interval: 3600 # 86400 # 1 day
 
   1305167549149707:
-    interval: 86400 # 1 day
+    interval: 3600 # 86400 # 1 day

+ 8 - 4
src/main/java/cz/senslog/watchdog/app/Application.java

@@ -4,11 +4,13 @@ package cz.senslog.watchdog.app;
 import cz.senslog.watchdog.config.Configuration;
 import cz.senslog.watchdog.messagebroker.MessageBroker;
 import cz.senslog.watchdog.provider.DataProvider;
+import cz.senslog.watchdog.util.DateTrunc;
 import cz.senslog.watchdog.util.schedule.Scheduler;
 import org.apache.logging.log4j.LogManager;
 import org.apache.logging.log4j.Logger;
 
 import java.io.IOException;
+import java.time.LocalDateTime;
 import java.time.LocalTime;
 
 public class Application extends Thread {
@@ -46,6 +48,7 @@ public class Application extends Thread {
         try {
             config = Configuration.load(configFile);
         } catch (IOException e) {
+            logger.catching(e);
             System.exit(1);
         }
 
@@ -53,11 +56,12 @@ public class Application extends Thread {
         MessageBroker messageBroker = MessageBroker.create(config.getMessageBrokerConfig());
         Watcher watcher = Watcher.create(config.getWatchingObjectsConfig(), dataProvider, messageBroker);
 
-        long period = config.getWatchingObjectsConfig().getMinInterval();
-//        LocalTime startAt = config.getWatchingObjectsConfig().getStartAt();
-        LocalTime startAt = LocalTime.of(17, 15);
+        int period = config.getWatchingObjectsConfig().getMinInterval();
+        LocalDateTime startAtTime = DateTrunc.trunc(LocalDateTime.now(), period).plusSeconds(period);
+
         Scheduler scheduler = Scheduler.createBuilder()
-                    .addTask(watcher::check, period, startAt)
+                    .addTask(watcher::check, period)
+//                    .addTask(watcher::check, period, startAtTime)
                 .build();
 
         scheduler.start();

+ 10 - 10
src/main/java/cz/senslog/watchdog/app/Watcher.java

@@ -2,8 +2,8 @@ package cz.senslog.watchdog.app;
 
 import cz.senslog.watchdog.config.MonitoredObjectsConfig;
 import cz.senslog.watchdog.messagebroker.MessageBroker;
-import cz.senslog.watchdog.messagebroker.Report;
-import cz.senslog.watchdog.messagebroker.SimpleReport;
+import cz.senslog.watchdog.domain.Report;
+import cz.senslog.watchdog.domain.SimpleReport;
 import cz.senslog.watchdog.provider.DataProvider;
 import cz.senslog.watchdog.provider.Record;
 import cz.senslog.watchdog.util.Tuple;
@@ -11,11 +11,16 @@ import org.apache.logging.log4j.LogManager;
 import org.apache.logging.log4j.Logger;
 
 import java.time.Instant;
+import java.time.LocalDateTime;
+import java.time.ZoneId;
+import java.time.ZoneOffset;
 import java.util.List;
 import java.util.stream.Collectors;
 
-import static cz.senslog.watchdog.messagebroker.StatusReport.FAIL;
-import static cz.senslog.watchdog.messagebroker.StatusReport.OK;
+import static cz.senslog.watchdog.domain.StatusReport.FAIL;
+import static cz.senslog.watchdog.domain.StatusReport.OK;
+import static java.time.LocalDateTime.ofInstant;
+import static java.time.ZoneOffset.UTC;
 
 public class Watcher {
 
@@ -44,12 +49,7 @@ public class Watcher {
                 .map(t -> new SimpleReport(t.getItem1(), t.getItem1().isValid(t.getItem2().get(), now) ? OK : FAIL))
                 .collect(Collectors.toList());
 
-        List<Record> recordsToReport = reports.stream() // TODO temporary solution
-                .filter(r -> r.getStatus().equals(FAIL))
-                .map(SimpleReport::getRecord)
-                .collect(Collectors.toList());
-
-        messageBroker.send(new Report(now, recordsToReport), status -> {
+        messageBroker.send(new Report(ofInstant(now, UTC), reports), status -> {
             String brokerType = messageBroker.getType().name().toLowerCase();
             if (status.isSuccess()) {
                 logger.info("The report at '{}' was send via '{}' broker successfully.",

+ 6 - 5
src/main/java/cz/senslog/watchdog/config/MonitoredObjectsConfig.java

@@ -1,5 +1,6 @@
 package cz.senslog.watchdog.config;
 
+import cz.senslog.watchdog.domain.Source;
 import cz.senslog.watchdog.provider.Record;
 import cz.senslog.watchdog.util.Tuple;
 
@@ -147,9 +148,9 @@ public class MonitoredObjectsConfig {
     }
 
     public Optional<Long> getInterval(Record record) {
-        Tuple<String, String> source = record.getSource();
-        String unitId = source.getItem1();
-        String sensorId = source.getItem2();
+        Source source = record.getSource();
+        String unitId = String.valueOf(source.getUnitId());
+        String sensorId = String.valueOf(source.getSensorId());
         Unit unit = units.get(unitId);
         if (unit == null) {
             return Optional.empty();
@@ -166,8 +167,8 @@ public class MonitoredObjectsConfig {
         return Optional.of(sensor.interval);
     }
 
-    public long getMinInterval() {
-        return minInterval;
+    public int getMinInterval() {
+        return (int) minInterval;
     }
 
     public long getMaxInterval() {

+ 33 - 29
src/main/java/cz/senslog/watchdog/provider/ObservationInfo.java → src/main/java/cz/senslog/watchdog/domain/ObservationInfo.java

@@ -1,64 +1,68 @@
-package cz.senslog.watchdog.provider;
+package cz.senslog.watchdog.domain;
 
-import cz.senslog.watchdog.util.Tuple;
+import cz.senslog.watchdog.messagebroker.writer.TableRowWriter;
+import cz.senslog.watchdog.provider.Record;
 
 import java.time.Instant;
 import java.time.OffsetDateTime;
 import java.util.Objects;
 
+import static java.lang.String.valueOf;
+
 public class ObservationInfo implements Record {
 
-    private final long unitId;
-    private final long sensorId;
+    private final Source source;
     private final OffsetDateTime timestamp;
 
     public ObservationInfo(long unitId, long sensorId, OffsetDateTime timestamp) {
-        this.unitId = unitId;
-        this.sensorId = sensorId;
+        this.source = new Source(unitId, sensorId);
         this.timestamp = timestamp;
     }
 
     @Override
-    public String toString() {
-        return "{" +
-                "unitId=" + unitId +
-                ", sensorId=" + sensorId +
-                ", timestamp=" + timestamp +
-                '}';
+    public Source getSource() {
+        return source;
     }
 
     @Override
-    public boolean equals(Object o) {
-        if (this == o) return true;
-        if (o == null || getClass() != o.getClass()) return false;
-        ObservationInfo info = (ObservationInfo) o;
-        return unitId == info.unitId &&
-                sensorId == info.sensorId &&
-                Objects.equals(timestamp, info.timestamp);
+    public Instant getTimestamp() {
+        return timestamp.toInstant();
     }
 
+
     @Override
-    public int hashCode() {
-        return Objects.hash(unitId, sensorId, timestamp);
+    public boolean isValid(long interval, Instant now) {
+        return timestamp.plusSeconds(interval).toInstant().isAfter(now);
     }
 
     @Override
-    public Tuple<String, String> getSource() {
-        return Tuple.of(String.valueOf(unitId), String.valueOf(sensorId));
+    public void writeRow(TableRowWriter writer) {
+        writer
+                .cell(valueOf(source.getUnitId()))
+                .cell(valueOf(source.getSensorId()))
+                .cell(valueOf(timestamp))
+                .end();
     }
 
     @Override
-    public Instant getTimestamp() {
-        return timestamp.toInstant();
+    public String toString() {
+        return "ObservationInfo{" +
+                "source=" + source +
+                ", timestamp=" + timestamp +
+                '}';
     }
 
     @Override
-    public String getMessage() {
-        return String.format("%s;%s;%s", unitId, sensorId, timestamp);
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+        ObservationInfo info = (ObservationInfo) o;
+        return Objects.equals(source, info.source) &&
+                Objects.equals(timestamp, info.timestamp);
     }
 
     @Override
-    public boolean isValid(long interval, Instant now) {
-        return timestamp.plusSeconds(interval).toInstant().isAfter(now);
+    public int hashCode() {
+        return Objects.hash(source, timestamp);
     }
 }

+ 24 - 0
src/main/java/cz/senslog/watchdog/domain/Report.java

@@ -0,0 +1,24 @@
+package cz.senslog.watchdog.domain;
+
+import java.time.LocalDateTime;
+import java.util.List;
+
+public class Report {
+
+    private final LocalDateTime created;
+
+    private final List<SimpleReport> reports;
+
+    public Report(LocalDateTime created, List<SimpleReport> reports) {
+        this.created = created;
+        this.reports = reports;
+    }
+
+    public LocalDateTime getCreated() {
+        return created;
+    }
+
+    public List<SimpleReport> getReports() {
+        return reports;
+    }
+}

+ 1 - 2
src/main/java/cz/senslog/watchdog/messagebroker/SimpleReport.java → src/main/java/cz/senslog/watchdog/domain/SimpleReport.java

@@ -1,11 +1,10 @@
-package cz.senslog.watchdog.messagebroker;
+package cz.senslog.watchdog.domain;
 
 import cz.senslog.watchdog.provider.Record;
 
 public class SimpleReport {
 
     private final Record record;
-
     private final StatusReport status;
 
     public SimpleReport(Record record, StatusReport status) {

+ 44 - 0
src/main/java/cz/senslog/watchdog/domain/Source.java

@@ -0,0 +1,44 @@
+package cz.senslog.watchdog.domain;
+
+import java.util.Objects;
+
+public class Source {
+
+    private final long unitId;
+    private final long sensorId;
+
+    public Source(long unitId, long sensorId) {
+        this.unitId = unitId;
+        this.sensorId = sensorId;
+    }
+
+    public long getUnitId() {
+        return unitId;
+    }
+
+    public long getSensorId() {
+        return sensorId;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+        Source source = (Source) o;
+        return unitId == source.unitId &&
+                sensorId == source.sensorId;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(unitId, sensorId);
+    }
+
+    @Override
+    public String toString() {
+        return "Source{" +
+                "unitId=" + unitId +
+                ", sensorId=" + sensorId +
+                '}';
+    }
+}

+ 1 - 1
src/main/java/cz/senslog/watchdog/messagebroker/StatusReport.java → src/main/java/cz/senslog/watchdog/domain/StatusReport.java

@@ -1,4 +1,4 @@
-package cz.senslog.watchdog.messagebroker;
+package cz.senslog.watchdog.domain;
 
 public enum StatusReport {
     OK, FAIL

+ 9 - 6
src/main/java/cz/senslog/watchdog/messagebroker/ConsoleMessageBroker.java

@@ -1,6 +1,10 @@
 package cz.senslog.watchdog.messagebroker;
 
 import cz.senslog.watchdog.config.MessageBrokerType;
+import cz.senslog.watchdog.domain.Report;
+import cz.senslog.watchdog.domain.SimpleReport;
+import cz.senslog.watchdog.messagebroker.writer.DelimiterTableWriter;
+import cz.senslog.watchdog.messagebroker.writer.TableWriter;
 import org.apache.logging.log4j.LogManager;
 import org.apache.logging.log4j.Logger;
 
@@ -10,11 +14,10 @@ public class ConsoleMessageBroker implements MessageBroker {
 
     @Override
     public void send(Report report, MessageBrokerHandler status) {
-        logger.info("Sending a report created at {} to the console.", report.getCreated());
-        logger.info("Record({}): {};{};{}", "?", "unitId", "sensorId", "lastObservation");
-        for (int i = 0; i < report.getRecords().size(); i++) {
-            logger.info("Record({}): {}", i+1, report.getRecords().get(i).getMessage());
-        }
+        TableWriter tableWriter = DelimiterTableWriter.create(";")
+                .cell("unitId").cell("sensorId").cell("lastObservation").end();
+        report.getReports().stream().map(SimpleReport::getRecord).forEach(r -> r.writeRow(tableWriter.row()));
+        logger.info("Sending a report created at {} to the console.\n{}", report.getCreated(), tableWriter.table());
         status.handle(new MessageStatus(report));
     }
 
@@ -22,4 +25,4 @@ public class ConsoleMessageBroker implements MessageBroker {
     public MessageBrokerType getType() {
         return MessageBrokerType.CONSOLE;
     }
-}
+}

+ 2 - 0
src/main/java/cz/senslog/watchdog/messagebroker/MessageBroker.java

@@ -3,6 +3,8 @@ package cz.senslog.watchdog.messagebroker;
 import cz.senslog.watchdog.config.EmailMessageBrokerConfig;
 import cz.senslog.watchdog.config.MessageBrokerConfig;
 import cz.senslog.watchdog.config.MessageBrokerType;
+import cz.senslog.watchdog.domain.Report;
+import cz.senslog.watchdog.messagebroker.email.EmailMessageBroker;
 
 public interface MessageBroker {
 

+ 2 - 0
src/main/java/cz/senslog/watchdog/messagebroker/MessageStatus.java

@@ -1,5 +1,7 @@
 package cz.senslog.watchdog.messagebroker;
 
+import cz.senslog.watchdog.domain.Report;
+
 public class MessageStatus {
 
     private final Report report;

+ 0 - 27
src/main/java/cz/senslog/watchdog/messagebroker/Report.java

@@ -1,27 +0,0 @@
-package cz.senslog.watchdog.messagebroker;
-
-import cz.senslog.watchdog.provider.Record;
-
-import java.time.Instant;
-import java.util.List;
-
-public class Report {
-
-    private final Instant created;
-
-    private final List<Record> records;
-
-    public Report(Instant created, List<Record> records) {
-        this.created = created;
-        this.records = records;
-    }
-
-    public Instant getCreated() {
-        return created;
-    }
-
-    public List<Record> getRecords() {
-        return records;
-    }
-
-}

+ 36 - 36
src/main/java/cz/senslog/watchdog/messagebroker/EmailMessageBroker.java → src/main/java/cz/senslog/watchdog/messagebroker/email/EmailMessageBroker.java

@@ -1,7 +1,16 @@
-package cz.senslog.watchdog.messagebroker;
+package cz.senslog.watchdog.messagebroker.email;
 
 import cz.senslog.watchdog.config.EmailMessageBrokerConfig;
 import cz.senslog.watchdog.config.MessageBrokerType;
+import cz.senslog.watchdog.domain.ObservationInfo;
+import cz.senslog.watchdog.domain.SimpleReport;
+import cz.senslog.watchdog.domain.StatusReport;
+import cz.senslog.watchdog.messagebroker.MessageBroker;
+import cz.senslog.watchdog.messagebroker.MessageBrokerHandler;
+import cz.senslog.watchdog.messagebroker.MessageStatus;
+import cz.senslog.watchdog.domain.Report;
+import cz.senslog.watchdog.messagebroker.writer.HtmlTableWriter;
+import cz.senslog.watchdog.messagebroker.writer.TableWriter;
 import cz.senslog.watchdog.provider.Record;
 import org.apache.logging.log4j.LogManager;
 import org.apache.logging.log4j.Logger;
@@ -9,8 +18,11 @@ import org.apache.logging.log4j.Logger;
 import javax.mail.*;
 import javax.mail.internet.*;
 import java.time.Instant;
+import java.time.format.DateTimeFormatter;
 import java.util.*;
 
+import static cz.senslog.watchdog.util.TimeConverter.dayToSec;
+import static java.time.format.DateTimeFormatter.ofPattern;
 import static javax.mail.Message.RecipientType.TO;
 
 public class EmailMessageBroker implements MessageBroker {
@@ -22,13 +34,10 @@ public class EmailMessageBroker implements MessageBroker {
     private final EmailMessageBrokerConfig config;
     private final Session session;
 
-    private final Set<Record> sendRecords;
-
 
     public EmailMessageBroker(EmailMessageBrokerConfig config) {
         this.config = config;
         this.session = openSession(config);
-        this.sendRecords = new HashSet<>();
     }
 
     private Session openSession(EmailMessageBrokerConfig config) {
@@ -47,7 +56,7 @@ public class EmailMessageBroker implements MessageBroker {
         return Session.getInstance(props, auth);
     }
 
-    private Message createMessage(Instant created, List<Record> records) throws MessagingException {
+    private Message createMessage(Report report) throws MessagingException {
         Message message = new MimeMessage(session);
         message.setFrom(new InternetAddress(config.getSender()));
         message.setRecipients(TO, InternetAddress.parse(config.getRecipient()));
@@ -55,18 +64,26 @@ public class EmailMessageBroker implements MessageBroker {
 
         MimeBodyPart mimeBodyPart = new MimeBodyPart();
         StringBuilder content = new StringBuilder();
-        content.append(String.format("Data reported at the time <b>%s</b>", created))
-                .append(BREAK_LINE).append(BREAK_LINE);
-
-        content.append("<b>") // TODO refactor
-                .append("unitId").append(";")
-                .append("sensorId").append(";")
-                .append("lastObservation")
-                .append("</b>").append(BREAK_LINE);
 
-        for (Record record : records) {
-            content.append(record.getMessage()).append(BREAK_LINE);
+        TableWriter tableWriter = HtmlTableWriter.createWithHeader("width: 100%;", "background-color: #dddddd")
+                .cell("unitId").cell("sensorId").cell("timestamp").cell("reported").cell("status").end();
+
+        String reportedTime = report.getCreated().format(ofPattern("yyyy-MM-dd HH:mm:ss"));
+        final String rowStyle = "border: 1px solid #dddddd; text-align: left; padding: 8px;";
+        for (SimpleReport simpleReport : report.getReports()) {
+            if (simpleReport.getRecord() instanceof ObservationInfo) {
+                ObservationInfo observation = (ObservationInfo)simpleReport.getRecord();
+                tableWriter.row(rowStyle + (simpleReport.getStatus().equals(StatusReport.OK) ? "background-color: #CCFFCC" : "background-color: #FFCCCC"))
+                            .cell(String.valueOf(observation.getSource().getUnitId()), rowStyle)
+                            .cell(String.valueOf(observation.getSource().getSensorId()), rowStyle)
+                            .cell(observation.getTimestamp().toString(), rowStyle)
+                            .cell(reportedTime, rowStyle)
+                            .cell(simpleReport.getStatus().name(), rowStyle)
+                        .end();
+            }
         }
+
+        content.append(tableWriter.table()).append(BREAK_LINE);
         mimeBodyPart.setContent(content.toString(), "text/html");
 
         Multipart multipart = new MimeMultipart();
@@ -76,16 +93,6 @@ public class EmailMessageBroker implements MessageBroker {
         return message;
     }
 
-    private List<Record> prepareRecords(Report report) {
-        List<Record> readyToSend = new ArrayList<>();
-        for (Record record : report.getRecords()) {
-            if (!sendRecords.contains(record)) {
-                readyToSend.add(record);
-            }
-        }
-        return readyToSend;
-    }
-
     @Override
     public void send(Report report, MessageBrokerHandler status) {
         if (report == null) {
@@ -93,25 +100,18 @@ public class EmailMessageBroker implements MessageBroker {
             status.handle(new MessageStatus(null, "No report to send.")); return;
         }
 
-        List<Record> recordsToSend = prepareRecords(report);
-        if (recordsToSend.isEmpty()) {
-            logger.info("The same report was already send.");
-            status.handle(new MessageStatus(report)); return;
-        }
-
         try {
-            Message message = createMessage(report.getCreated(), recordsToSend);
+            Message message = createMessage(report);
             logger.info("Sending a message via email.");
             Transport.send(message);
             logger.info("The message was send successfully.");
-            sendRecords.addAll(recordsToSend);
             status.handle(new MessageStatus(report));
         } catch (MessagingException e) {
             logger.catching(e);
             status.handle(new MessageStatus(report, e.getMessage()));
         }
 
-        vanish();
+      //  vanish();
     }
 
     @Override
@@ -120,9 +120,9 @@ public class EmailMessageBroker implements MessageBroker {
     }
 
     public Record[] vanish() {
-        Instant deadline = Instant.now().minusSeconds(60 * 60 * 24 * config.getVanishPeriodDays());
+        Instant deadline = Instant.now().minusSeconds(dayToSec(config.getVanishPeriodDays()));
         List<Record> deletedRecords = new ArrayList<>();
-        Iterator<Record> iterator = sendRecords.iterator();
+        Iterator<Record> iterator = Collections.emptyListIterator();
         while(iterator.hasNext()) {
             Record record = iterator.next();
             if (record.getTimestamp().isBefore(deadline)) {

+ 7 - 0
src/main/java/cz/senslog/watchdog/messagebroker/writer/AbstractWriter.java

@@ -0,0 +1,7 @@
+package cz.senslog.watchdog.messagebroker.writer;
+
+public interface AbstractWriter {
+
+
+
+}

+ 65 - 0
src/main/java/cz/senslog/watchdog/messagebroker/writer/DelimiterTableWriter.java

@@ -0,0 +1,65 @@
+package cz.senslog.watchdog.messagebroker.writer;
+
+public class DelimiterTableWriter implements TableWriter, TableRowWriter {
+
+    private final String delimiter;
+    private final StringBuilder tableBuilder;
+    private int rowCounter, cellCounter;
+
+    public static DelimiterTableWriter create(String delimiter) {
+        return new DelimiterTableWriter(delimiter);
+    }
+
+    private DelimiterTableWriter(String delimiter) {
+        this.delimiter = delimiter;
+        this.tableBuilder = new StringBuilder();
+        this.rowCounter = 0;
+        this.cellCounter = 0;
+    }
+
+    @Override
+    public TableRowWriter row() {
+        if (rowCounter > 0) {
+            tableBuilder.append("\n");
+        }
+        return this;
+    }
+
+    @Override
+    public TableRowWriter row(String style) {
+        return row();
+    }
+
+    @Override
+    public TableWriter emptyRow() {
+        // TODO implement
+        return this;
+    }
+
+    @Override
+    public String table() {
+        return tableBuilder.toString();
+    }
+
+    @Override
+    public TableRowWriter cell(CharSequence sequence) {
+        if (cellCounter > 0) {
+            tableBuilder.append(delimiter);
+        }
+        tableBuilder.append(sequence);
+        cellCounter++;
+        return this;
+    }
+
+    @Override
+    public TableRowWriter cell(CharSequence sequence, String style) {
+        return cell(sequence);
+    }
+
+    @Override
+    public TableWriter end() {
+        cellCounter = 0;
+        rowCounter++;
+        return this;
+    }
+}

+ 112 - 0
src/main/java/cz/senslog/watchdog/messagebroker/writer/HtmlTableWriter.java

@@ -0,0 +1,112 @@
+package cz.senslog.watchdog.messagebroker.writer;
+
+public class HtmlTableWriter implements TableWriter {
+
+    private final StringBuilder tableBuilder;
+
+    private int rowCounter;
+    private int rowCellCounter;
+
+    public static TableWriter create() {
+        return create("");
+    }
+
+    public static TableWriter create(String style) {
+        return new HtmlTableWriter(style);
+    }
+
+    public static TableHeaderWriter createWithHeader(String tableStyle) {
+        return createWithHeader(tableStyle, "");
+    }
+
+    public static TableHeaderWriter createWithHeader() {
+        return createWithHeader("", "");
+    }
+
+    public static TableHeaderWriter createWithHeader(String tableStyle, String headerStyle) {
+        return new HtmlTableRowWriter(new HtmlTableWriter(tableStyle), "th", headerStyle);
+    }
+
+    private HtmlTableWriter(String style) {
+        this.tableBuilder = new StringBuilder(String.format("<table style=\"%s\">", style));
+        this.rowCounter = 0;
+        this.rowCellCounter = -1;
+    }
+
+    @Override
+    public TableRowWriter row() {
+        return row("");
+    }
+
+    @Override
+    public TableRowWriter row(String style) {
+        return new HtmlTableRowWriter(this, "td", style);
+    }
+
+    @Override
+    public TableWriter emptyRow() {
+        if (rowCounter <= 0) {
+            throw new RuntimeException("Empty row can not be the first row in the table.");
+        }
+
+        TableRowWriter rowWriter = row();
+        for (int i = 0; i < rowCellCounter; i++) {
+            rowWriter.cell("");
+        }
+        return rowWriter.end();
+    }
+
+    @Override
+    public String table() {
+        tableBuilder.append("</table>");
+        return tableBuilder.toString();
+    }
+
+    private static class HtmlTableRowWriter implements TableRowWriter, TableHeaderWriter {
+
+        private final HtmlTableWriter tableWriter;
+        private final StringBuilder rowBuilder;
+        private final String rowElement;
+
+        private int localRowCellCounter;
+
+        private HtmlTableRowWriter(HtmlTableWriter tableWriter, String rowElement, String style) {
+            this.tableWriter = tableWriter;
+            this.rowBuilder = new StringBuilder(String.format("<tr style=\"%s\">", style));
+            this.rowElement = rowElement;
+            this.localRowCellCounter = 0;
+        }
+
+
+        @Override
+        public TableRowWriter cell(CharSequence sequence) {
+            rowBuilder.append(String.format("<%s>%s</%s>", rowElement, sequence, rowElement));
+            localRowCellCounter++;
+            return this;
+        }
+
+        @Override
+        public TableRowWriter cell(CharSequence sequence, String style) {
+            rowBuilder.append(String.format("<%s style=\"%s\">%s</%s>", rowElement, style, sequence, rowElement));
+            localRowCellCounter++;
+            return this;        }
+
+        @Override
+        public TableWriter end() {
+            rowBuilder.append("</tr>");
+            tableWriter.tableBuilder.append(rowBuilder);
+
+            if (tableWriter.rowCellCounter < 0) {
+                tableWriter.rowCellCounter = localRowCellCounter;
+            }
+
+            if (tableWriter.rowCellCounter != localRowCellCounter) {
+                throw new RuntimeException("Inserted row does not contain the same length of already inserted rows.");
+            }
+
+            tableWriter.rowCounter++;
+
+            return tableWriter;
+        }
+    }
+}

+ 4 - 0
src/main/java/cz/senslog/watchdog/messagebroker/writer/TableHeaderWriter.java

@@ -0,0 +1,4 @@
+package cz.senslog.watchdog.messagebroker.writer;
+
+public interface TableHeaderWriter extends TableRowWriter {
+}

+ 9 - 0
src/main/java/cz/senslog/watchdog/messagebroker/writer/TableRowWriter.java

@@ -0,0 +1,9 @@
+package cz.senslog.watchdog.messagebroker.writer;
+
+public interface TableRowWriter {
+
+    TableRowWriter cell(CharSequence sequence);
+    TableRowWriter cell(CharSequence sequence, String style);
+    TableWriter end();
+
+}

+ 9 - 0
src/main/java/cz/senslog/watchdog/messagebroker/writer/TableWriter.java

@@ -0,0 +1,9 @@
+package cz.senslog.watchdog.messagebroker.writer;
+
+public interface TableWriter {
+
+    TableRowWriter row();
+    TableRowWriter row(String style);
+    TableWriter emptyRow();
+    String table();
+}

+ 10 - 4
src/main/java/cz/senslog/watchdog/provider/Record.java

@@ -1,17 +1,23 @@
 package cz.senslog.watchdog.provider;
 
-
+import cz.senslog.watchdog.domain.Source;
+import cz.senslog.watchdog.messagebroker.writer.TableHeaderWriter;
+import cz.senslog.watchdog.messagebroker.writer.TableRowWriter;
+import cz.senslog.watchdog.messagebroker.writer.TableWriter;
 import cz.senslog.watchdog.util.Tuple;
 
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.io.Writer;
 import java.time.Instant;
 
 public interface Record {
 
-    Tuple<String, String> getSource();
+    Source getSource();
 
     Instant getTimestamp();
 
-    String getMessage();
-
     boolean isValid(long interval, Instant now);
+
+    void writeRow(TableRowWriter writer);
 }

+ 1 - 1
src/main/java/cz/senslog/watchdog/provider/database/DatabaseDataProvider.java

@@ -2,7 +2,7 @@ package cz.senslog.watchdog.provider.database;
 
 import cz.senslog.watchdog.config.DatabaseConfig;
 import cz.senslog.watchdog.provider.DataProvider;
-import cz.senslog.watchdog.provider.ObservationInfo;
+import cz.senslog.watchdog.domain.ObservationInfo;
 import cz.senslog.watchdog.provider.Record;
 import org.jdbi.v3.core.Jdbi;
 

+ 1 - 1
src/main/java/cz/senslog/watchdog/provider/database/SensLogRepository.java

@@ -1,6 +1,6 @@
 package cz.senslog.watchdog.provider.database;
 
-import cz.senslog.watchdog.provider.ObservationInfo;
+import cz.senslog.watchdog.domain.ObservationInfo;
 import org.jdbi.v3.core.Jdbi;
 
 import java.time.OffsetDateTime;

+ 3 - 1
src/main/java/cz/senslog/watchdog/provider/ws/WebServiceDataProvider.java

@@ -3,7 +3,7 @@ package cz.senslog.watchdog.provider.ws;
 import com.google.gson.reflect.TypeToken;
 import cz.senslog.watchdog.config.WebServiceConfig;
 import cz.senslog.watchdog.provider.DataProvider;
-import cz.senslog.watchdog.provider.ObservationInfo;
+import cz.senslog.watchdog.domain.ObservationInfo;
 import cz.senslog.watchdog.provider.Record;
 import cz.senslog.watchdog.util.http.HttpClient;
 import cz.senslog.watchdog.util.http.HttpRequest;
@@ -43,8 +43,10 @@ public class WebServiceDataProvider implements DataProvider {
                         .addParam("user", config.getUser())
                         .build()
                 ).build();
+        logger.info("Getting new data from the server: {}.", request.getUrl());
 
         HttpResponse response = httpClient.send(request);
+        logger.info("Received data with the status '{}' from the server {}.", response.getStatus(), request.getUrl());
 
         if (response.isOk()) {
             String jsonBody = response.getBody();

+ 137 - 0
src/main/java/cz/senslog/watchdog/util/DateTrunc.java

@@ -0,0 +1,137 @@
+package cz.senslog.watchdog.util;
+
+import java.time.LocalDateTime;
+import java.time.OffsetDateTime;
+import java.util.Arrays;
+import java.util.function.BiConsumer;
+
+
+public final class DateTrunc {
+
+    public enum Option {
+        SECOND  (1),
+        MINUTE  (60),
+        HOUR    (3_600),
+        DAY     (86_400),
+        WEEK    (604_800),
+        MONTH   (2_629_743), // 2 629 743.83
+        YEAR    (31_556_926)
+
+        ;
+        private final int sec;
+        Option(int sec) {
+            this.sec = sec;
+        }
+        public final int getSeconds() { return sec; }
+    }
+
+    private DateTrunc() {}
+
+    public static OffsetDateTime trunc(OffsetDateTime dateTime, int period) {
+        if (dateTime == null) { return null; }
+        return OffsetDateTime.of(trunc(dateTime.toLocalDateTime(), period), dateTime.getOffset());
+    }
+
+    public static OffsetDateTime trunc(OffsetDateTime dateTime, Option option) {
+        if (dateTime == null) { return null; }
+        return OffsetDateTime.of(trunc(dateTime.toLocalDateTime(), option), dateTime.getOffset());
+    }
+
+    public static LocalDateTime trunc(LocalDateTime dateTime, int period) {
+        return trunc(dateTime, PeriodUtils.disassemble(period));
+    }
+
+    public static LocalDateTime trunc(LocalDateTime dateTime, Option option) {
+        return trunc(dateTime, PeriodUtils.disassemble(option));
+    }
+
+    private static LocalDateTime trunc(LocalDateTime dateTime, int[] optionComponents) {
+        if (optionComponents == null || optionComponents.length != Option.values().length) {
+            return dateTime;
+        }
+
+        LocalDateTime result = dateTime.withNano(0);
+        for (Option option : Option.values()) {
+            int value = optionComponents[option.ordinal()];
+            if (value == -1) { continue; }
+            switch (option) {
+                case YEAR: {
+                    result = result.withYear(value == 0 ? 0 : result.getYear() - (result.getYear() % value));
+                } break;
+                case MONTH: {
+                    result = result.withMonth(value == 0 ? 1 : result.getMonthValue() - (result.getMonthValue() % value));
+                } break;
+                case WEEK: {
+                    // TODO: implement
+                } break;
+                case DAY: {
+                    result = result.withDayOfMonth(value == 0 ? 1 : result.getDayOfMonth() - (result.getDayOfMonth() % value));
+                } break;
+                case HOUR: {
+                    result = result.withHour(value == 0 ? 0 : result.getHour() - (result.getHour() % value));
+                } break;
+                case MINUTE: {
+                    result = result.withMinute(value == 0 ? 0 : result.getMinute() - (result.getMinute() % value));
+                } break;
+                case SECOND: {
+                    result = result.withSecond(value == 0 ? 0 : result.getSecond() - (result.getSecond() % value));
+                } break;
+            }
+        }
+        return result;
+    }
+
+    private static class PeriodUtils {
+
+        private static int[] disassemble(Option option) {
+            int [] cmp = new int[Option.values().length];
+            Arrays.fill(cmp, -1);
+            if (option == null) {
+                return cmp;
+            }
+            Arrays.fill(cmp, 0, option.ordinal(), 0);
+            cmp[option.ordinal()] = 1;
+            return cmp;
+        }
+
+
+        private static int[] disassemble(int period) {
+            int [] cmp = new int[Option.values().length];
+            BiConsumer<Option, Double> setValue = (o, v) -> cmp[o.ordinal()] = v.intValue() == 0 ? -1 : v.intValue();
+            double periodSec = Math.min(period, Option.YEAR.sec);
+
+            double years = periodSec / Option.YEAR.sec;
+            setValue.accept(Option.YEAR, years);
+            periodSec %= Option.YEAR.sec;
+
+            double months =  round((years - (int)years) * 12.0, 5);
+            setValue.accept(Option.MONTH, months);
+
+            double weeksOfYears = periodSec / Option.WEEK.sec;
+            setValue.accept(Option.WEEK, 0.0); // TODO
+
+            double days = (periodSec % Option.WEEK.sec) / Option.DAY.sec;
+            setValue.accept(Option.DAY, days);
+
+            double hours = (days - (int)days) * 24.0;
+            setValue.accept(Option.HOUR, hours);
+
+            double minutes = (hours - (int)hours) * 60.0;
+            setValue.accept(Option.MINUTE, minutes);
+
+            double seconds = (minutes - (int)minutes) * 60.0;
+            setValue.accept(Option.SECOND, seconds);
+
+            for (int i = 0; i < cmp.length; i++) {
+                if (cmp[i] > 0) { break; }
+                cmp[i] = 0;
+            }
+
+            return cmp;
+        }
+    }
+
+    public static double round(double value, int scale) {
+        return Math.round(value * Math.pow(10, scale)) / Math.pow(10, scale);
+    }
+}

+ 14 - 0
src/main/java/cz/senslog/watchdog/util/TimeConverter.java

@@ -0,0 +1,14 @@
+package cz.senslog.watchdog.util;
+
+public final class TimeConverter {
+
+    private TimeConverter() {}
+
+    public static long dayToSec(long day) {
+        return 86_400 * day;
+    }
+
+    public static long secToMillis(long sec) {
+        return 1_000 * sec;
+    }
+}

+ 28 - 22
src/main/java/cz/senslog/watchdog/util/schedule/ScheduleTask.java

@@ -1,33 +1,45 @@
 package cz.senslog.watchdog.util.schedule;
 
-import java.time.LocalTime;
+
+import java.time.LocalDateTime;
 import java.time.temporal.ChronoUnit;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.ScheduledExecutorService;
 import java.util.concurrent.ScheduledFuture;
 
-import static java.util.concurrent.TimeUnit.SECONDS;
+import static cz.senslog.watchdog.util.TimeConverter.secToMillis;
+import static java.time.LocalTime.now;
+import static java.util.concurrent.TimeUnit.MILLISECONDS;
 
 public final class ScheduleTask {
 
-    private static final int DELAY = 2;
+    private static final int DEFAULT_DELAY_MILLIS = 2_000; // 2s
 
     private TaskDescription description;
     private final Runnable task;
-    private final long period;
-    private final LocalTime startAt;
+    private final long periodMillis;
 
-//    public ScheduleTask(String name, Runnable task, long period) {
-//        this.description = new TaskDescription(name, Status.STOPPED);
-//        this.task = task;
-//        this.period = period;
-//    }
+    private final LocalDateTime startAt;
+    private final long delayMillis;
 
-    public ScheduleTask(String name, Runnable task, long period, LocalTime startAt) {
+    private ScheduleTask(String name, Runnable task, long periodSec, LocalDateTime startAt, long delaySec) {
         this.description = new TaskDescription(name, Status.STOPPED);
         this.task = task;
-        this.period = period;
+        this.periodMillis = secToMillis(periodSec);
         this.startAt = startAt;
+        this.delayMillis = secToMillis(delaySec);
+    }
+
+    public ScheduleTask(String name, Runnable task, long periodSec, LocalDateTime startAt) {
+        this(name, task, periodSec, startAt, -1);
+    }
+
+    public ScheduleTask(String name, Runnable task, long periodSec, int delaySec) {
+        this(name, task, periodSec, null, delaySec);
+    }
+
+    public ScheduleTask(String name, Runnable task, long periodSec) {
+        this(name, task, periodSec, null);
     }
 
     public TaskDescription getDescription() {
@@ -38,19 +50,13 @@ public final class ScheduleTask {
         return task;
     }
 
-    public long getPeriod() {
-        return period;
-    }
-
-    public LocalTime getStartAt() {
-        return startAt;
+    public long getPeriodMillis() {
+        return periodMillis;
     }
 
     public void schedule(ScheduledExecutorService scheduledService, CountDownLatch latch) {
-        long initialDelay = LocalTime.now().until(getStartAt(), ChronoUnit.SECONDS);
-        System.out.println("Task will run in " + initialDelay + " seconds.");
-
-        ScheduledFuture<?> future = scheduledService.scheduleAtFixedRate(getTask(), initialDelay, getPeriod(), SECONDS);
+        long delay = startAt != null ? now().until(startAt, ChronoUnit.MILLIS) : delayMillis > 0 ? delayMillis : DEFAULT_DELAY_MILLIS;
+        ScheduledFuture<?> future = scheduledService.scheduleAtFixedRate(task, delay, periodMillis, MILLISECONDS);
         description = new TaskDescription(description.getName(), Status.RUNNING);
         new Thread(() -> {
             try {

+ 9 - 2
src/main/java/cz/senslog/watchdog/util/schedule/Scheduler.java

@@ -1,5 +1,6 @@
 package cz.senslog.watchdog.util.schedule;
 
+import java.time.LocalDateTime;
 import java.time.LocalTime;
 import java.util.Set;
 
@@ -17,8 +18,14 @@ public interface Scheduler {
 
     interface SchedulerBuilder {
 
-        SchedulerBuilder addTask(String name, Runnable task, long period, LocalTime startAt);
-        SchedulerBuilder addTask(Runnable task, long period, LocalTime startAt);
+        SchedulerBuilder addTask(String name, Runnable task, long period);
+        SchedulerBuilder addTask(Runnable task, long period);
+
+        SchedulerBuilder addTask(String name, Runnable task, long period, int delaySec);
+        SchedulerBuilder addTask(Runnable task, long period, int delaySec);
+
+        SchedulerBuilder addTask(String name, Runnable task, long period, LocalDateTime startAt);
+        SchedulerBuilder addTask(Runnable task, long period, LocalDateTime startAt);
 
         Scheduler build();
     }

+ 27 - 2
src/main/java/cz/senslog/watchdog/util/schedule/SchedulerBuilderImpl.java

@@ -1,5 +1,6 @@
 package cz.senslog.watchdog.util.schedule;
 
+import java.time.LocalDateTime;
 import java.time.LocalTime;
 import java.util.HashSet;
 import java.util.Set;
@@ -13,13 +14,37 @@ public class SchedulerBuilderImpl implements Scheduler.SchedulerBuilder {
     }
 
     @Override
-    public Scheduler.SchedulerBuilder addTask(String name, Runnable task, long period, LocalTime startAt) {
+    public Scheduler.SchedulerBuilder addTask(String name, Runnable task, long period) {
+        tasks.add(new ScheduleTask(name, task, period));
+        return this;
+    }
+
+    @Override
+    public Scheduler.SchedulerBuilder addTask(Runnable task, long period) {
+        tasks.add(new ScheduleTask(task.getClass().getSimpleName(), task, period));
+        return this;
+    }
+
+    @Override
+    public Scheduler.SchedulerBuilder addTask(String name, Runnable task, long period, int delaySec) {
+        tasks.add(new ScheduleTask(name, task, period, delaySec));
+        return this;
+    }
+
+    @Override
+    public Scheduler.SchedulerBuilder addTask(Runnable task, long period, int delaySec) {
+        tasks.add(new ScheduleTask(task.getClass().getSimpleName(), task, period, delaySec));
+        return this;
+    }
+
+    @Override
+    public Scheduler.SchedulerBuilder addTask(String name, Runnable task, long period, LocalDateTime startAt) {
         tasks.add(new ScheduleTask(name, task, period, startAt));
         return this;
     }
 
     @Override
-    public Scheduler.SchedulerBuilder addTask(Runnable task, long period, LocalTime startAt) {
+    public Scheduler.SchedulerBuilder addTask(Runnable task, long period, LocalDateTime startAt) {
         tasks.add(new ScheduleTask(task.getClass().getSimpleName(), task, period, startAt));
         return this;
     }

+ 14 - 0
src/test/java/cz/senslog/watchdog/messagebroker/email/EmailMessageBrokerTest.java

@@ -0,0 +1,14 @@
+package cz.senslog.watchdog.messagebroker.email;
+
+import org.junit.jupiter.api.Test;
+
+import java.io.IOException;
+
+class EmailMessageBrokerTest {
+
+    @Test
+    void send() throws IOException {
+
+
+    }
+}

+ 31 - 0
src/test/java/cz/senslog/watchdog/messagebroker/writer/HtmlTableWriterTest.java

@@ -0,0 +1,31 @@
+package cz.senslog.watchdog.messagebroker.writer;
+
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+class HtmlTableWriterTest {
+
+    @Test
+    void create() {
+
+        TableWriter tableWriter = HtmlTableWriter.createWithHeader()
+                    .cell("Firstname")
+                    .cell("Lastname")
+                    .cell("Age")
+                .end()
+                .row()
+                    .cell("Jill")
+                    .cell("Smith")
+                    .cell("50")
+                .end()
+                .emptyRow()
+                .row()
+                    .cell("Eve")
+                    .cell("Jackson")
+                    .cell("94")
+                .end();
+
+        System.out.println(tableWriter.table());
+    }
+}

+ 20 - 0
src/test/java/cz/senslog/watchdog/util/schedule/SchedulerTest.java

@@ -0,0 +1,20 @@
+package cz.senslog.watchdog.util.schedule;
+
+import org.junit.jupiter.api.Test;
+
+import java.time.LocalDateTime;
+import java.time.LocalTime;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+class SchedulerTest {
+
+    @Test
+    void start() {
+
+//        Scheduler scheduler = Scheduler.createBuilder()
+//                    .addTask(() -> System.out.println(LocalDateTime.now()), 5, LocalTime.of(9, 58, 10))
+//                .build();
+//        scheduler.start();
+    }
+}