Forráskód Böngészése

Connector for Soils Sount

Lukas Cerny 1 éve
szülő
commit
29120536a2
38 módosított fájl, 1259 hozzáadás és 42 törlés
  1. 34 0
      config/soilscountToSenslog.yaml
  2. 0 7
      connector-fetch-alapans/src/main/java/cz/senslog/connector/fetch/alapans/Main.java
  3. 20 0
      connector-fetch-fofr/pom.xml
  4. 0 0
      connector-fetch-senslog-telemetry/src/main/java/cz/senslog/connector/fetch/senslog/telemetry/ConnectorFetchSenslogTelemetryProvider.java
  5. 12 0
      connector-fetch-senslog-telemetry/src/main/java/cz/senslog/connector/fetch/senslog/telemetry/SensLogTelemetryConfig.java
  6. 35 13
      connector-fetch-senslog-telemetry/src/main/java/cz/senslog/connector/fetch/senslog/telemetry/SensLogTelemetryFetcher.java
  7. 0 0
      connector-fetch-senslog-telemetry/src/main/java/cz/senslog/connector/fetch/senslog/telemetry/SensLogTelemetryProxySession.java
  8. 0 0
      connector-fetch-senslog-telemetry/src/main/java/cz/senslog/connector/fetch/senslog/telemetry/SensLogTelemetrySession.java
  9. 6 3
      connector-fetch-senslog-telemetry/src/test/java/cz/senslog/connector/fetch/senslog/telemetry/SensLogTelemetryFetcherTest.java
  10. 33 0
      connector-fetch-soilscount/pom.xml
  11. 43 0
      connector-fetch-soilscount/src/main/java/cz/senslog/connector/fetch/soilscount/ConnectorFetchSoilscountProvider.java
  12. 36 0
      connector-fetch-soilscount/src/main/java/cz/senslog/connector/fetch/soilscount/ProxySession.java
  13. 22 0
      connector-fetch-soilscount/src/main/java/cz/senslog/connector/fetch/soilscount/SessionModel.java
  14. 82 0
      connector-fetch-soilscount/src/main/java/cz/senslog/connector/fetch/soilscount/SoilScountConfig.java
  15. 223 0
      connector-fetch-soilscount/src/main/java/cz/senslog/connector/fetch/soilscount/SoilScountFetcher.java
  16. 1 0
      connector-fetch-soilscount/src/main/resources/META-INF/services/cz.senslog.connector.fetch.api.ConnectorFetchProvider
  17. 39 0
      connector-fetch-soilscount/src/test/java/cz/senslog/connector/fetch/soilscount/SoilScountFetcherTest.java
  18. 7 0
      connector-model/src/main/java/cz/senslog/connector/model/config/PropertyConfig.java
  19. 18 0
      connector-model/src/main/java/cz/senslog/connector/model/converter/FofrModelTelemetryModelConverter.java
  20. 2 0
      connector-model/src/main/java/cz/senslog/connector/model/converter/ModelConverterProvider.java
  21. 94 0
      connector-model/src/main/java/cz/senslog/connector/model/converter/SoilscountSenslogV1Converter.java
  22. 21 0
      connector-model/src/main/java/cz/senslog/connector/model/fofr/FofrModel.java
  23. 6 0
      connector-model/src/main/java/cz/senslog/connector/model/fofr/ParcelUpdate.java
  24. 25 0
      connector-model/src/main/java/cz/senslog/connector/model/soilscount/Device.java
  25. 88 0
      connector-model/src/main/java/cz/senslog/connector/model/soilscount/Measurement.java
  26. 26 0
      connector-model/src/main/java/cz/senslog/connector/model/soilscount/SoilscountModel.java
  27. 19 0
      connector-model/src/main/java/cz/senslog/connector/model/telemetry/TelemetryModel.java
  28. 20 0
      connector-push-telemetry/pom.xml
  29. 33 12
      connector-tools/src/main/java/cz/senslog/connector/tools/http/HttpClient.java
  30. 1 1
      connector-tools/src/main/java/cz/senslog/connector/tools/http/HttpContentType.java
  31. 62 0
      connector-tools/src/main/java/cz/senslog/connector/tools/jwt/JWClaim.java
  32. 49 0
      connector-tools/src/main/java/cz/senslog/connector/tools/jwt/JWTDecoder.java
  33. 25 0
      connector-tools/src/main/java/cz/senslog/connector/tools/jwt/JWTHeader.java
  34. 79 0
      connector-tools/src/main/java/cz/senslog/connector/tools/jwt/JWTPayload.java
  35. 32 0
      connector-tools/src/main/java/cz/senslog/connector/tools/jwt/JWToken.java
  36. 11 0
      connector-tools/src/main/java/cz/senslog/connector/tools/util/Encoding.java
  37. 33 6
      docker-compose.yaml
  38. 22 0
      pom.xml

+ 34 - 0
config/soilscountToSenslog.yaml

@@ -0,0 +1,34 @@
+settings:
+  - SoilScount: # name of the fetcher module, e.g., Demo
+      name: "Soil Scount"
+      provider: "cz.senslog.connector.fetch.soilscount.ConnectorFetchSoilscountProvider"
+
+      startDate: "2024-11-10T00:00:00"
+      period: 1  # period in hours
+
+      authUrl: "https://www.soilscouts.fi/api/v1/auth/login//" # keep the // (cos the bad implementation of the resource-side)
+      refreshUrl: "https://www.soilscouts.fi/api/v1/auth/token/refresh//" # keep the //
+      devicesUrl: "https://www.soilscouts.fi/api/v1/devices/"
+      measurementsUrl: "https://www.soilscouts.fi/api/v2/measurements/"
+
+      auth:
+        "username": "mkepka@kgm.zcu.cz"
+        "password": "Asense2023"
+
+      allowedDevices: [28779, 28780, 28781, 28782, 28783, 28784, 28785, 28786]
+
+  - SenslogV1:
+      name: "Senslog V1"
+      provider: "cz.senslog.connector.push.rest.senslog.v1.SenslogV1ConnectorPushProvider"
+      baseUrl: "https://sensor.lesprojekt.cz/senslog15"
+      auth:
+        username: "watchdog"
+        password: "HAFhaf"
+
+connectors:
+  - SoilScountSenslogV1:
+      fetcher: "SoilScount"
+      pusher: "SenslogV1"
+      period: 1200          # 86_400 = 24h
+#      startAt: "02:30:00" # hh:mm:ss  # non-mandatory attribute
+      initDelay: 5        # non-mandatory attribute

+ 0 - 7
connector-fetch-alapans/src/main/java/cz/senslog/connector/fetch/alapans/Main.java

@@ -1,7 +0,0 @@
-package cz.senslog.connector.fetch.alapans;
-
-public class Main {
-    public static void main(String[] args) {
-        System.out.println("Hello world!");
-    }
-}

+ 20 - 0
connector-fetch-fofr/pom.xml

@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+    <parent>
+        <groupId>cz.senslog</groupId>
+        <artifactId>connector-period</artifactId>
+        <version>1.0-SNAPSHOT</version>
+    </parent>
+
+    <artifactId>connector-fetch-fofr</artifactId>
+
+    <properties>
+        <maven.compiler.source>17</maven.compiler.source>
+        <maven.compiler.target>17</maven.compiler.target>
+        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+    </properties>
+
+</project>

+ 0 - 0
connector-fetch-senslog-telemetry/src/main/java/cz/senslog/fetch/senslog/telemetry/ConnectorFetchSenslogTelemetryProvider.java → connector-fetch-senslog-telemetry/src/main/java/cz/senslog/connector/fetch/senslog/telemetry/ConnectorFetchSenslogTelemetryProvider.java


+ 12 - 0
connector-fetch-senslog-telemetry/src/main/java/cz/senslog/fetch/senslog/telemetry/SensLogTelemetryConfig.java → connector-fetch-senslog-telemetry/src/main/java/cz/senslog/connector/fetch/senslog/telemetry/SensLogTelemetryConfig.java

@@ -3,6 +3,8 @@ package cz.senslog.fetch.senslog.telemetry;
 import cz.senslog.connector.model.config.DefaultConfig;
 
 import java.time.LocalDateTime;
+import java.util.Collections;
+import java.util.Set;
 
 public class SensLogTelemetryConfig {
 
@@ -12,6 +14,7 @@ public class SensLogTelemetryConfig {
     private final int campaignId;
     private final int limit;
     private final int interval;
+    private final Set<Long> allowedStations;
 
     SensLogTelemetryConfig(DefaultConfig defaultConfig) {
         this.baseUrl = defaultConfig.getStringProperty("baseUrl");
@@ -26,6 +29,11 @@ public class SensLogTelemetryConfig {
         this.campaignId = defaultConfig.getIntegerProperty("campaignId");
         this.limit = defaultConfig.getIntegerProperty("limit");
         this.interval = defaultConfig.getIntegerProperty("interval");
+        if (defaultConfig.containsProperty("allowedStations")) {
+            this.allowedStations = defaultConfig.getSetProperty("allowedStations", Long.class);
+        } else {
+            this.allowedStations = Collections.emptySet();
+        }
     }
 
     public String getBaseUrl() {
@@ -51,4 +59,8 @@ public class SensLogTelemetryConfig {
     public int getInterval() {
         return interval;
     }
+
+    public Set<Long> getAllowedStations() {
+        return allowedStations;
+    }
 }

+ 35 - 13
connector-fetch-senslog-telemetry/src/main/java/cz/senslog/fetch/senslog/telemetry/SensLogTelemetryFetcher.java → connector-fetch-senslog-telemetry/src/main/java/cz/senslog/connector/fetch/senslog/telemetry/SensLogTelemetryFetcher.java

@@ -99,19 +99,40 @@ public class SensLogTelemetryFetcher implements ConnectorFetcher<SensLogTelemetr
             }
         }
 
-        HttpRequest request = HttpRequest.newBuilder().GET()
-                .url(URLBuilder.newBuilder(config.getBaseUrl(), String.format("campaigns/%d/units/observations", config.getCampaignId()))
-                        .addParam("limit", config.getLimit())
-                        .addParam("format", "geojson")
-                        .addParam("from", session.getFromTime().format(DateTimeFormatter.ISO_OFFSET_DATE_TIME))
-                        .addParam("to", session.getToTime().format(DateTimeFormatter.ISO_OFFSET_DATE_TIME))
-                        .addParam("zone", "UTC")
-                        .addParam("offset", session.getOffset())
-                        .addParam("navigationLinks", "false")
-                        .build())
-                .header(ACCEPT, APPLICATION_GEO_JSON)
-                .header(AUTHORIZATION, "Bearer " + config.getBearerToken())
-                .build();
+        HttpRequest request;
+        if (config.getAllowedStations().isEmpty()) {
+
+            request = HttpRequest.newBuilder().GET()
+                    .url(URLBuilder.newBuilder(config.getBaseUrl(), String.format("campaigns/%d/units/observations", config.getCampaignId()))
+                            .addParam("limit", config.getLimit())
+                            .addParam("format", "geojson")
+                            .addParam("from", session.getFromTime().format(DateTimeFormatter.ISO_OFFSET_DATE_TIME))
+                            .addParam("to", session.getToTime().format(DateTimeFormatter.ISO_OFFSET_DATE_TIME))
+                            .addParam("zone", "UTC")
+                            .addParam("offset", session.getOffset())
+                            .addParam("navigationLinks", "false")
+                            .build())
+                    .header(ACCEPT, APPLICATION_GEO_JSON)
+                    .header(AUTHORIZATION, "Bearer " + config.getBearerToken())
+                    .build();
+        } else {
+
+            long unitId = config.getAllowedStations().iterator().next();
+
+            request = HttpRequest.newBuilder().GET()
+                    .url(URLBuilder.newBuilder(config.getBaseUrl(), String.format("campaigns/%d/units/%d/observations", config.getCampaignId(), unitId))
+                            .addParam("limit", config.getLimit())
+                            .addParam("format", "geojson")
+                            .addParam("from", session.getFromTime().format(DateTimeFormatter.ISO_OFFSET_DATE_TIME))
+                            .addParam("to", session.getToTime().format(DateTimeFormatter.ISO_OFFSET_DATE_TIME))
+                            .addParam("zone", "UTC")
+                            .addParam("offset", session.getOffset())
+                            .addParam("navigationLinks", "false")
+                            .build())
+                    .header(ACCEPT, APPLICATION_GEO_JSON)
+                    .header(AUTHORIZATION, "Bearer " + config.getBearerToken())
+                    .build();
+        }
 
         HttpResponse response = httpClient.send(request);
 
@@ -140,6 +161,7 @@ public class SensLogTelemetryFetcher implements ConnectorFetcher<SensLogTelemetr
         }
 
         if (geoSize <= 0) {
+            logger.warn("Retrieved zero data within the interval of " + session.getFromTime() + " to " + session.getToTime());
             return GeoJsonModel.empty();
         }
 

+ 0 - 0
connector-fetch-senslog-telemetry/src/main/java/cz/senslog/fetch/senslog/telemetry/SensLogTelemetryProxySession.java → connector-fetch-senslog-telemetry/src/main/java/cz/senslog/connector/fetch/senslog/telemetry/SensLogTelemetryProxySession.java


+ 0 - 0
connector-fetch-senslog-telemetry/src/main/java/cz/senslog/fetch/senslog/telemetry/SensLogTelemetrySession.java → connector-fetch-senslog-telemetry/src/main/java/cz/senslog/connector/fetch/senslog/telemetry/SensLogTelemetrySession.java


+ 6 - 3
connector-fetch-senslog-telemetry/src/test/java/cz/senslog/fetch/senslog/telemetry/SensLogTelemetryFetcherTest.java → connector-fetch-senslog-telemetry/src/test/java/cz/senslog/connector/fetch/senslog/telemetry/SensLogTelemetryFetcherTest.java

@@ -6,6 +6,8 @@ import cz.senslog.connector.model.config.DefaultConfig;
 import cz.senslog.connector.tools.http.HttpClient;
 import org.junit.jupiter.api.Test;
 
+import java.util.Collections;
+
 import static org.junit.jupiter.api.Assertions.*;
 
 class SensLogTelemetryFetcherTest {
@@ -15,11 +17,12 @@ class SensLogTelemetryFetcherTest {
 
         DefaultConfig defaultConfig = new DefaultConfig("", null);
         defaultConfig.setProperty("baseUrl", "https://theros.wirelessinfo.cz/");
-        defaultConfig.setProperty("startAt", "2023-08-25T00:00:00+00:00");
+        defaultConfig.setProperty("startAt", "2024-06-23T20:00:00+00:00");
         defaultConfig.setProperty("bearerToken", "#123");
-        defaultConfig.setProperty("campaignId", 2);
-        defaultConfig.setProperty("limit", 10);
+        defaultConfig.setProperty("campaignId", 4);
+        defaultConfig.setProperty("limit", 200);
         defaultConfig.setProperty("interval", 12);
+        defaultConfig.setProperty("allowedStations", Collections.singletonList(1305167563014004L));
 
         SensLogTelemetryConfig config = new SensLogTelemetryConfig(defaultConfig);
         SensLogTelemetryFetcher fetcher = new SensLogTelemetryFetcher(config, HttpClient.newHttpSSLClient());

+ 33 - 0
connector-fetch-soilscount/pom.xml

@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+    <parent>
+        <groupId>cz.senslog</groupId>
+        <artifactId>connector-period</artifactId>
+        <version>1.0-SNAPSHOT</version>
+    </parent>
+
+    <artifactId>connector-fetch-soilscount</artifactId>
+    <name>fetch-soilscount</name>
+    <packaging>jar</packaging>
+
+    <dependencies>
+        <dependency>
+            <groupId>cz.senslog</groupId>
+            <artifactId>connector-fetch-api</artifactId>
+            <version>${project.parent.version}</version>
+        </dependency>
+    </dependencies>
+
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-assembly-plugin</artifactId>
+            </plugin>
+        </plugins>
+    </build>
+
+</project>

+ 43 - 0
connector-fetch-soilscount/src/main/java/cz/senslog/connector/fetch/soilscount/ConnectorFetchSoilscountProvider.java

@@ -0,0 +1,43 @@
+package cz.senslog.connector.fetch.soilscount;
+
+import cz.senslog.connector.fetch.api.ExecutableFetcher;
+import cz.senslog.connector.model.api.AbstractModel;
+import cz.senslog.connector.model.config.DefaultConfig;
+import cz.senslog.connector.model.soilscount.SoilscountModel;
+import cz.senslog.connector.tools.http.HttpClient;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+import static cz.senslog.connector.tools.http.HttpClient.newHttpClient;
+
+public class ConnectorFetchSoilscountProvider implements cz.senslog.connector.fetch.api.ConnectorFetchProvider {
+
+    private static final Logger logger = LogManager.getLogger(ConnectorFetchSoilscountProvider.class);
+
+
+    @Override
+    public ExecutableFetcher<? extends AbstractModel> createExecutableFetcher(DefaultConfig defaultConfig) {
+        logger.info("Initialization a new fetch provider {}.", ConnectorFetchSoilscountProvider.class.getSimpleName());
+
+        logger.debug("Creating a new configuration.");
+        SoilScountConfig config = new SoilScountConfig(defaultConfig);
+        logger.info("Configuration for {} was created successfully.", SoilScountFetcher.class.getSimpleName());
+
+
+        logger.debug("Creating a new instance of {}.", SoilScountFetcher.class);
+        SoilScountFetcher fetcher = new SoilScountFetcher(config, HttpClient.newHttpSSLClient());
+        logger.info("Fetcher for {} was created successfully.", SoilScountFetcher.class.getSimpleName());
+
+        ExecutableFetcher<SoilscountModel> executor = ExecutableFetcher.create(fetcher);
+        logger.info("Fetcher executor for {} was created successfully.", SoilScountFetcher.class.getSimpleName());
+
+        {
+            logger.debug("Creating a new instance of {}.", ProxySession.class);
+            ProxySession proxySession = new ProxySession(fetcher, newHttpClient());
+            logger.info("Fetcher session for {} was created successfully.", ProxySession.class);
+            executor = ExecutableFetcher.createWithProxySession(proxySession);
+        }
+
+        return executor;
+    }
+}

+ 36 - 0
connector-fetch-soilscount/src/main/java/cz/senslog/connector/fetch/soilscount/ProxySession.java

@@ -0,0 +1,36 @@
+package cz.senslog.connector.fetch.soilscount;
+
+import cz.senslog.connector.fetch.api.ConnectorFetcher;
+import cz.senslog.connector.fetch.api.FetchProxySession;
+import cz.senslog.connector.model.soilscount.SoilscountModel;
+import cz.senslog.connector.tools.http.HttpClient;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+import java.util.Optional;
+
+public class ProxySession extends FetchProxySession<SessionModel, SoilscountModel> {
+
+    private static final Logger logger = LogManager.getLogger(ProxySession.class);
+
+    private final HttpClient httpClient;
+
+    public ProxySession(ConnectorFetcher<SessionModel, SoilscountModel> fetcher, HttpClient httpClient) {
+        super(fetcher);
+        this.httpClient = httpClient;
+    }
+
+    @Override
+    protected SessionModel preProcessing(Optional<SessionModel> previousSession) {
+        SessionModel session = previousSession.filter(SessionModel::isActive).orElseGet(() -> new SessionModel(true));
+
+        // TODO
+
+        return session;
+    }
+
+    @Override
+    protected void postProcessing(Optional<SoilscountModel> model, Optional<SessionModel> session) {
+
+    }
+}

+ 22 - 0
connector-fetch-soilscount/src/main/java/cz/senslog/connector/fetch/soilscount/SessionModel.java

@@ -0,0 +1,22 @@
+package cz.senslog.connector.fetch.soilscount;
+
+import cz.senslog.connector.model.api.ProxySessionModel;
+
+import java.time.OffsetDateTime;
+
+public class SessionModel extends ProxySessionModel {
+
+    private OffsetDateTime startAt;
+
+    public SessionModel(boolean isActive) {
+        super(isActive);
+    }
+
+    public OffsetDateTime getStartAt() {
+        return startAt;
+    }
+
+    public void setStartAt(OffsetDateTime startAt) {
+        this.startAt = startAt;
+    }
+}

+ 82 - 0
connector-fetch-soilscount/src/main/java/cz/senslog/connector/fetch/soilscount/SoilScountConfig.java

@@ -0,0 +1,82 @@
+package cz.senslog.connector.fetch.soilscount;
+
+import cz.senslog.connector.model.config.DefaultConfig;
+import cz.senslog.connector.model.config.PropertyConfig;
+
+import java.time.LocalDateTime;
+import java.util.Set;
+
+public class SoilScountConfig {
+
+    public static class AuthConfig {
+
+        private final String username;
+        private final String password;
+
+        private AuthConfig(PropertyConfig config) {
+            this.username = config.getStringProperty("username");
+            this.password = config.getStringProperty("password");
+        }
+
+        public String getUsername() {
+            return username;
+        }
+
+        public String getPassword() {
+            return password;
+        }
+    }
+
+    private final LocalDateTime startDate;
+    private final int period;
+    private final Set<Integer> allowedDevices;
+    private final String authUrl;
+    private final String refreshUrl;
+    private final String devicesUrl;
+    private final String measurementsUrl;
+    private final AuthConfig authConfig;
+
+    SoilScountConfig(DefaultConfig defaultConfig) {
+        this.startDate = defaultConfig.getLocalDateTimeProperty("startDate");
+        this.period = defaultConfig.getIntegerProperty("period");
+        this.allowedDevices = defaultConfig.getSetProperty("allowedDevices", Integer.class);
+        this.authUrl = defaultConfig.getStringProperty("authUrl");
+        this.refreshUrl = defaultConfig.getStringProperty("refreshUrl");
+        this.devicesUrl = defaultConfig.getStringProperty("devicesUrl");
+        this.measurementsUrl = defaultConfig.getStringProperty("measurementsUrl");
+        this.authConfig = new AuthConfig(defaultConfig.getPropertyConfig("auth"));
+
+    }
+
+    public LocalDateTime getStartDate() {
+        return startDate;
+    }
+
+    public int getPeriod() {
+        return period;
+    }
+
+    public String getAuthUrl() {
+        return authUrl;
+    }
+
+    public String getRefreshUrl() {
+        return refreshUrl;
+    }
+
+    public String getDevicesUrl() {
+        return devicesUrl;
+    }
+
+    public String getMeasurementsUrl() {
+        return measurementsUrl;
+    }
+
+    public AuthConfig getAuthConfig() {
+        return authConfig;
+    }
+
+    public Set<Integer> getAllowedDevices() {
+        return allowedDevices;
+    }
+}

+ 223 - 0
connector-fetch-soilscount/src/main/java/cz/senslog/connector/fetch/soilscount/SoilScountFetcher.java

@@ -0,0 +1,223 @@
+package cz.senslog.connector.fetch.soilscount;
+
+import cz.senslog.connector.fetch.api.ConnectorFetcher;
+import cz.senslog.connector.model.soilscount.Device;
+import cz.senslog.connector.model.soilscount.Measurement;
+import cz.senslog.connector.model.soilscount.SoilscountModel;
+import cz.senslog.connector.tools.http.*;
+import cz.senslog.connector.tools.json.BasicJson;
+import cz.senslog.connector.tools.jwt.JWTDecoder;
+import cz.senslog.connector.tools.jwt.JWToken;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+import java.time.*;
+import java.time.format.DateTimeFormatter;
+import java.util.*;
+
+import static cz.senslog.connector.tools.http.HttpContentType.APPLICATION_JSON;
+import static cz.senslog.connector.tools.json.BasicJson.jsonToObject;
+import static cz.senslog.connector.tools.json.BasicJson.objectToJson;
+import static java.lang.String.format;
+import static java.time.Instant.ofEpochSecond;
+
+public class SoilScountFetcher implements ConnectorFetcher<SessionModel, SoilscountModel> {
+
+    private static final Logger logger = LogManager.getLogger(SoilScountFetcher.class);
+
+    private final SoilScountConfig config;
+    private final HttpClient httpClient;
+
+    private AuthTokens authTokens;
+    private List<Device> devices;
+    private String devicesAsParam;
+
+    private static class AuthTokens {
+        private final JWToken access;
+        private final String refresh;
+
+        public AuthTokens(JWToken access, String refresh) {
+            this.access = access;
+            this.refresh = refresh;
+        }
+    }
+
+    public SoilScountFetcher() { this(null, null); }
+
+    public SoilScountFetcher(SoilScountConfig config, HttpClient httpClient) {
+        this.config = config;
+        this.httpClient = httpClient;
+    }
+
+    @Override
+    public void init() {
+        {
+            HttpRequest request = HttpRequest.newBuilder().POST()
+                    .url(URLBuilder.newBuilder(config.getAuthUrl()).build())
+                    .header(HttpHeader.ACCEPT, APPLICATION_JSON)
+                    .contentType(APPLICATION_JSON)
+                    .body(objectToJson(config.getAuthConfig()))
+                    .build();
+
+            HttpResponse response = httpClient.send(request);
+
+            if (response.isError()) {
+                throw logger.throwing(new IllegalStateException(format(
+                        "Can not login. %s", response.getBody()
+                )));
+            }
+
+            Map<String, String> tokensMap = BasicJson.<Map<String, String>>jsonToObject(response.getBody(), Map.class);
+            authTokens = new AuthTokens(JWTDecoder.decodeJWT(tokensMap.get("access")), tokensMap.get("refresh"));
+
+            if (authTokens.access ==  null) {
+                throw logger.throwing(new IllegalStateException("Authentication failed. No valid access token found."));
+            }
+        }
+
+        {
+            String accessToken = getAccessToken();
+            HttpRequest request = HttpRequest.newBuilder().GET()
+                    .url(URLBuilder.newBuilder(config.getDevicesUrl()).build())
+                    .header(HttpHeader.ACCEPT, APPLICATION_JSON)
+                    .header(HttpHeader.AUTHORIZATION, String.format("Bearer %s", accessToken))
+                    .build();
+
+            HttpResponse response = httpClient.send(request);
+
+            if (response.isError()) {
+                throw logger.throwing(new IllegalStateException(format(
+                        "Can not get devices. %s", response.getBody()
+                )));
+            }
+
+            List<?> devicesMap = jsonToObject(response.getBody(), List.class);
+            devices = new ArrayList<>(devicesMap.size());
+            for (Object d : devicesMap) {
+                if (d instanceof Map) {
+                    Map<?, ?> device = (Map<?, ?>) d;
+                    int deviceId = ((Double) device.get("id")).intValue();
+                    if (config.getAllowedDevices().contains(deviceId)) {
+                        devices.add(new Device(deviceId,
+                                ((Double) device.get("serial_number")).longValue(),
+                                (String) device.get("name")
+                        ));
+                    }
+                }
+            }
+            List<String> devAsStr = new ArrayList<>(devices.size());
+            for (Device d : devices) {
+                devAsStr.add(Long.toString(d.getId()));
+            }
+            devicesAsParam = String.join(",", devAsStr);
+        }
+    }
+
+    private String getAccessToken() {
+
+        if (ofEpochSecond(authTokens.access.getPayload().getExp()).isAfter(Instant.now())) {
+            return authTokens.access.getRaw();
+        }
+
+        Map<String, String> body = new HashMap<>();
+        body.put("refresh", authTokens.refresh);
+
+        HttpRequest request = HttpRequest.newBuilder().POST()
+                .url(URLBuilder.newBuilder(config.getRefreshUrl()).build())
+                .header(HttpHeader.ACCEPT, APPLICATION_JSON)
+                .contentType(APPLICATION_JSON)
+                .body(objectToJson(body))
+                .build();
+
+        HttpResponse response = httpClient.send(request);
+
+        if (response.isError()) {
+            logger.error("Can not refresh access token.");
+            return null;
+        }
+
+        Map<String, String> tokensMap = BasicJson.<Map<String, String>>jsonToObject(response.getBody(), Map.class);
+        authTokens = new AuthTokens(JWTDecoder.decodeJWT(tokensMap.get("access")), tokensMap.get("refresh"));
+
+        if (authTokens.access ==  null) {
+            logger.error("Refreshing token failed. No valid access token.");
+            return null;
+        }
+
+        return authTokens.access.getRaw();
+    }
+
+    @Override
+    public SoilscountModel fetch(Optional<SessionModel> sessionOpt) {
+        SessionModel session = sessionOpt.orElse(new SessionModel(false));
+
+        OffsetDateTime startAt = OffsetDateTime.of(config.getStartDate(), ZoneOffset.UTC);
+        if (session.getStartAt() != null && startAt.isBefore(session.getStartAt())) {
+            startAt = session.getStartAt();
+        }
+        OffsetDateTime endAt = startAt.plusHours(config.getPeriod());
+
+        String accessToken = getAccessToken();
+        HttpRequest request = HttpRequest.newBuilder().GET()
+                .url(URLBuilder.newBuilder(config.getMeasurementsUrl())
+                        .addParam("since", startAt.format(DateTimeFormatter.ISO_DATE_TIME))
+                        .addParam("until", endAt.format(DateTimeFormatter.ISO_DATE_TIME))
+                        .addParam("device", devicesAsParam)
+                        .build())
+                .header(HttpHeader.ACCEPT, APPLICATION_JSON)
+                .header(HttpHeader.AUTHORIZATION, String.format("Bearer %s", accessToken))
+                .build();
+
+        HttpResponse response = httpClient.send(request);
+
+        if (response.isError()) {
+            throw logger.throwing(new IllegalStateException(format(
+                    "Can not get new measurements. %s", response.getBody()
+            )));
+        }
+
+        Map<?, ?> measureMap = jsonToObject(response.getBody(), Map.class);
+        boolean hasNext = measureMap.get("next") != null;
+        List<?> results = (List<?>) measureMap.get("results");
+        List<Measurement> measurements = new ArrayList<>(results.size());
+
+        OffsetDateTime minTimestamp = OffsetDateTime.MAX;
+        OffsetDateTime maxTimestamp = OffsetDateTime.MIN;
+
+        for (Object r : results) {
+            if (r instanceof Map) {
+                Map<?, ?> m = (Map<?, ?>) r;
+                Measurement measurement = (new Measurement(
+                        OffsetDateTime.parse((String) m.get("timestamp")),
+                        ((Double) m.get("device")).longValue(),
+                        (Double)m.get("temperature"),
+                        (Double)m.get("moisture"),
+                        (Double)m.get("conductivity"),
+                        (Double)m.get("dielectricity"),
+                        ((Double)m.get("site")).intValue(),
+                        (Double)m.get("salinity"),
+                        (Double)m.get("field_capacity"),
+                        (Double)m.get("wilting_point"),
+                        (Double)m.get("water_balance"),
+                        (Double)m.get("oxygen")
+                ));
+                measurements.add(measurement);
+
+                if (measurement.getTimestamp().isAfter(maxTimestamp)) {
+                    maxTimestamp = measurement.getTimestamp();
+                } else if (measurement.getTimestamp().isBefore(minTimestamp)) {
+                    minTimestamp = measurement.getTimestamp();
+                }
+            }
+        }
+
+        OffsetDateTime now = OffsetDateTime.now();
+        if (hasNext || endAt.isBefore(now)) {
+            session.setStartAt(endAt);
+        } else if (endAt.isAfter(now)) {
+            session.setStartAt(maxTimestamp);
+        }
+
+        return new SoilscountModel(devices, measurements, minTimestamp, maxTimestamp);
+    }
+}

+ 1 - 0
connector-fetch-soilscount/src/main/resources/META-INF/services/cz.senslog.connector.fetch.api.ConnectorFetchProvider

@@ -0,0 +1 @@
+cz.senslog.connector.fetch.soilscount.ConnectorFetchSoilscountProvider

+ 39 - 0
connector-fetch-soilscount/src/test/java/cz/senslog/connector/fetch/soilscount/SoilScountFetcherTest.java

@@ -0,0 +1,39 @@
+package cz.senslog.connector.fetch.soilscount;
+
+import cz.senslog.connector.model.config.DefaultConfig;
+import cz.senslog.connector.tools.http.HttpClient;
+import org.junit.jupiter.api.Test;
+
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Optional;
+
+class SoilScountFetcherTest {
+
+    @Test
+    void fetch() throws Exception {
+
+        DefaultConfig defaultConfig = new DefaultConfig("id", null);
+        defaultConfig.setProperty("startDate", "2024-11-08T00:00:00");
+        defaultConfig.setProperty("period", 12);
+        defaultConfig.setProperty("allowedDevices", new HashSet<>(Arrays.asList(28779, 28780, 28781, 28782, 28783, 28784, 28785, 28786)));
+        defaultConfig.setProperty("authUrl", "https://www.soilscouts.fi/api/v1/auth/login//");
+        defaultConfig.setProperty("refreshUrl", "https://www.soilscouts.fi/api/v1/auth/login//");
+        defaultConfig.setProperty("devicesUrl", "https://www.soilscouts.fi/api/v1/devices");
+        defaultConfig.setProperty("measurementsUrl", "https://www.soilscouts.fi/api/v2/measurements");
+        defaultConfig.setProperty("auth", new HashMap<String, String>(){{
+            put("username", "mkepka@kgm.zcu.cz");
+            put("password", "Asense2023");
+        }});
+
+
+        SoilScountFetcher fetcher = new SoilScountFetcher(new SoilScountConfig(defaultConfig), HttpClient.newHttpClient());
+
+        fetcher.init();
+        Optional<SessionModel> session = Optional.of(new SessionModel(true));
+        for (int i = 0; i < 10; i++) {
+            fetcher.fetch(session);
+        }
+    }
+}

+ 7 - 0
connector-model/src/main/java/cz/senslog/connector/model/config/PropertyConfig.java

@@ -210,6 +210,13 @@ public class PropertyConfig {
                 res.add(ClassUtils.cast(o, type));
             }
             return res;
+        } else if (value instanceof Set) {
+            Set<?> set = (Set<?>) value;
+            Set<T> res = new HashSet<>(set.size());
+            for (Object o : set) {
+                res.add(ClassUtils.cast(o, type));
+            }
+            return res;
         }
 
         return emptySet();

+ 18 - 0
connector-model/src/main/java/cz/senslog/connector/model/converter/FofrModelTelemetryModelConverter.java

@@ -0,0 +1,18 @@
+package cz.senslog.connector.model.converter;
+
+import cz.senslog.connector.model.api.Converter;
+import cz.senslog.connector.model.fofr.FofrModel;
+import cz.senslog.connector.model.fofr.ParcelUpdate;
+import cz.senslog.connector.model.logger.LoggerModel;
+import cz.senslog.connector.model.telemetry.TelemetryModel;
+
+import java.util.Arrays;
+
+public class FofrModelTelemetryModelConverter implements Converter<FofrModel, TelemetryModel> {
+
+    @Override
+    public TelemetryModel convert(FofrModel model) {
+        String body = Arrays.toString(model.getUpdates().toArray(new ParcelUpdate[0]));
+        return new TelemetryModel(body, model.getFrom(), model.getTo());
+    }
+}

+ 2 - 0
connector-model/src/main/java/cz/senslog/connector/model/converter/ModelConverterProvider.java

@@ -29,5 +29,7 @@ public class ModelConverterProvider extends ConverterProvider {
         register(Senslog1ModelAnalyticsModelConverter.class);
         register(GeoJsonTransientConverter.class);
         register(DemoModelLoggerModelConverter.class);
+        register(FofrModelTelemetryModelConverter.class);
+        register(SoilscountSenslogV1Converter.class);
     }
 }

+ 94 - 0
connector-model/src/main/java/cz/senslog/connector/model/converter/SoilscountSenslogV1Converter.java

@@ -0,0 +1,94 @@
+package cz.senslog.connector.model.converter;
+
+import cz.senslog.connector.model.api.Converter;
+import cz.senslog.connector.model.soilscount.Measurement;
+import cz.senslog.connector.model.soilscount.SoilscountModel;
+import cz.senslog.connector.model.v1.Observation;
+import cz.senslog.connector.model.v1.Record;
+import cz.senslog.connector.model.v1.SenslogV1Model;
+
+import java.time.OffsetDateTime;
+import java.util.*;
+import java.util.function.Function;
+
+public class SoilscountSenslogV1Converter implements Converter<SoilscountModel, SenslogV1Model> {
+
+    private static final Map<Long, Function<Measurement, List<Observation>>> DEVICE_TO_UNIT_ID;
+
+    private static final int NM_OF_SENSORS;
+
+    static {
+        NM_OF_SENSORS = 3;
+        DEVICE_TO_UNIT_ID = new HashMap<>();
+        DEVICE_TO_UNIT_ID.put(28779L, (u) -> Arrays.asList(
+                new Observation(){{setUnitId(428024647L); setSensorId(410240030L); setValue(u.getMoisture());      }},
+                new Observation(){{setUnitId(428024647L); setSensorId(340620030L); setValue(u.getTemperature());   }},
+                new Observation(){{setUnitId(428024647L); setSensorId(580040030L); setValue(u.getOxygen());        }}
+        ));
+
+        DEVICE_TO_UNIT_ID.put(28780L, (u) -> Arrays.asList(
+                new Observation(){{setUnitId(428024656L); setSensorId(410240015L); setValue(u.getMoisture());      }},
+                new Observation(){{setUnitId(428024656L); setSensorId(340620015L); setValue(u.getTemperature());   }},
+                new Observation(){{setUnitId(428024656L); setSensorId(580040015L); setValue(u.getOxygen());        }}
+        ));
+
+        DEVICE_TO_UNIT_ID.put(28781L, (u) -> Arrays.asList(
+                new Observation(){{setUnitId(428024699L); setSensorId(410240030L); setValue(u.getMoisture());      }},
+                new Observation(){{setUnitId(428024699L); setSensorId(340620030L); setValue(u.getTemperature());   }},
+                new Observation(){{setUnitId(428024699L); setSensorId(570060030L); setValue(u.getSalinity());        }}
+        ));
+
+        DEVICE_TO_UNIT_ID.put(28782L, (u) -> Arrays.asList(
+                new Observation(){{setUnitId(428024700L); setSensorId(410240015L); setValue(u.getMoisture());      }},
+                new Observation(){{setUnitId(428024700L); setSensorId(340620015L); setValue(u.getTemperature());   }},
+                new Observation(){{setUnitId(428024700L); setSensorId(570060015L); setValue(u.getSalinity());      }}
+        ));
+
+        DEVICE_TO_UNIT_ID.put(28783L, (u) -> Arrays.asList(
+                new Observation(){{setUnitId(428024701L); setSensorId(410240030L); setValue(u.getMoisture());      }},
+                new Observation(){{setUnitId(428024701L); setSensorId(340620030L); setValue(u.getTemperature());   }},
+                new Observation(){{setUnitId(428024701L); setSensorId(570060030L); setValue(u.getSalinity());      }}
+        ));
+
+        DEVICE_TO_UNIT_ID.put(28784L, (u) -> Arrays.asList(
+                new Observation(){{setUnitId(428024702L); setSensorId(410240015L); setValue(u.getMoisture());      }},
+                new Observation(){{setUnitId(428024702L); setSensorId(340620015L); setValue(u.getTemperature());   }},
+                new Observation(){{setUnitId(428024702L); setSensorId(570060015L); setValue(u.getSalinity());      }}
+        ));
+
+        DEVICE_TO_UNIT_ID.put(28785L, (u) -> Arrays.asList(
+                new Observation(){{setUnitId(428024703L); setSensorId(410240030L); setValue(u.getMoisture());      }},
+                new Observation(){{setUnitId(428024703L); setSensorId(340620030L); setValue(u.getTemperature());   }},
+                new Observation(){{setUnitId(428024703L); setSensorId(570060030L); setValue(u.getSalinity());      }}
+        ));
+
+        DEVICE_TO_UNIT_ID.put(28786L, (u) -> Arrays.asList(
+                new Observation(){{setUnitId(428024704L); setSensorId(410240015L); setValue(u.getMoisture());       }},
+                new Observation(){{setUnitId(428024704L); setSensorId(340620015L); setValue(u.getTemperature());    }},
+                new Observation(){{setUnitId(428024704L); setSensorId(570060015L); setValue(u.getSalinity());       }}
+        ));
+    }
+
+    @Override
+    public SenslogV1Model convert(SoilscountModel inputModel) {
+        if (inputModel == null) { return null; }
+        if (inputModel.getMeasurement() == null || inputModel.getMeasurement().isEmpty()) {
+            return new SenslogV1Model(Collections.emptyList(), inputModel.getFrom(), inputModel.getTo());
+        }
+
+        List<Record> observations = new ArrayList<>(inputModel.getMeasurement().size() * NM_OF_SENSORS);
+        for (Measurement measurement : inputModel.getMeasurement()) {
+            if (!DEVICE_TO_UNIT_ID.containsKey(measurement.getDeviceId())) { continue; }
+
+            OffsetDateTime timestamp = measurement.getTimestamp();
+            for (Observation observation : DEVICE_TO_UNIT_ID.get(measurement.getDeviceId()).apply(measurement)) {
+                if (observation.getValue() != null) {
+                    observation.setTime(timestamp);
+                    observations.add(observation);
+                }
+            }
+        }
+
+        return new SenslogV1Model(observations, inputModel.getFrom(), inputModel.getTo());
+    }
+}

+ 21 - 0
connector-model/src/main/java/cz/senslog/connector/model/fofr/FofrModel.java

@@ -0,0 +1,21 @@
+package cz.senslog.connector.model.fofr;
+
+import cz.senslog.connector.model.api.AbstractModel;
+
+import java.time.OffsetDateTime;
+import java.util.List;
+
+public class FofrModel extends AbstractModel {
+
+    private final List<ParcelUpdate> updates;
+
+
+    public FofrModel(List<ParcelUpdate> updates, OffsetDateTime from, OffsetDateTime to) {
+        super(from, to);
+        this.updates = updates;
+    }
+
+    public List<ParcelUpdate> getUpdates() {
+        return updates;
+    }
+}

+ 6 - 0
connector-model/src/main/java/cz/senslog/connector/model/fofr/ParcelUpdate.java

@@ -0,0 +1,6 @@
+package cz.senslog.connector.model.fofr;
+
+public class ParcelUpdate {
+
+
+}

+ 25 - 0
connector-model/src/main/java/cz/senslog/connector/model/soilscount/Device.java

@@ -0,0 +1,25 @@
+package cz.senslog.connector.model.soilscount;
+
+public class Device {
+    private final long id;
+    private final long serialNumber;
+    private final String name;
+
+    public Device(long id, long serialNumber, String name) {
+        this.id = id;
+        this.serialNumber = serialNumber;
+        this.name = name;
+    }
+
+    public long getId() {
+        return id;
+    }
+
+    public long getSerialNumber() {
+        return serialNumber;
+    }
+
+    public String getName() {
+        return name;
+    }
+}

+ 88 - 0
connector-model/src/main/java/cz/senslog/connector/model/soilscount/Measurement.java

@@ -0,0 +1,88 @@
+package cz.senslog.connector.model.soilscount;
+
+import java.time.OffsetDateTime;
+
+public class Measurement {
+
+    private final OffsetDateTime timestamp;
+    private final long deviceId;
+    private final Double temperature;
+    private final Double moisture;
+    private final Double conductivity;
+    private final Double dielectricity;
+    private final Integer site;
+    private final Double salinity;
+    private final Double fieldCapacity;
+    private final Double wiltingPoint;
+    private final Double waterBalance;
+    private final Double oxygen;
+//    private final Double battery;
+
+    public Measurement(OffsetDateTime timestamp, long deviceId, Double temperature, Double moisture, Double conductivity, Double dielectricity, Integer site, Double salinity, Double fieldCapacity, Double wiltingPoint, Double waterBalance, Double oxygen) {
+        this.timestamp = timestamp;
+        this.deviceId = deviceId;
+        this.temperature = temperature;
+        this.moisture = moisture;
+        this.conductivity = conductivity;
+        this.dielectricity = dielectricity;
+        this.site = site;
+        this.salinity = salinity;
+        this.fieldCapacity = fieldCapacity;
+        this.wiltingPoint = wiltingPoint;
+        this.waterBalance = waterBalance;
+        this.oxygen = oxygen;
+//        this.battery = battery;
+    }
+
+    public OffsetDateTime getTimestamp() {
+        return timestamp;
+    }
+
+    public long getDeviceId() {
+        return deviceId;
+    }
+
+    public Double getTemperature() {
+        return temperature;
+    }
+
+    public Double getMoisture() {
+        return moisture;
+    }
+
+    public Double getConductivity() {
+        return conductivity;
+    }
+
+    public Double getDielectricity() {
+        return dielectricity;
+    }
+
+    public Integer getSite() {
+        return site;
+    }
+
+    public Double getSalinity() {
+        return salinity;
+    }
+
+    public Double getFieldCapacity() {
+        return fieldCapacity;
+    }
+
+    public Double getWiltingPoint() {
+        return wiltingPoint;
+    }
+
+    public Double getWaterBalance() {
+        return waterBalance;
+    }
+
+    public Double getOxygen() {
+        return oxygen;
+    }
+
+//    public Double getBattery() {
+//        return battery;
+//    }
+}

+ 26 - 0
connector-model/src/main/java/cz/senslog/connector/model/soilscount/SoilscountModel.java

@@ -0,0 +1,26 @@
+package cz.senslog.connector.model.soilscount;
+
+import cz.senslog.connector.model.api.AbstractModel;
+
+import java.time.OffsetDateTime;
+import java.util.List;
+
+public class SoilscountModel extends AbstractModel {
+
+    private final List<Device> devices;
+    private final List<Measurement> measurement;
+
+    public SoilscountModel(List<Device> devices, List<Measurement> measurement, OffsetDateTime from, OffsetDateTime to) {
+        super(from, to);
+        this.devices = devices;
+        this.measurement = measurement;
+    }
+
+    public List<Device> getDevices() {
+        return devices;
+    }
+
+    public List<Measurement> getMeasurement() {
+        return measurement;
+    }
+}

+ 19 - 0
connector-model/src/main/java/cz/senslog/connector/model/telemetry/TelemetryModel.java

@@ -0,0 +1,19 @@
+package cz.senslog.connector.model.telemetry;
+
+import cz.senslog.connector.model.api.AbstractModel;
+
+import java.time.OffsetDateTime;
+
+public class TelemetryModel extends AbstractModel {
+
+    private final String body;
+
+    public TelemetryModel(String body, OffsetDateTime from, OffsetDateTime to) {
+        super(from, to);
+        this.body = body;
+    }
+
+    public String getBody() {
+        return body;
+    }
+}

+ 20 - 0
connector-push-telemetry/pom.xml

@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+    <parent>
+        <groupId>cz.senslog</groupId>
+        <artifactId>connector-period</artifactId>
+        <version>1.0-SNAPSHOT</version>
+    </parent>
+
+    <artifactId>connector-push-telemetry</artifactId>
+
+    <properties>
+        <maven.compiler.source>17</maven.compiler.source>
+        <maven.compiler.target>17</maven.compiler.target>
+        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+    </properties>
+
+</project>

+ 33 - 12
connector-tools/src/main/java/cz/senslog/connector/tools/http/HttpClient.java

@@ -38,6 +38,7 @@ import java.io.InputStreamReader;
 import java.net.Socket;
 import java.net.URI;
 import java.net.URISyntaxException;
+import java.nio.charset.Charset;
 import java.security.KeyManagementException;
 import java.security.NoSuchAlgorithmException;
 import java.security.SecureRandom;
@@ -59,7 +60,9 @@ import static org.apache.hc.core5.http.HttpHeaders.*;
  */
 public class HttpClient {
 
-    /** Instance of http client. */
+    /**
+     * Instance of http client.
+     */
     private final org.apache.hc.client5.http.classic.HttpClient client;
     private final CookieStore cookieStore;
 
@@ -97,6 +100,7 @@ public class HttpClient {
 
     /**
      * Factory method to create a new instance of client.
+     *
      * @return new instance of {@code HttpClient}.
      */
     public static HttpClient newHttpClient() {
@@ -142,15 +146,23 @@ public class HttpClient {
 
     /**
      * Sends http request.
+     *
      * @param request - virtual request.
      * @return virtual response.
      */
     public HttpResponse send(HttpRequest request) {
+        return send(request, Charset.defaultCharset());
+    }
+
+    public HttpResponse send(HttpRequest request, Charset charset) {
         try {
             switch (request.getMethod()) {
-                case GET:  return sendGet(request);
-                case POST: return sendPost(request);
-                default: return HttpResponse.newBuilder()
+                case GET:
+                    return sendGet(request, charset);
+                case POST:
+                    return sendPost(request, charset);
+                default:
+                    return HttpResponse.newBuilder()
                             .body("Request does not contain method definition.")
                             .status(HttpCode.METHOD_NOT_ALLOWED).build();
             }
@@ -159,7 +171,7 @@ public class HttpClient {
                     .body(e.getMessage()).status(HttpCode.BAD_REQUEST)
                     .build();
         } catch (IOException e) {
-            return  HttpResponse.newBuilder()
+            return HttpResponse.newBuilder()
                     .body(e.getMessage()).status(HttpCode.SERVER_ERROR)
                     .build();
         }
@@ -167,12 +179,13 @@ public class HttpClient {
 
     /**
      * Sends GET request.
+     *
      * @param request - virtual request.
      * @return virtual response of the request.
      * @throws URISyntaxException throws if host url is not valid.
-     * @throws IOException throws if anything happen during sending.
+     * @throws IOException        throws if anything happen during sending.
      */
-    private HttpResponse sendGet(HttpRequest request) throws IOException, URISyntaxException {
+    private HttpResponse sendGet(HttpRequest request, Charset charset) throws IOException, URISyntaxException {
 
         URI uri = request.getUrl().toURI();
         HttpGet requestGet = new HttpGet(uri);
@@ -186,18 +199,19 @@ public class HttpClient {
         return client.execute(requestGet, res -> HttpResponse.newBuilder()
                 .status(res.getCode())
                 .headers(getHeaders(res))
-                .body(getBody(res.getEntity()))
+                .body(getBody(res.getEntity(), charset))
                 .build());
     }
 
     /**
      * Sends POST request.
+     *
      * @param request - virtual request.
      * @return virtual response of the request.
      * @throws URISyntaxException throws if host url is not valid.
-     * @throws IOException throws if anything happen during sending.
+     * @throws IOException        throws if anything happen during sending.
      */
-    private HttpResponse sendPost(HttpRequest request) throws URISyntaxException, IOException {
+    private HttpResponse sendPost(HttpRequest request, Charset charset) throws URISyntaxException, IOException {
 
         URI uri = request.getUrl().toURI();
         HttpPost requestPost = new HttpPost(uri);
@@ -212,12 +226,13 @@ public class HttpClient {
         return client.execute(requestPost, res -> HttpResponse.newBuilder()
                 .status(res.getCode())
                 .headers(getHeaders(res))
-                .body(getBody(res.getEntity()))
+                .body(getBody(res.getEntity(), charset))
                 .build());
     }
 
     /**
      * Sets basic headers to each request.
+     *
      * @param userRequest - virtual request.
      * @param httpRequest - real request prepared to send.
      */
@@ -233,6 +248,7 @@ public class HttpClient {
 
     /**
      * Returns map of headers from the response.
+     *
      * @param response - response message.
      * @return map of headers.
      */
@@ -246,14 +262,19 @@ public class HttpClient {
 
     /**
      * Returns body from the response.
+     *
      * @param entity - response entity.
      * @return string body of the response.
      * @throws IOException can not get body from the response.
      */
     private String getBody(HttpEntity entity) throws IOException {
+        return getBody(entity, Charset.defaultCharset());
+    }
+
+    private String getBody(HttpEntity entity, Charset charset) throws IOException {
         if (entity == null) return "";
         InputStream contentStream = entity.getContent();
-        InputStreamReader bodyStream = new InputStreamReader(contentStream);
+        InputStreamReader bodyStream = new InputStreamReader(contentStream, charset);
         BufferedReader rd = new BufferedReader(bodyStream);
         StringBuilder bodyBuffer = new StringBuilder();
         String line;

+ 1 - 1
connector-tools/src/main/java/cz/senslog/connector/tools/http/HttpContentType.java

@@ -4,7 +4,7 @@ public final class HttpContentType {
     public final static String APPLICATION_ATOM_XML         = "application/atom+xml";
     public final static String APPLICATION_FORM_URLENCODED  = "application/x-www-form-urlencoded";
     public final static String APPLICATION_JSON             = "application/json";
-    public final static String APPLICATION_GEO_JSON         = "application/geo+json";
+    public final static String APPLICATION_GEO_JSON         = "application/geojson";
     public final static String APPLICATION_OCTET_STREAM     = "application/octet-stream";
     public final static String APPLICATION_SVG_XML          = "application/svg+xml";
     public final static String APPLICATION_XHTML_XML        = "application/xhtml+xml";

+ 62 - 0
connector-tools/src/main/java/cz/senslog/connector/tools/jwt/JWClaim.java

@@ -0,0 +1,62 @@
+package cz.senslog.connector.tools.jwt;
+
+import java.util.Objects;
+
+public class JWClaim {
+
+    private final Object claim;
+
+    public JWClaim(Object claim) {
+        Objects.requireNonNull(claim);
+        this.claim = claim;
+    }
+
+    public long getAsLong() {
+        if (claim instanceof Long) {
+            return (Long) claim;
+        } else if (claim instanceof Double) {
+            return ((Double) claim).longValue();
+        } else if (claim instanceof Integer) {
+            return ((Integer) claim).longValue();
+        }
+        throw new ClassCastException("No long cast for the claim: " + claim);
+    }
+
+    public double getAsDouble() {
+        if (claim instanceof Long) {
+            return ((Long) claim).doubleValue();
+        } else if (claim instanceof Double) {
+            return (Double) claim;
+        } else if (claim instanceof Integer) {
+            return ((Integer) claim).doubleValue();
+        }
+        throw new ClassCastException("No double cast for the claim: " + claim);
+    }
+
+    public int getAsInt() {
+        if (claim instanceof Long) {
+            return ((Long) claim).intValue();
+        } else if (claim instanceof Double) {
+            return ((Double) claim).intValue();
+        } else if (claim instanceof Integer) {
+            return (Integer) claim;
+        }
+        throw new ClassCastException("No int cast for the claim: " + claim);
+    }
+
+    public boolean getAsBoolean() {
+        if (claim instanceof Boolean) {
+            return (Boolean) claim;
+        }
+        throw new ClassCastException("No boolean cast for the claim: " + claim);
+    }
+
+    public String getAsString() {
+        return claim.toString();
+    }
+
+    @Override
+    public String toString() {
+        return getAsString();
+    }
+}

+ 49 - 0
connector-tools/src/main/java/cz/senslog/connector/tools/jwt/JWTDecoder.java

@@ -0,0 +1,49 @@
+package cz.senslog.connector.tools.jwt;
+
+import cz.senslog.connector.tools.json.BasicJson;
+import cz.senslog.connector.tools.util.Tuple;
+
+import java.util.Base64;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+import static cz.senslog.connector.tools.json.BasicJson.jsonToObject;
+
+public class JWTDecoder {
+
+    static class Header {
+        String alg, typ;
+    }
+
+    public static JWToken decodeJWT(String jwtToken) {
+        try {
+            String[] parts = jwtToken.split("\\.");
+            if (parts.length != 3) {
+                throw new IllegalArgumentException("Invalid JWT token format.");
+            }
+
+            Header header = jsonToObject(new String(Base64.getUrlDecoder().decode(parts[0])), Header.class);
+            String decodedPayload = new String(Base64.getUrlDecoder().decode(parts[1]));
+            Map<String, JWClaim> payloadMap = BasicJson.<Map<String, Object>>jsonToObject(decodedPayload, Map.class)
+                    .entrySet().stream().map(e -> Tuple.of(e.getKey(), new JWClaim(e.getValue())))
+                    .collect(Collectors.toMap(Tuple::getItem1, Tuple::getItem2));
+            String signature = parts[2];
+
+            JWTPayload payload = new JWTPayload(
+                    payloadMap.containsKey("sub") ? payloadMap.remove("sub").getAsString() : null,
+                    payloadMap.containsKey("iss") ? payloadMap.remove("iss").getAsString() : null,
+                    payloadMap.containsKey("aud") ? payloadMap.remove("aud").getAsString() : null,
+                    payloadMap.containsKey("exp") ? payloadMap.remove("exp").getAsLong() : 0L ,
+                    payloadMap.containsKey("iat") ? payloadMap.remove("iat").getAsLong() : 0L ,
+                    payloadMap.containsKey("jti") ? payloadMap.remove("jti").getAsString() : null,
+                    payloadMap
+            );
+
+            return new JWToken(jwtToken, new JWTHeader(header), payload, signature);
+        } catch (Exception e) {
+            System.err.println("Error decoding JWT: " + e.getMessage());
+        }
+
+        return null;
+    }
+}

+ 25 - 0
connector-tools/src/main/java/cz/senslog/connector/tools/jwt/JWTHeader.java

@@ -0,0 +1,25 @@
+package cz.senslog.connector.tools.jwt;
+
+public class JWTHeader {
+
+    private final String alg;
+    private final String typ;
+
+    JWTHeader(JWTDecoder.Header header) {
+        this.alg = header.alg;
+        this.typ = header.typ;
+    }
+
+    public JWTHeader(String alg, String typ) {
+        this.alg = alg;
+        this.typ = typ;
+    }
+
+    public String getAlg() {
+        return alg;
+    }
+
+    public String getTyp() {
+        return typ;
+    }
+}

+ 79 - 0
connector-tools/src/main/java/cz/senslog/connector/tools/jwt/JWTPayload.java

@@ -0,0 +1,79 @@
+package cz.senslog.connector.tools.jwt;
+
+import java.util.Map;
+
+public class JWTPayload {
+
+    /** sub (subject): Subject of the JWT (the user) */
+    private final String sub;
+
+    /** iss (issuer): Issuer of the JWT */
+     private final String iss;
+
+     /** aud (audience): Recipient for which the JWT is intended */
+    private final String aud;
+
+    /** exp (expiration time): Time after which the JWT expires */
+    private final long exp;
+
+    /** iat (issued at time): Time at which the JWT was issued; can be used to determine age of the JWT */
+    private final long iat;
+
+    /** jti (JWT ID): Unique identifier; can be used to prevent the JWT from being replayed (allows a token to be used only once) */
+    private final String jti;
+
+    private final Map<String, JWClaim> claims;
+
+    public JWTPayload(String sub, String iss, String aud, long exp, long iat, String jti, Map<String, JWClaim> claims) {
+        this.sub = sub;
+        this.iss = iss;
+        this.aud = aud;
+        this.exp = exp;
+        this.iat = iat;
+        this.jti = jti;
+        this.claims = claims;
+    }
+
+    public String getSub() {
+        return sub;
+    }
+
+    public String getIss() {
+        return iss;
+    }
+
+    public String getAud() {
+        return aud;
+    }
+
+    public long getExp() {
+        return exp;
+    }
+
+    public long getIat() {
+        return iat;
+    }
+
+    public String getJti() {
+        return jti;
+    }
+
+    public JWClaim getClaim(String claimName, Object defValue) {
+        return claims.getOrDefault(claimName, new JWClaim(defValue));
+    }
+
+    public JWClaim getClaim(String claimName) {
+        return claims.get(claimName);
+    }
+
+    public JWClaim remove(String claimName) {
+        return claims.remove(claimName);
+    }
+
+    public JWClaim remove(String claimName, Object defValue) {
+        if (claims.containsKey(claimName)) {
+            return claims.remove(claimName);
+        }
+        return new JWClaim(defValue);
+    }
+}

+ 32 - 0
connector-tools/src/main/java/cz/senslog/connector/tools/jwt/JWToken.java

@@ -0,0 +1,32 @@
+package cz.senslog.connector.tools.jwt;
+
+public class JWToken {
+
+    private final String raw;
+    private final JWTHeader header;
+    private final JWTPayload payload;
+    private final String signature;
+
+    public JWToken(String raw, JWTHeader header, JWTPayload payload, String signature) {
+        this.raw = raw;
+        this.header = header;
+        this.payload = payload;
+        this.signature = signature;
+    }
+
+    public String getRaw() {
+        return raw;
+    }
+
+    public JWTHeader getHeader() {
+        return header;
+    }
+
+    public JWTPayload getPayload() {
+        return payload;
+    }
+
+    public String getSignature() {
+        return signature;
+    }
+}

+ 11 - 0
connector-tools/src/main/java/cz/senslog/connector/tools/util/Encoding.java

@@ -0,0 +1,11 @@
+package cz.senslog.connector.tools.util;
+
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
+
+public final class Encoding {
+
+    public static final Charset WINDOWS_1250 = Charset.forName("cp1250");
+
+    public static final Charset UTF8 = StandardCharsets.UTF_8;
+}

+ 33 - 6
docker-compose.yaml

@@ -12,7 +12,7 @@ services:
     volumes:
 #      - D:\drutes-predict:/data/drutes-predict
       - /data/drutes-predict:/data/drutes-predict
-    restart: always
+#    restart: always
     environment:
       APP_PARAMS: -cf config/drutesSenslog1.yaml
       TZ: Europe/Prague
@@ -24,7 +24,7 @@ services:
       context: .
       args:
         MAVEN_PROFILE: LoraWanSenslog1
-    restart: always
+#    restart: always
     environment:
       APP_PARAMS: -cf config/lorawanSenslog1.yaml
 
@@ -37,7 +37,7 @@ services:
          MAVEN_PROFILE: FieldClimateSenslog1
     ports:
       - "5005:5005"
-    restart: always
+#    restart: always
     environment:
         APP_PARAMS: -cf config/fieldclimateSenslog10.yaml
         DEBUG: "false"
@@ -62,7 +62,7 @@ services:
         MAVEN_PROFILE: GfsSenslog1
     ports:
       - "5005:5005"
-    restart: always
+#    restart: always
     environment:
       APP_PARAMS: -cf config/gfsSenslog.yaml
       DEBUG: "true"
@@ -76,7 +76,7 @@ services:
         MAVEN_PROFILE: Senslog1Analytics
 #    ports:
 #      - "5005:5005"
-    restart: always
+#    restart: always
     environment:
       APP_PARAMS: -cf config/senslog1Analytics.yaml
       DEBUG: "false"
@@ -90,11 +90,24 @@ services:
         MAVEN_PROFILE: SenslogTelemetryTheros
     ports:
       - "5005:5005"
-    restart: always
+#    restart: always
     environment:
       APP_PARAMS: -cf config/senslogTelemetryTheros.yaml
       DEBUG: "true"
 
+  fofr2telemetry:
+    container_name: fofr2telemetry
+    build:
+      dockerfile: docker/Dockerfile
+      context: .
+      args:
+        MAVEN_PROFILE: FofrTelemetry
+    ports:
+      - "5005:5005"
+    environment:
+      APP_PARAMS: -cf config/fofrToTelemetry.yaml
+      DEBUG: "true"
+
   demo2logger:
     container_name: demoLogger
     build:
@@ -105,3 +118,17 @@ services:
     environment:
       APP_PARAMS: -cf config/demoToLogger.yaml
       DEBUG: "false"
+
+  soilscount2senslog:
+    container_name: soilscountsenslog
+    build:
+      dockerfile: docker/Dockerfile
+      context: .
+      args:
+        MAVEN_PROFILE: SoilscountSenslog
+    ports:
+      - "5005:5005"
+    environment:
+      APP_PARAMS: -cf config/soilscountToSenslog.yaml
+      DEBUG: "true"
+

+ 22 - 0
pom.xml

@@ -85,6 +85,22 @@
         </profile>
 
         <profile>
+            <id>FofrTelemetry</id>
+            <modules>
+                <module>connector-fetch-fofr</module>
+                <module>connector-push-telemetry</module>
+            </modules>
+        </profile>
+
+        <profile>
+            <id>SoilscountSenslog</id>
+            <modules>
+                <module>connector-fetch-soilscount</module>
+                <module>connector-push-senslog-v1</module>
+            </modules>
+        </profile>
+
+        <profile>
             <id>all</id>
             <activation>
                 <activeByDefault>true</activeByDefault>
@@ -102,6 +118,9 @@
                 <module>connector-push-analytics</module>
                 <module>connector-push-theros</module>
                 <module>connector-push-logger</module>
+                <module>connector-fetch-fofr</module>
+                <module>connector-fetch-soilscount</module>
+                <module>connector-push-telemetry</module>
             </modules>
         </profile>
     </profiles>
@@ -120,6 +139,9 @@
         <module>connector-fetch-senslog-telemetry</module>
         <module>connector-push-theros</module>
         <module>connector-push-logger</module>
+        <module>connector-fetch-fofr</module>
+        <module>connector-fetch-soilscount</module>
+        <module>connector-push-telemetry</module>
     </modules>
 
     <properties>