瀏覽代碼

Minimal Connector for Open Meteo

Lukas Cerny 1 天之前
當前提交
c00f6e880f
共有 100 個文件被更改,包括 5545 次插入0 次删除
  1. 5 0
      .dockerignore
  2. 6 0
      .gitignore
  3. 二進制
      config/.lorawanSenslog1.yaml.un~
  4. 38 0
      config/openMeteoEra5LandToSenslog.yaml
  5. 75 0
      config/openMeteoIconEUToSenslog.yaml
  6. 33 0
      connector-app/license.txt
  7. 50 0
      connector-app/pom.xml
  8. 147 0
      connector-app/src/main/java/cz/senslog/connector/app/Application.java
  9. 18 0
      connector-app/src/main/java/cz/senslog/connector/app/Main.java
  10. 98 0
      connector-app/src/main/java/cz/senslog/connector/app/config/AppConfig.java
  11. 52 0
      connector-app/src/main/java/cz/senslog/connector/app/config/ConfigurationServiceImpl.java
  12. 194 0
      connector-app/src/main/java/cz/senslog/connector/app/config/Connector.java
  13. 235 0
      connector-app/src/main/java/cz/senslog/connector/app/config/ConnectorBuilder.java
  14. 45 0
      connector-app/src/main/java/cz/senslog/connector/app/config/FileBuilderImpl.java
  15. 257 0
      connector-app/src/main/java/cz/senslog/connector/app/config/FileConfigurationServiceImpl.java
  16. 83 0
      connector-app/src/main/java/cz/senslog/connector/app/config/Parameters.java
  17. 56 0
      connector-app/src/main/java/cz/senslog/connector/app/config/ServiceProvider.java
  18. 44 0
      connector-app/src/main/java/cz/senslog/connector/app/config/api/ConfigurationService.java
  19. 28 0
      connector-app/src/main/java/cz/senslog/connector/app/config/api/FileBuilder.java
  20. 25 0
      connector-app/src/main/java/cz/senslog/connector/app/config/api/FileConfigurationService.java
  21. 2 0
      connector-app/src/main/resources/project.properties
  22. 271 0
      connector-app/src/test/java/cz/senslog/connector/app/config/ConnectorBuilderTest.java
  23. 21 0
      connector-app/src/test/java/cz/senslog/connector/app/config/FileBuilderImplTest.java
  24. 77 0
      connector-app/src/test/java/cz/senslog/connector/app/config/FileConfigurationServiceImplTest.java
  25. 56 0
      connector-app/src/test/java/cz/senslog/connector/app/config/ParametersTest.java
  26. 7 0
      connector-app/src/test/java/cz/senslog/connector/app/config/TestFetchProviderClass.java
  27. 7 0
      connector-app/src/test/java/cz/senslog/connector/app/config/TestPushProviderClass.java
  28. 26 0
      connector-app/src/test/resources/test_valid_config.yaml
  29. 33 0
      connector-fetch-api/license.txt
  30. 33 0
      connector-fetch-api/pom.xml
  31. 55 0
      connector-fetch-api/src/main/java/cz/senslog/connector/fetch/ConnectorFetch.java
  32. 25 0
      connector-fetch-api/src/main/java/cz/senslog/connector/fetch/api/ConnectorFetchProvider.java
  33. 33 0
      connector-fetch-api/src/main/java/cz/senslog/connector/fetch/api/ConnectorFetcher.java
  34. 48 0
      connector-fetch-api/src/main/java/cz/senslog/connector/fetch/api/ExecutableFetcher.java
  35. 33 0
      connector-fetch-openmeteo/license.txt
  36. 32 0
      connector-fetch-openmeteo/pom.xml
  37. 38 0
      connector-fetch-openmeteo/src/main/java/cz/senslog/connector/fetch/openmeteo/ConnectorFetchOpenMeteoProvider.java
  38. 157 0
      connector-fetch-openmeteo/src/main/java/cz/senslog/connector/fetch/openmeteo/OpenMeteoConfig.java
  39. 199 0
      connector-fetch-openmeteo/src/main/java/cz/senslog/connector/fetch/openmeteo/OpenMeteoFetcher.java
  40. 22 0
      connector-fetch-openmeteo/src/main/java/cz/senslog/connector/fetch/openmeteo/Station.java
  41. 1 0
      connector-fetch-openmeteo/src/main/resources/META-INF/services/cz.senslog.connector.fetch.api.ConnectorFetchProvider
  42. 33 0
      connector-model/license.txt
  43. 22 0
      connector-model/pom.xml
  44. 33 0
      connector-model/src/main/java/cz/senslog/connector/model/api/AbstractModel.java
  45. 27 0
      connector-model/src/main/java/cz/senslog/connector/model/api/Converter.java
  46. 66 0
      connector-model/src/main/java/cz/senslog/connector/model/api/ConverterProvider.java
  47. 22 0
      connector-model/src/main/java/cz/senslog/connector/model/api/ProxySessionModel.java
  48. 14 0
      connector-model/src/main/java/cz/senslog/connector/model/api/VoidSession.java
  49. 82 0
      connector-model/src/main/java/cz/senslog/connector/model/config/ConnectorDescriptor.java
  50. 45 0
      connector-model/src/main/java/cz/senslog/connector/model/config/DefaultConfig.java
  51. 55 0
      connector-model/src/main/java/cz/senslog/connector/model/config/HostConfig.java
  52. 291 0
      connector-model/src/main/java/cz/senslog/connector/model/config/PropertyConfig.java
  53. 24 0
      connector-model/src/main/java/cz/senslog/connector/model/converter/ModelConverterProvider.java
  54. 34 0
      connector-model/src/main/java/cz/senslog/connector/model/converter/OpenMeteoToSensLogConverter.java
  55. 24 0
      connector-model/src/main/java/cz/senslog/connector/model/openmeteo/OpenMeteoModel.java
  56. 50 0
      connector-model/src/main/java/cz/senslog/connector/model/senslog/SensLogModel.java
  57. 71 0
      connector-model/src/test/java/cz/senslog/connector/model/api/ConverterProviderTest.java
  58. 25 0
      connector-model/src/test/java/cz/senslog/connector/model/config/ConnectorDescriptorTest.java
  59. 20 0
      connector-model/src/test/java/cz/senslog/connector/model/config/DefaultConfigTest.java
  60. 22 0
      connector-model/src/test/java/cz/senslog/connector/model/config/HostConfigTest.java
  61. 86 0
      connector-model/src/test/java/cz/senslog/connector/model/config/PropertyConfigTest.java
  62. 33 0
      connector-push-api/license.txt
  63. 33 0
      connector-push-api/pom.xml
  64. 56 0
      connector-push-api/src/main/java/cz/senslog/connector/push/ConnectorPush.java
  65. 25 0
      connector-push-api/src/main/java/cz/senslog/connector/push/api/ConnectorPushProvider.java
  66. 30 0
      connector-push-api/src/main/java/cz/senslog/connector/push/api/ConnectorPusher.java
  67. 33 0
      connector-push-senslog/license.txt
  68. 39 0
      connector-push-senslog/pom.xml
  69. 32 0
      connector-push-senslog/src/main/java/cz/senslog/connector/push/senslog/ConnectorPushSensLogProvider.java
  70. 19 0
      connector-push-senslog/src/main/java/cz/senslog/connector/push/senslog/SensLogConfig.java
  71. 85 0
      connector-push-senslog/src/main/java/cz/senslog/connector/push/senslog/SensLogPusher.java
  72. 1 0
      connector-push-senslog/src/main/resources/META-INF/services/cz.senslog.connector.push.api.ConnectorPushProvider
  73. 33 0
      connector-tools/license.txt
  74. 88 0
      connector-tools/pom.xml
  75. 11 0
      connector-tools/src/main/java/cz/senslog/connector/tools/exception/ModuleInterruptedException.java
  76. 11 0
      connector-tools/src/main/java/cz/senslog/connector/tools/exception/ParseException.java
  77. 11 0
      connector-tools/src/main/java/cz/senslog/connector/tools/exception/PropertyNotFoundException.java
  78. 11 0
      connector-tools/src/main/java/cz/senslog/connector/tools/exception/SyntaxException.java
  79. 13 0
      connector-tools/src/main/java/cz/senslog/connector/tools/exception/UnsupportedFileException.java
  80. 246 0
      connector-tools/src/main/java/cz/senslog/connector/tools/http/HttpClient.java
  81. 25 0
      connector-tools/src/main/java/cz/senslog/connector/tools/http/HttpCode.java
  82. 20 0
      connector-tools/src/main/java/cz/senslog/connector/tools/http/HttpContentType.java
  83. 53 0
      connector-tools/src/main/java/cz/senslog/connector/tools/http/HttpCookie.java
  84. 14 0
      connector-tools/src/main/java/cz/senslog/connector/tools/http/HttpHeader.java
  85. 8 0
      connector-tools/src/main/java/cz/senslog/connector/tools/http/HttpMethod.java
  86. 105 0
      connector-tools/src/main/java/cz/senslog/connector/tools/http/HttpRequest.java
  87. 82 0
      connector-tools/src/main/java/cz/senslog/connector/tools/http/HttpRequestBuilder.java
  88. 80 0
      connector-tools/src/main/java/cz/senslog/connector/tools/http/HttpResponse.java
  89. 45 0
      connector-tools/src/main/java/cz/senslog/connector/tools/http/HttpResponseBuilder.java
  90. 114 0
      connector-tools/src/main/java/cz/senslog/connector/tools/http/URLBuilder.java
  91. 196 0
      connector-tools/src/main/java/cz/senslog/connector/tools/json/BasicJson.java
  92. 25 0
      connector-tools/src/main/java/cz/senslog/connector/tools/json/BasicJsonDeserializer.java
  93. 9 0
      connector-tools/src/main/java/cz/senslog/connector/tools/json/FormatFunction.java
  94. 34 0
      connector-tools/src/main/java/cz/senslog/connector/tools/util/ClassUtils.java
  95. 34 0
      connector-tools/src/main/java/cz/senslog/connector/tools/util/Next.java
  96. 36 0
      connector-tools/src/main/java/cz/senslog/connector/tools/util/NextImpl.java
  97. 23 0
      connector-tools/src/main/java/cz/senslog/connector/tools/util/Pipe.java
  98. 30 0
      connector-tools/src/main/java/cz/senslog/connector/tools/util/PipeImpl.java
  99. 35 0
      connector-tools/src/main/java/cz/senslog/connector/tools/util/Pipeline.java
  100. 56 0
      connector-tools/src/main/java/cz/senslog/connector/tools/util/StringUtils.java

+ 5 - 0
.dockerignore

@@ -0,0 +1,5 @@
+.gitignore
+.idea
+docker-compose.yaml
+README.md
+*.tar

+ 6 - 0
.gitignore

@@ -0,0 +1,6 @@
+.idea
+*.iml
+bin
+logs
+target
+*.log

二進制
config/.lorawanSenslog1.yaml.un~


+ 38 - 0
config/openMeteoEra5LandToSenslog.yaml

@@ -0,0 +1,38 @@
+settings:
+  - OpenMeteo:
+      name: "Open Meteo Era5 Land"
+      provider: "cz.senslog.connector.fetch.openmeteo.ConnectorFetchOpenMeteoProvider"
+
+      baseUrl: "https://archive-api.open-meteo.com/v1/archive"
+
+      timeZone: "Europe/Prague"
+      startDate: "2026-01-16"
+#      endDate: "2026-01-15"
+      period: 24 # 1 day
+
+      allowedStation:
+        url: "https://sensor.lesprojekt.cz/senslogOTS3/rest/forecast/era5land/plan"
+        params:
+            latitude: ${latitude}
+            longitude: ${longitude}
+            start_date: ${currentDate}
+            end_date: ${currentDate}
+            models: era5_land
+            hourly: [temperature_2m, relative_humidity_2m, dew_point_2m, precipitation, rain, snowfall, pressure_msl, surface_pressure,
+                      et0_fao_evapotranspiration, cloud_cover,wind_speed_10m, wind_direction_10m, wind_gusts_10m,
+                      soil_temperature_0_to_7cm,soil_temperature_7_to_28cm,soil_temperature_28_to_100cm,soil_temperature_100_to_255cm,
+                      soil_moisture_0_to_7cm,soil_moisture_7_to_28cm,soil_moisture_28_to_100cm,soil_moisture_100_to_255cm]
+
+  - Senslog:
+      name: "Senslog Pass Trough"
+      provider: "cz.senslog.connector.push.senslog.ConnectorPushSensLogProvider"
+
+      baseUrl: "https://sensor.lesprojekt.cz/senslogOTS3/rest/forecast/era5land"
+
+connectors:
+  - era5LandSenslog:
+      fetcher: "OpenMeteo"
+      pusher: "Senslog"
+      period: 86_400 # 24h
+      startAt: "01:00:00" # hh:mm:ss  # non-mandatory attribute
+#      initDelay: 10        # non-mandatory attribute

+ 75 - 0
config/openMeteoIconEUToSenslog.yaml

@@ -0,0 +1,75 @@
+settings:
+
+  - iconeu:
+      name: "[RT] Open Meteo Icon EU"
+      provider: "cz.senslog.connector.fetch.openmeteo.ConnectorFetchOpenMeteoProvider"
+
+      baseUrl: "https://previous-runs-api.open-meteo.com/v1/forecast"
+
+      timeZone: "UTC"
+      startDate: "now"
+      period: -1 # -1 = disabled
+
+
+      allowedStation:
+        url: "https://sensor.lesprojekt.cz/senslogOTS3/rest/forecast/openmeteo/plan"
+        params:
+          latitude: ${latitude}
+          longitude: ${longitude}
+          models: icon_eu
+          forecast_days: 3
+          hourly: [temperature_2m,temperature_2m_previous_day2,relative_humidity_2m,relative_humidity_2m_previous_day2,
+                   dew_point_2m,dew_point_2m_previous_day1,dew_point_2m_previous_day2,apparent_temperature,apparent_temperature_previous_day1,apparent_temperature_previous_day2,
+                   precipitation,precipitation_previous_day1,precipitation_previous_day2,rain,rain_previous_day1,rain_previous_day2,showers,snowfall,weather_code,surface_pressure,cloud_cover,
+                   wind_direction_10m,wind_speed_10m,showers_previous_day1,snowfall_previous_day1,
+                   weather_code_previous_day1,surface_pressure_previous_day1,cloud_cover_previous_day1,wind_speed_10m_previous_day1,wind_direction_10m_previous_day1,
+                   showers_previous_day2,snowfall_previous_day2,weather_code_previous_day2,surface_pressure_previous_day2,cloud_cover_previous_day2,wind_speed_10m_previous_day2,
+                   wind_direction_10m_previous_day2,relative_humidity_2m_previous_day1,temperature_2m_previous_day1]
+
+  - iconeu_past:
+      name: "[past] Open Meteo Icon EU"
+      provider: "cz.senslog.connector.fetch.openmeteo.ConnectorFetchOpenMeteoProvider"
+
+      baseUrl: "https://previous-runs-api.open-meteo.com/v1/forecast"
+
+      timeZone: "UTC"
+      startDate: "2024-12-01" # Dec. 1 2024
+      endDate: "2025-12-14" # Dec. 14
+      period: 24 # 1 day
+
+      allowedStation:
+        url: "https://sensor.lesprojekt.cz/senslogOTS3/rest/forecast/openmeteo/plan"
+        minId: 4
+        params:
+          latitude: ${latitude}
+          longitude: ${longitude}
+          models: icon_eu
+          start_date: ${currentDate}
+          end_date: ${currentDate}
+          hourly: [ temperature_2m,temperature_2m_previous_day2,relative_humidity_2m,relative_humidity_2m_previous_day2,
+                    dew_point_2m,dew_point_2m_previous_day1,dew_point_2m_previous_day2,apparent_temperature,apparent_temperature_previous_day1,apparent_temperature_previous_day2,
+                    precipitation,precipitation_previous_day1,precipitation_previous_day2,rain,rain_previous_day1,rain_previous_day2,showers,snowfall,weather_code,surface_pressure,cloud_cover,
+                    wind_direction_10m,wind_speed_10m,showers_previous_day1,snowfall_previous_day1,
+                    weather_code_previous_day1,surface_pressure_previous_day1,cloud_cover_previous_day1,wind_speed_10m_previous_day1,wind_direction_10m_previous_day1,
+                    showers_previous_day2,snowfall_previous_day2,weather_code_previous_day2,surface_pressure_previous_day2,cloud_cover_previous_day2,wind_speed_10m_previous_day2,
+                    wind_direction_10m_previous_day2,relative_humidity_2m_previous_day1,temperature_2m_previous_day1 ]
+  - Senslog:
+      name: "Senslog Pass Trough"
+      provider: "cz.senslog.connector.push.senslog.ConnectorPushSensLogProvider"
+
+      baseUrl: "https://sensor.lesprojekt.cz/senslogOTS3/rest/forecast/openmeteo"
+
+connectors:
+  - IconEUSenslog_RT:
+      fetcher: "iconeu"
+      pusher: "Senslog"
+      period: 10_800          # 3h
+      startAt: "03:00:00" # hh:mm:ss  # non-mandatory attribute
+#      initDelay: 10        # non-mandatory attribute
+
+#  - IconEUSenslog_PAST:
+#      fetcher: "iconeu_past"
+#      pusher: "Senslog"
+#      period: 10           # seconds
+##      startAt: "22:10:00" # hh:mm:ss  # non-mandatory attribute
+#      initDelay: 10        # non-mandatory attribute

+ 33 - 0
connector-app/license.txt

@@ -0,0 +1,33 @@
+BSD-3-Clause
+
+Copyright (c) 2020 University of West Bohemia (UWB) https://www.zcu.cz/en, LESPROJEKT-SLUŽBY s.r.o. (LESP) https://www.lesprojekt.cz/en
+
+Copyright 2026 <COPYRIGHT HOLDER>
+
+Redistribution and use in source and binary
+forms, with or without modification, are permitted provided that the following
+conditions are met:
+
+1. Redistributions of source code must retain the above
+copyright notice, this list of conditions and the following disclaimer.
+
+2.
+Redistributions in binary form must reproduce the above copyright notice, this
+list of conditions and the following disclaimer in the documentation and/or
+other materials provided with the distribution.
+
+3. Neither the name of the
+copyright holder nor the names of its contributors may be used to endorse or
+promote products derived from this software without specific prior written
+permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+“AS IS” AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
+TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

+ 50 - 0
connector-app/pom.xml

@@ -0,0 +1,50 @@
+<?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">
+    <parent>
+        <artifactId>connector-period</artifactId>
+        <groupId>cz.senslog</groupId>
+        <version>1.0-SNAPSHOT</version>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+
+    <artifactId>connector-app</artifactId>
+    <name>app</name>
+
+    <dependencies>
+        <dependency>
+            <groupId>cz.senslog</groupId>
+            <artifactId>connector-fetch-api</artifactId>
+            <version>${project.parent.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>cz.senslog</groupId>
+            <artifactId>connector-push-api</artifactId>
+            <version>${project.parent.version}</version>
+        </dependency>
+
+        <dependency>
+            <groupId>com.beust</groupId>
+            <artifactId>jcommander</artifactId>
+            <version>1.82</version>
+        </dependency>
+
+    </dependencies>
+
+    <build>
+        <resources>
+            <resource>
+                <directory>src/main/resources</directory>
+                <filtering>true</filtering>
+            </resource>
+        </resources>
+        <plugins>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-assembly-plugin</artifactId>
+            </plugin>
+        </plugins>
+    </build>
+
+</project>

+ 147 - 0
connector-app/src/main/java/cz/senslog/connector/app/Application.java

@@ -0,0 +1,147 @@
+// Copyright (c) 2026 UWB & LESP.
+// The UWB & LESP license this file to you under the BSD-3-Clause license.
+
+package cz.senslog.connector.app;
+
+import cz.senslog.connector.app.config.*;
+import cz.senslog.connector.app.config.api.ConfigurationService;
+import cz.senslog.connector.app.config.api.FileConfigurationService;
+import cz.senslog.connector.fetch.ConnectorFetch;
+import cz.senslog.connector.model.converter.ModelConverterProvider;
+import cz.senslog.connector.push.ConnectorPush;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+import java.io.IOException;
+import java.util.Set;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+
+/**
+ * The class {@code Application} represents a trigger for entire application.
+ *
+ * @author Lukas Cerny
+ * @version 1.0
+ * @since 1.0
+ */
+class Application extends Thread {
+
+    private static final Logger logger = LogManager.getLogger(Application.class);
+
+    /** Attribute of basic configuration values of the application. */
+    private final AppConfig appConfig;
+
+    /** Attribute of input parameters of the application. */
+    private final Parameters params;
+
+    private CountDownLatch latch;
+
+    private ScheduledExecutorService scheduler;
+
+    /**
+     * Initialization method to trigger the application.
+     * @param args - array of parameters.
+     * @return new thread of {@code Runnable}.
+     * @throws IOException throws if input parameters or application configuration file can not be parsed.
+     */
+    static Thread init(String... args) throws IOException {
+        AppConfig appConfig = AppConfig.load();
+        Parameters parameters = Parameters.parse(appConfig, args);
+
+        if (parameters.isHelp()) {
+            return new Thread(parameters::printHelp);
+        }
+
+        Application app = new Application(appConfig, parameters);
+        Runtime.getRuntime().addShutdownHook(new Thread(app::interrupt, "clean-app"));
+
+        return app;
+    }
+
+    /**
+     * Private constructor of the class. Accessible via static init method {@link Application#init(String...)}.
+     * @param appConfig basic configuration of the application. More info of the class {@see AppConfig}.
+     * @param parameters parsed input parameters of the application. More info of the class  {@see Parameters}.
+     */
+    private Application(AppConfig appConfig, Parameters parameters) {
+        super("app");
+
+        this.appConfig = appConfig;
+        this.params = parameters;
+    }
+
+    /**
+     * Override method from {@link Thread} to clean up all scheduled processes after the application is being determined.
+     */
+    @Override
+    public void interrupt() {
+        logger.info("Stopping the application {} version {}", appConfig.getName(), appConfig.getVersion());
+
+        if (latch != null) {
+            for (int i = 0; i < latch.getCount(); i++) {
+                latch.countDown();
+            }
+        }
+
+        logger.info("Cleaning all connector's threads.");
+        if (scheduler != null) {
+            scheduler.shutdownNow();
+        }
+
+        logger.info("The application was stopped.");
+        super.interrupt();
+    }
+
+    /**
+     * The main method executing the application.
+     * 1. Contains configuration services, which provide configuration for modules.
+     *      Only configuration by file is available and required passing the filename as an execution parameter.
+     * 2. All configured modules as loaded and initialized by its configuration.
+     * 3. The class {@link ConnectorBuilder} creates connectors that then are scheduled by the scheduler.
+     *      Each connector is running within a thread separately.
+     */
+    @Override
+    public void run() {
+        logger.info("Starting the application {} version {}", appConfig.getName(), appConfig.getVersion());
+
+
+        ConfigurationService configService;
+        try {
+            FileConfigurationService service = ConfigurationService.newFileBuilder()
+                    .fileName(params.getConfigFileName()).build();
+
+            service.load();
+
+            configService = service;
+        } catch (IOException e) {
+            logger.catching(e); return;
+        }
+
+
+        ServiceProvider serviceProvider = new ServiceProvider(ConnectorFetch::getProvider, ConnectorPush::getProvider);
+        ModelConverterProvider converterProvider = new ModelConverterProvider();
+        ConnectorBuilder connectorBuilder = ConnectorBuilder.init(serviceProvider, converterProvider, configService);
+        Set<Connector> connectors = connectorBuilder.createConnectors();
+
+        if (!connectors.isEmpty()) {
+            scheduler = Executors.newScheduledThreadPool(connectors.size());
+            latch = new CountDownLatch(connectors.size());
+
+            logger.info("Starting a scheduler for {} connector(s).", connectors.size());
+            connectors.forEach(c -> c.schedule(scheduler, latch));
+
+            try {
+                logger.info("Waiting for the working threads.");
+                latch.await();
+                logger.info("All scheduled connector finished their job.");
+            } catch (InterruptedException e) {
+                logger.catching(e);
+            }
+        } else {
+            logger.warn("No connectors were loaded.");
+        }
+
+        interrupt();
+    }
+}

+ 18 - 0
connector-app/src/main/java/cz/senslog/connector/app/Main.java

@@ -0,0 +1,18 @@
+// Copyright (c) 2026 UWB & LESP.
+// The UWB & LESP license this file to you under the BSD-3-Clause license.
+
+package cz.senslog.connector.app;
+
+/**
+ * Main of the application.
+ *
+ * @author Lukas Cerny
+ * @version 1.0
+ * @since 1.0
+ */
+public class Main {
+
+    public static void main(String[] args) throws Exception {
+        Application.init(args).start();
+    }
+}

+ 98 - 0
connector-app/src/main/java/cz/senslog/connector/app/config/AppConfig.java

@@ -0,0 +1,98 @@
+// Copyright (c) 2026 UWB & LESP.
+// The UWB & LESP license this file to you under the BSD-3-Clause license.
+
+package cz.senslog.connector.app.config;
+
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Properties;
+
+/**
+ * The class {@code AppConfig} represents basic configuration of
+ * the application. The configuration file is located in resources
+ * and the values are connected to the properties in parent pom.xml.
+ *
+ * @author Lukas Cerny
+ * @version 1.0
+ * @since 1.0
+ */
+public class AppConfig {
+
+    private static final Logger logger = LogManager.getLogger(AppConfig.class);
+
+
+    /** Name of the properties configuration file located in the 'resources' folder. */
+    private static final String PROPERTIES_FILE_NAME = "project.properties";
+
+    /** Attribute of loaded properties. */
+    private final Properties properties;
+
+    /**
+     * Static method to load the configuration file.
+     * @return new instance of {@code AppConfig}.
+     * @throws IOException throws if the file is not loaded successfully.
+     */
+    public static AppConfig load() throws IOException {
+        logger.debug("Loading application configuration file '{}'", PROPERTIES_FILE_NAME);
+
+        Properties properties = new Properties();
+
+        logger.debug("Getting the class loader from the class {}.", AppConfig.class.getName());
+        ClassLoader loader = AppConfig.class.getClassLoader();
+
+        logger.debug("Opening the file '{}'.", PROPERTIES_FILE_NAME);
+        InputStream stream = loader.getResourceAsStream(PROPERTIES_FILE_NAME);
+
+        logger.debug("Parsing application configuration file '{}'", PROPERTIES_FILE_NAME);
+        properties.load(stream);
+
+        logger.debug("Application configuration file was loaded successfully.");
+        return new AppConfig(properties);
+    }
+
+    /**
+     * Private constructor of the class. Accessible via static init method {@link AppConfig#load()}.
+     * @param properties - loaded properties from the file.
+     */
+    private AppConfig(Properties properties) {
+        this.properties = properties;
+    }
+
+    /**
+     * Returns name of the application defined in pom.xml
+     * @return name of the application or 'unknown'.
+     */
+    public String getName() {
+        return getProperty("app.name", "unknown");
+    }
+
+    /**
+     * Returns version of the application defined in pom.xml
+     * @return version of the application or empty string.
+     */
+    public String getVersion() {
+        return getProperty("app.version", "");
+    }
+
+    /**
+     * General method the get a property .
+     * @param propertyName - name of the property.
+     * @param defaultValue - default value if property does not exists.
+     * @return value of the property or default value.
+     */
+    private String getProperty(String propertyName, String defaultValue) {
+        logger.debug("Getting property with the name '{}'", propertyName);
+        String value = properties.getProperty(propertyName);
+
+        if (value == null) {
+            logger.debug("Property '{}' was not found. Used default property value '{}'.", propertyName, defaultValue);
+            return defaultValue;
+        } else {
+            logger.debug("Property '{}' was loaded successfully.", propertyName);
+            return value;
+        }
+    }
+}

+ 52 - 0
connector-app/src/main/java/cz/senslog/connector/app/config/ConfigurationServiceImpl.java

@@ -0,0 +1,52 @@
+// Copyright (c) 2026 UWB & LESP.
+// The UWB & LESP license this file to you under the BSD-3-Clause license.
+
+package cz.senslog.connector.app.config;
+
+import cz.senslog.connector.app.config.api.ConfigurationService;
+import cz.senslog.connector.model.config.ConnectorDescriptor;
+import cz.senslog.connector.model.config.DefaultConfig;
+
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * The class {@code ConfigurationServiceImpl} represents an implementation of {@link ConfigurationService}.
+ * The class is used to provide a registration for some new connectors and class configurations.
+ *
+ * @author Lukas Cerny
+ * @version 1.0
+ * @since 1.0
+ */
+public abstract class ConfigurationServiceImpl implements ConfigurationService {
+
+    private final Set<ConnectorDescriptor> connectorDescriptors;
+    private final Map<String, DefaultConfig> configurations;
+
+    ConfigurationServiceImpl() {
+        this.connectorDescriptors = new HashSet<>();
+        this.configurations = new HashMap<>();
+    }
+
+    protected void addConnectorDescriptor(ConnectorDescriptor descriptor) {
+        connectorDescriptors.add(descriptor);
+    }
+
+    protected void addProviderConfiguration(String providerId, DefaultConfig config) {
+        if (!configurations.containsKey(providerId)) {
+            configurations.put(providerId, config);
+        }
+    }
+
+    @Override
+    public Set<ConnectorDescriptor> getConnectorDescriptors() {
+        return connectorDescriptors;
+    }
+
+    @Override
+    public DefaultConfig getConfigForProviderId(String providerId) {
+        return configurations.get(providerId);
+    }
+}

+ 194 - 0
connector-app/src/main/java/cz/senslog/connector/app/config/Connector.java

@@ -0,0 +1,194 @@
+// Copyright (c) 2026 UWB & LESP.
+// The UWB & LESP license this file to you under the BSD-3-Clause license.
+
+package cz.senslog.connector.app.config;
+
+import cz.senslog.connector.tools.exception.ModuleInterruptedException;
+import cz.senslog.connector.fetch.api.ExecutableFetcher;
+import cz.senslog.connector.model.api.AbstractModel;
+import cz.senslog.connector.model.api.Converter;
+import cz.senslog.connector.push.api.ConnectorPusher;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.LocalTime;
+import java.time.temporal.ChronoUnit;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.concurrent.*;
+
+import static cz.senslog.connector.tools.util.Pipeline.of;
+import static java.util.Optional.ofNullable;
+import static java.util.concurrent.TimeUnit.MILLISECONDS;
+
+/**
+ * The class {@code Connector} represents a created connector
+ * which allows to be scheduled by defined period.
+ * The idea is to wrap functionality of a connector. The flow is
+ * defined as 'fetcher' -> 'converter' -> 'pusher'.
+ *
+ * @author Lukas Cerny
+ * @version 1.0
+ * @since 1.0
+ */
+public final class Connector {
+
+    private static final Logger logger = LogManager.getLogger(Connector.class);
+
+    /** Default initialization delay value when the scheduler starts to schedule tasks (in millis). */
+    private static final int DEFAULT_DELAY_MILLIS = 2_000; // 2s
+
+    /** Default value for scheduling tasks when the value missing in the configuration file (in millis). */
+    private static final int DEFAULT_SCHEDULE_PERIOD_MILLIS = 3_600_000;  // every hour
+
+    /** Name of the connector */
+    private final String name;
+
+    /** Instance of a fetcher that provides data. */
+    private final ExecutableFetcher<? super AbstractModel> fetcherExecutor;
+
+    /** Instance of a pusher that receives data. */
+    private final ConnectorPusher<? super AbstractModel> pusher;
+
+    /** Converter between fetch and push. */
+    private final Converter<? super AbstractModel, ? super AbstractModel> converter;
+
+    /** Period for scheduler. */
+    private final Integer period;
+
+    /** Initialization delay for scheduler. */
+    private final Integer initDelay;
+
+    private final LocalTime startAt;
+
+    /**
+     * Constructor allows to set all attributes.
+     * @param name - name of the connector.
+     * @param fetcherExecutor - instance of fetcher.
+     * @param pusher - instance of pusher.
+     * @param converter - instance of converter.
+     * @param period - period for scheduling.
+     */
+    public Connector(
+            String name,
+            ExecutableFetcher<? super AbstractModel> fetcherExecutor,
+            ConnectorPusher<? super AbstractModel> pusher,
+            Converter<? super AbstractModel, ? super AbstractModel> converter,
+            Integer period,
+            Integer initDelay,
+            LocalTime startAt
+    ) {
+        this.name = name;
+        this.fetcherExecutor = fetcherExecutor;
+        this.pusher = pusher;
+        this.converter = converter;
+        this.period = period != null ? period * 1_000 : null; // to millis
+        this.initDelay = initDelay != null ? initDelay * 1_000 : null; // to millis
+        this.startAt = startAt;
+    }
+
+    /**
+     * Returns name of the connector.
+     * @return name of the connector.
+     */
+    public String getName() {
+        return name;
+    }
+
+    /**
+     * Returns period for scheduler.
+     * @return period for scheduler.
+     */
+    public Optional<Integer> getPeriod() {
+        return ofNullable(period);
+    }
+
+    /**
+     * Returns initialization delay for scheduler.
+     * @return delay for scheduler.
+     */
+    public Optional<Integer> getInitDelay() {
+        return ofNullable(initDelay);
+    }
+
+    /**
+     * Returns time to schedule the connector at.
+     * @return time to execute the connector
+     */
+    public Optional<LocalTime> getStartAt() {
+        return ofNullable(startAt);
+    }
+
+    /**
+     * Returns scheduling runnable task of the connector flow.
+     * @return runnable task
+     */
+    public Runnable getTask() {
+        return () -> of(fetcherExecutor::execute).pipe(converter::convert).end(pusher::push);
+    }
+
+    /**
+     * Schedules connector according to settings. Input parameters are scheduled service
+     * {@link ScheduledExecutorService} and {@link CountDownLatch} uses as a thread barrier.
+     * @param scheduledService - scheduled service
+     * @param latch - thread counter
+     */
+    public void schedule(ScheduledExecutorService scheduledService, CountDownLatch latch) {
+
+        long schedulePeriod = getPeriod().orElse(DEFAULT_SCHEDULE_PERIOD_MILLIS);
+        LocalTime startAtTime = getStartAt().orElse(null);
+        Runnable task = getTask();
+
+        StringBuilder logScheduling = new StringBuilder("Scheduling the '"+getName()+"' starts");
+        long delay = DEFAULT_DELAY_MILLIS;
+        if (startAtTime != null) {
+            LocalDateTime now = LocalDateTime.now();
+            LocalDate startAtDate = now.toLocalTime().isBefore(startAtTime) ? now.toLocalDate() : now.toLocalDate().plusDays(1);
+            LocalDateTime startAt = LocalDateTime.of(startAtDate, startAtTime);
+            logScheduling.append(" at ").append(startAt);
+            delay = now.until(startAt, ChronoUnit.MILLIS);
+            if (delay <= 0) {
+                delay = DEFAULT_DELAY_MILLIS;
+            }
+        } else if (getInitDelay().isPresent()) {
+            delay = getInitDelay().get();
+            logScheduling.append(" in ").append(delay);
+        }
+        logScheduling.append(" with the period ").append(schedulePeriod).append(" milliseconds.");
+
+        logger.info(logScheduling);
+        ScheduledFuture<?> future = scheduledService.scheduleAtFixedRate(task, delay, schedulePeriod, MILLISECONDS);
+
+        new Thread(() -> {
+            try {
+                future.get();
+            } catch (ExecutionException e) {
+                if (e.getCause() instanceof ModuleInterruptedException) {
+                    logger.warn(e.getMessage());
+                } else {
+                    logger.catching(e);
+                }
+            } catch (Exception e) {
+                logger.catching(e);
+            } finally {
+                future.cancel(true);
+                latch.countDown();
+            }
+        }, "thread-"+getName()).start();
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+        Connector connector = (Connector) o;
+        return Objects.equals(name, connector.name);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(name);
+    }
+}

+ 235 - 0
connector-app/src/main/java/cz/senslog/connector/app/config/ConnectorBuilder.java

@@ -0,0 +1,235 @@
+// Copyright (c) 2026 UWB & LESP.
+// The UWB & LESP license this file to you under the BSD-3-Clause license.
+
+package cz.senslog.connector.app.config;
+
+import cz.senslog.connector.app.config.api.ConfigurationService;
+import cz.senslog.connector.model.config.ConnectorDescriptor;
+import cz.senslog.connector.model.config.DefaultConfig;
+import cz.senslog.connector.fetch.api.ConnectorFetchProvider;
+import cz.senslog.connector.fetch.api.ConnectorFetcher;
+import cz.senslog.connector.fetch.api.ExecutableFetcher;
+import cz.senslog.connector.model.api.AbstractModel;
+import cz.senslog.connector.model.api.Converter;
+import cz.senslog.connector.model.api.ConverterProvider;
+import cz.senslog.connector.push.api.ConnectorPushProvider;
+import cz.senslog.connector.push.api.ConnectorPusher;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+import java.lang.reflect.ParameterizedType;
+import java.lang.reflect.Type;
+import java.util.HashSet;
+import java.util.Set;
+
+import static java.lang.String.format;
+
+/**
+ * The class {@code ConnectorBuilder} provides a builder for the class {@link Connector}.
+ * The class creates new connectors according to configuration.
+ *
+ * @author Lukas Cerny
+ * @version 1.0
+ * @since 1.0
+ */
+public final class ConnectorBuilder {
+
+    private static final Logger logger = LogManager.getLogger(ConnectorBuilder.class);
+
+    /** Attribute provides fetch/push providers ({@link ServiceProvider}). */
+    private final ServiceProvider serviceProvider;
+
+    /** Attribute provides converters ({@link ConverterProvider}). */
+    private final ConverterProvider converterProvider;
+
+    /** Attribute provides configuration ({@link ConfigurationService}). */
+    private final ConfigurationService configService;
+
+    /**
+     * Static method for initialization. Sets all attributes.
+     * @param serviceProvider - service for fetch/push providers.
+     * @param converterProvider - service for converter.
+     * @param configService - service for configuration.
+     * @return new instance of {@code ConnectorBuilder}.
+     */
+    public static ConnectorBuilder init(ServiceProvider serviceProvider, ConverterProvider converterProvider, ConfigurationService configService) {
+        return new ConnectorBuilder(serviceProvider, converterProvider, configService);
+    }
+
+    /**
+     * Private constructor of the class. Accessible via static init method {@link ConnectorBuilder#init(ServiceProvider, ConverterProvider, ConfigurationService)}.
+     * @param serviceProvider - service for fetch/push providers.
+     * @param converterProvider - service for converter.
+     * @param configService - service for configuration.
+     */
+    private ConnectorBuilder(ServiceProvider serviceProvider, ConverterProvider converterProvider, ConfigurationService configService) {
+        this.serviceProvider = serviceProvider;
+        this.converterProvider = converterProvider;
+        this.configService = configService;
+    }
+
+    /**
+     * Creates and returns new instance of fetcher.
+     * @param fetchProviderId - id of fetcher provider.
+     * @return new instance of fetcher {@code ExecutableFetcher}.
+     * @throws Exception throws if the fetch provider does not exist or any configuration does not exist.
+     */
+    private ExecutableFetcher<?> createFetcherExecutor(String fetchProviderId) throws Exception {
+        logger.debug("Creating a new instance of fetcher for {}.", fetchProviderId);
+
+        DefaultConfig config = configService.getConfigForProviderId(fetchProviderId);
+        if (config == null) {
+            throw logger.throwing(new Exception(format(
+                    "Can not find a default settings for the provider %s.", fetchProviderId
+            )));
+        }
+
+        ConnectorFetchProvider provider = serviceProvider.getFetchProvider(config.getProvider());
+        if (provider == null) {
+            throw logger.throwing(new Exception(format(
+                    "Can not find a fetch provider instance for the %s.", config.getProvider()
+            )));
+        }
+
+        return provider.createExecutableFetcher(config);
+    }
+
+    /**
+     * Creates and returns new instance of pusher.
+     * @param pushProviderId - class of push provider.
+     * @return new instance of pusher {@code ConnectorPusher}.
+     * @throws Exception throws if the push provider does not exist or any configuration does not exist.
+     */
+    private ConnectorPusher<?> getPusherInstance(String pushProviderId) throws Exception {
+        logger.debug("Creating a new instance of pusher for {}.", pushProviderId);
+
+        DefaultConfig config = configService.getConfigForProviderId(pushProviderId);
+        if (config == null) {
+            throw logger.throwing(new Exception(format(
+                    "Can not find a default settings for the provider %s.", pushProviderId
+            )));
+        }
+
+        ConnectorPushProvider provider = serviceProvider.getPushProvider(config.getProvider());
+        if (provider == null) {
+            throw logger.throwing(new Exception(format(
+                    "Can not find a push provider instance for the %s.", config.getProvider()
+            )));
+        }
+
+        return provider.createPusher(config);
+    }
+
+    /**
+     * Creates connectors depends on configuration.
+     * For each connector descriptor is loaded fetch and push provider and created fetcher and pusher.
+     * From these instances is get their input model which is child of {@link AbstractModel}.
+     * If everything is successful then is called #init() method and created a new connector {@link Connector}.
+     * If anything throws an exception, creating of the connector will be skipped.
+     * @return set of created and valid connectors.
+     */
+    public Set<Connector> createConnectors() {
+        logger.info("Starting to create new connectors.");
+
+        logger.debug("Getting all connector descriptors from the configuration service.");
+        Set<ConnectorDescriptor> connectorDescriptors = configService.getConnectorDescriptors();
+        logger.debug("Creating an empty set of connectors with init size {}.", connectorDescriptors.size());
+        Set<Connector> connectors = new HashSet<>(connectorDescriptors.size());
+
+        for (ConnectorDescriptor connDesc : connectorDescriptors) {
+            try {
+                logger.debug("Getting descriptors for a new '{}' connector connection.", connDesc.getName());
+                logger.debug("Connector: {}", connDesc);
+
+                ExecutableFetcher fetcherExecutor = createFetcherExecutor(connDesc.getFetcherId());
+                ConnectorFetcher fetcher = fetcherExecutor.getRawFetcher();
+
+                ConnectorPusher pusher = getPusherInstance(connDesc.getPusherId());
+
+                Class<? extends AbstractModel> inputModel = getAbstractModelFromGeneric(fetcher.getClass());
+                Class<? extends AbstractModel> outputModel = getAbstractModelFromGeneric(pusher.getClass());
+
+                Converter converter = converterProvider.getConverter(inputModel, outputModel);
+                if (converter == null) {
+                    throw logger.throwing(new Exception(format(
+                            "Can not find converter for connector: %s -> %s.", fetcher.getClass(), pusher.getClass()
+                    )));
+                }
+
+                logger.info("Invocation of initialization method for the {}.", fetcher.getClass());
+                fetcher.init();
+
+                logger.info("Invocation of initialization method for the {}.", pusher.getClass());
+                pusher.init();
+
+                logger.debug("Creating a new {} connector.", connDesc.getName());
+                Connector connector = new Connector(connDesc.getName(),
+                        fetcherExecutor, pusher, converter,
+                        connDesc.getPeriod(), connDesc.getDelay(), connDesc.getStartAt());
+
+                logger.debug("Saving the {} connector.", connDesc.getName());
+                connectors.add(connector);
+
+                logger.info("New connector connection {} was created successfully.", connDesc.getName());
+            } catch (Exception e) {
+                logger.error("Creating of the connector {} was skipped.", connDesc.getName());
+                logger.catching(e);
+            }
+        }
+        return connectors;
+    }
+
+    /**
+     * Gets a generic parameters from the input class.
+     * Input class could be type of {@code ConnectorFetcher} or {@code ConnectorPusher}.
+     * @param aClass - class contain generic parameters.
+     * @return generic parameter extended from {@link AbstractModel}.
+     * @throws Exception throws if the model can not got as a generic parameter from the input class.
+     */
+    @SuppressWarnings("unchecked")
+    private Class<? extends AbstractModel> getAbstractModelFromGeneric(Class aClass) throws Exception {
+        Type[] classTypes = aClass.getGenericInterfaces();
+        if (classTypes.length == 0) {
+            throw logger.throwing(new Exception(format(
+                "%s does not implements any interface.", aClass
+            )));
+        }
+
+        Type interfaceType = classTypes[0];
+        if (!(interfaceType instanceof ParameterizedType)) {
+            throw logger.throwing(new Exception(format(
+                    "%s implemented interface does not contain generic parameters.", aClass
+            )));
+        }
+
+        ParameterizedType parameterizedInterfaceType = (ParameterizedType) interfaceType;
+        Type[] classArgumentTypes = parameterizedInterfaceType.getActualTypeArguments();
+        if (classArgumentTypes.length == 0) {
+            throw logger.throwing(new Exception(format(
+                    "%s implements empty generic parameters.", aClass
+            )));
+        }
+
+        Type classModelType; // TODO refactor
+        if (classArgumentTypes.length == 2) {
+            classModelType = classArgumentTypes[1];
+        } else {
+            classModelType = classArgumentTypes[0];
+        }
+
+        if (!(classModelType instanceof Class)) {
+            throw logger.throwing(new Exception(format(
+                    "%s contains generic parameters which are not instance of %s.", aClass, Class.class
+            )));
+        }
+
+        Class<?> classModel = (Class<?>) classModelType;
+        if (!AbstractModel.class.isAssignableFrom(classModel)) {
+            throw logger.throwing(new Exception(format(
+                    "%s does not contain generic parameters extended from %s", aClass, AbstractModel.class
+            )));
+        }
+
+       return (Class<? extends AbstractModel>) classModel;
+    }
+}

+ 45 - 0
connector-app/src/main/java/cz/senslog/connector/app/config/FileBuilderImpl.java

@@ -0,0 +1,45 @@
+// Copyright (c) 2026 UWB & LESP.
+// The UWB & LESP license this file to you under the BSD-3-Clause license.
+
+package cz.senslog.connector.app.config;
+
+import cz.senslog.connector.app.config.api.FileBuilder;
+import cz.senslog.connector.app.config.api.FileConfigurationService;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+/**
+ * The class {@code FileBuilderImpl} represents an implementation of {@link FileBuilder}.
+ *
+ * @author Lukas Cerny
+ * @version 1.0
+ * @since 1.0
+ */
+public class FileBuilderImpl implements FileBuilder {
+
+    private static final Logger logger = LogManager.getLogger(FileBuilderImpl.class);
+
+    /** Name of the file configuration. */
+    private String fileName;
+
+    /**
+     * Constructor.
+     */
+    public FileBuilderImpl() {
+        logger.debug("Creating a builder for the configuration service.");
+    }
+
+    @Override
+    public FileBuilder fileName(String fileName) {
+        this.fileName = fileName;
+        return this;
+    }
+
+    @Override
+    public FileConfigurationService build() {
+        logger.debug("Building a new FileConfigurationService");
+        FileConfigurationService service = new FileConfigurationServiceImpl(fileName);
+        logger.debug("FileConfigurationService was build successfully.");
+        return service;
+    }
+}

+ 257 - 0
connector-app/src/main/java/cz/senslog/connector/app/config/FileConfigurationServiceImpl.java

@@ -0,0 +1,257 @@
+// Copyright (c) 2026 UWB & LESP.
+// The UWB & LESP license this file to you under the BSD-3-Clause license.
+
+package cz.senslog.connector.app.config;
+
+import cz.senslog.connector.app.config.api.FileConfigurationService;
+import cz.senslog.connector.model.config.ConnectorDescriptor;
+import cz.senslog.connector.model.config.DefaultConfig;
+import cz.senslog.connector.tools.exception.UnsupportedFileException;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.yaml.snakeyaml.Yaml;
+
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.time.LocalTime;
+import java.util.*;
+
+import static java.time.format.DateTimeFormatter.ofPattern;
+
+/**
+ * The class {@code FileConfigurationServiceImpl} represents an implementation of {@link FileConfigurationService}.
+ * Configuration file is in YAML format and contains two major groups: 'settings' and 'connectors'.
+ * <h2>connectors</h2>
+ * Each connector must contain following attributes:
+ *  - name of connector
+ *  - ID of fetch provider (ID is mentioned in 'settings' group)
+ *  - ID of push provider (ID is mentioned in 'settings' group)
+ *  - period in second when the connector will be scheduled
+ *  Example:
+ *      connectors:
+ *          - ConnectorName:
+ *              fetcher: "<id>"
+ *              pusher: "<id>"
+ *              period: 60
+ * <h2>settings</h2>
+ * Each provider must contain basic attributes:
+ *  - identifier of provider
+ *  - name of provider
+ *  - provider class
+ *  Other attributes are dynamically loaded when they are needed
+ *  but must keep the key and value syntax.
+ *  Example:
+ *      settings:
+ *          - ProviderID:
+ *              name: "<name>"
+ *              provider: "cz.senslog.connector.ClassProvider"
+ *
+ *
+ * @author Lukas Cerny
+ * @version 1.0
+ * @since 1.0
+ */
+class FileConfigurationServiceImpl extends ConfigurationServiceImpl implements FileConfigurationService {
+
+    private static final Logger logger = LogManager.getLogger(FileConfigurationServiceImpl.class);
+
+    /** Name of the configuration file. */
+    private final String fileName;
+
+    /**
+     * Constructors sets all attributes.
+     * @param fileName - name of the configuration file.
+     */
+    FileConfigurationServiceImpl(String fileName) {
+        logger.debug("Creating a new FileConfigurationService.");
+        this.fileName = fileName;
+    }
+
+    @Override
+    public void load() throws IOException {
+        logger.info("Loading '{}' configuration file.", fileName);
+
+        if (!fileName.toLowerCase().endsWith(".yaml")) {
+            throw new UnsupportedFileException(fileName + "does not contain .yaml extension.");
+        }
+
+        Path filePath = Paths.get(fileName);
+        if (Files.notExists(filePath)) {
+            throw new FileNotFoundException(fileName + " does not exist");
+        }
+
+        Map<Object, Object> properties;
+
+        logger.debug("Opening the file '{}'.", fileName);
+        try (InputStream fileStream = Files.newInputStream(filePath)) {
+            logger.debug("Parsing the yaml file '{}'.", fileName);
+            properties = new Yaml().load(fileStream);
+            logger.debug("The configuration yaml file '{}' was parsed successfully.", fileName);
+        }
+
+        if (properties == null || properties.isEmpty()) {
+            throw new IOException(String.format(
+                    "The configuration yaml file %s is empty or was not loaded successfully. ", fileName
+            ));
+        }
+
+        logger.debug("Getting 'settings' property from the configuration file.");
+        List<?> settingsList = (List<?>)properties.get("settings");
+
+        logger.debug("Getting 'connectors' property from the configuration file.");
+        List<?> connectorsList = (List<?>) properties.get("connectors");
+
+        logger.debug("Starting to parse all connector descriptors from the config file.");
+        settings(settingsList);
+
+        logger.debug("Starting to create all connector connection from the configuration file.");
+        createConnectorDescriptors(connectorsList);
+
+        logger.info("The configuration file '{}' was parsed successfully.", fileName);
+    }
+
+    private void settings(List<?> settingsList) throws InvalidPropertiesFormatException {
+
+        logger.debug("Parsing 'settings' from the configuration file.");
+        for (Object settings : settingsList) {
+            if (!(settings instanceof Map)) {
+                throw logger.throwing(new InvalidPropertiesFormatException(
+                        "Property 'settings' is not in the valid format."
+                ));
+            }
+
+            Map<?, ?> settingsMap = (Map<?, ?>) settings;
+            for (Object settingsEntryObject : settingsMap.entrySet()) {
+                if (!(settingsEntryObject instanceof Map.Entry)) {
+                    throw logger.throwing(new InvalidPropertiesFormatException(
+                            "Values in property 'settings' are not accessible as a dictionary."
+                    ));
+                }
+
+                Map.Entry<?, ?> settingsEntry = (Map.Entry<?, ?>) settingsEntryObject;
+
+                String descriptorId = (String) settingsEntry.getKey();
+
+                logger.debug("Getting descriptor for the settings ID '{}'.", descriptorId);
+                Object settingsValuesObject = settingsEntry.getValue();
+                if (!(settingsValuesObject instanceof Map)) {
+                    throw logger.throwing(new InvalidPropertiesFormatException(
+                            "Values for the descriptor '"+descriptorId+"' are not accessible as a dictionary."
+                    ));
+                }
+
+                Map<?, ?> settingsValuesMap = (Map<?, ?>) settingsValuesObject;
+
+                logger.debug("Getting property 'provider' from the settings descriptor '{}'.", descriptorId);
+                String providerClassStr = (String) settingsValuesMap.get("provider");
+                if (providerClassStr == null) {
+                    throw logger.throwing(new NoSuchElementException(
+                            "Property 'provider' was not found."
+                    ));
+                }
+                settingsValuesMap.remove("provider");
+
+                Class<?> providerClass;
+                try {
+                    logger.debug("Creating a class from the provider class name {}.", providerClassStr);
+                    providerClass = Class.forName(providerClassStr);
+                } catch (ClassNotFoundException e) {
+                    logger.catching(e);
+                    continue;
+                }
+
+                logger.debug("Creating a new DefaultConfig class for the settings descriptor '{}'.", descriptorId);
+                DefaultConfig defaultConfig = new DefaultConfig(descriptorId, providerClass);
+
+                logger.debug("Starting to set all properties from the settings descriptor '{}'.", descriptorId);
+                for (Object propertyEntryObject : settingsValuesMap.entrySet()) {
+                    if (!(propertyEntryObject instanceof Map.Entry)) {
+                        throw logger.throwing(new InvalidPropertiesFormatException(
+                                        "Property values in the descriptor '"+descriptorId+"' are not accessible as a dictionary."
+                        ));
+                    }
+
+                    Map.Entry<?, ?> propertyEntry = (Map.Entry<?, ?>) propertyEntryObject;
+
+                    logger.trace("Setting property '{}' from the settings descriptor '{}'.", propertyEntry.getKey(), descriptorId);
+                    defaultConfig.setProperty((String) propertyEntry.getKey(), propertyEntry.getValue());
+                }
+
+                logger.debug("Saving the settings descriptor '{}'.", descriptorId);
+                addProviderConfiguration(descriptorId, defaultConfig);
+
+            }
+        }
+    }
+
+    private void createConnectorDescriptors(List<?> connectorsList) throws InvalidPropertiesFormatException {
+
+        logger.debug("Parsing 'connectors' from the configuration file.");
+        for (Object connector : connectorsList) {
+            if (!(connector instanceof Map)) {
+                throw logger.throwing(new InvalidPropertiesFormatException(
+                        "Property 'connectors' is not in the valid format."
+                ));
+            }
+
+            Map<?, ?> connectorMap = (Map<?, ?>) connector;
+
+            for (Object connectorEntryObject : connectorMap.entrySet()) {
+                if (!(connectorEntryObject instanceof Map.Entry)) {
+                    throw logger.throwing(new InvalidPropertiesFormatException(
+                            "Values in property 'connectors' are not accessible as a dictionary."
+                    ));
+                }
+
+                Map.Entry<?,?> connectorEntry = (Map.Entry<?,?>) connectorEntryObject;
+
+                String descriptorId = (String) connectorEntry.getKey();
+
+                logger.debug("Getting descriptor for the connector ID '{}'.", descriptorId);
+                Object connectorValuesObject = connectorEntry.getValue();
+                if (!(connectorValuesObject instanceof Map)) {
+                    throw logger.throwing(new InvalidPropertiesFormatException(
+                            "Values for the descriptor '"+descriptorId+"' are not accessible as a dictionary."
+                    ));
+                }
+
+                Map<?,?> connectorValuesMap = (Map<?,?>) connectorValuesObject;
+
+                logger.debug("Getting the fetch class provider for the connector ID '{}'.", descriptorId);
+                String fetchProviderId = (String) connectorValuesMap.get("fetcher");
+                if (fetchProviderId == null) {
+                    throw logger.throwing(new NoSuchElementException(
+                            "Property 'fetcher' does not exist in connector descriptor '"+descriptorId+"'."
+                    ));
+                }
+
+                logger.debug("Getting the push class provider for the connector ID '{}'.", descriptorId);
+                String pushProviderId = (String) connectorValuesMap.get("pusher");
+                if (pushProviderId == null) {
+                    throw logger.throwing(new NoSuchElementException(
+                            "Property 'pusher' does not exist in connector descriptor '"+descriptorId+"'."
+                    ));
+                }
+
+                logger.debug("Getting property 'period' from the connector descriptor '{}'.", descriptorId);
+                Integer period = (Integer) connectorValuesMap.get("period");
+
+                logger.debug("Getting property 'initDelay' from the connector descriptor '{}'.", descriptorId);
+                Integer delay = connectorValuesMap.containsKey("initDelay") ? (Integer) connectorValuesMap.get("initDelay") : null;
+
+                logger.debug("Getting property 'startAt' from the connector descriptor '{}'.", descriptorId);
+                LocalTime startAt = connectorValuesMap.containsKey("startAt") ? LocalTime.parse((String)connectorValuesMap.get("startAt"), ofPattern("HH:mm:ss")) : null;
+
+                logger.debug("Creating a new ConnectorDescriptor class for the connector descriptor '{}'.", descriptorId);
+                ConnectorDescriptor connDesc = new ConnectorDescriptor(descriptorId, fetchProviderId, pushProviderId, period, delay, startAt);
+
+                logger.debug("Saving the connector descriptor '{}'.", descriptorId);
+                addConnectorDescriptor(connDesc);
+            }
+        }
+    }
+}

+ 83 - 0
connector-app/src/main/java/cz/senslog/connector/app/config/Parameters.java

@@ -0,0 +1,83 @@
+// Copyright (c) 2026 UWB & LESP.
+// The UWB & LESP license this file to you under the BSD-3-Clause license.
+
+package cz.senslog.connector.app.config;
+
+import com.beust.jcommander.JCommander;
+import com.beust.jcommander.Parameter;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.util.Arrays;
+
+import static cz.senslog.connector.tools.util.StringUtils.isNotBlank;
+import static java.lang.String.format;
+import static java.nio.file.Files.notExists;
+import static java.nio.file.Paths.get;
+
+/**
+ * The class {@code Parameters} represents input parameters from
+ * the applications. For parsing is used {@see JCommander} library.
+ *
+ * @author Lukas Cerny
+ * @version 1.0
+ * @since 1.0
+ */
+public final class Parameters {
+
+    private static final Logger logger = LogManager.getLogger(Parameters.class);
+
+    private JCommander jCommander;
+
+    /**
+     * Static method to parse input parameters.
+     * @param appConfig - main configuration of the application.
+     * @param args - array of parameters in format e.g. ["-cf", "fileName"].
+     * @return instance of {@code Parameters}.
+     * @throws IOException throws if is chosen "-cf" or "-config-file" parameter and the file does not exist.
+     */
+    public static Parameters parse(AppConfig appConfig, String... args) throws IOException {
+        logger.debug("Parsing input parameters {}", Arrays.toString(args));
+
+        Parameters parameters = new Parameters();
+        JCommander jCommander = JCommander.newBuilder()
+                .programName(appConfig.getName())
+                .addObject(parameters).build();
+        parameters.jCommander = jCommander;
+
+        jCommander.parse(args);
+
+        String configFileName = parameters.getConfigFileName();
+        logger.debug("Checking existence of configuration file {}", configFileName);
+        if (isNotBlank(configFileName) && notExists(get(configFileName))) {
+            throw new FileNotFoundException(format("Config file %s does not exist.", configFileName));
+        }
+
+        logger.info("Parsing input parameters {} were parsed successfully.", Arrays.toString(args));
+        return parameters;
+    }
+
+    @Parameter(names = {"-h", "-help"}, help = true)
+    private boolean help = false;
+
+    @Parameter(names = {"-cf", "-config-file"}, description = "Configuration file in .yaml format.")
+    private String configFileName;
+
+    /**
+     * Returns name of the configuration file.
+     * @return string name.
+     */
+    public String getConfigFileName() {
+        return configFileName;
+    }
+
+    public boolean isHelp() {
+        return help;
+    }
+
+    public void printHelp() {
+        jCommander.usage();
+    }
+}

+ 56 - 0
connector-app/src/main/java/cz/senslog/connector/app/config/ServiceProvider.java

@@ -0,0 +1,56 @@
+// Copyright (c) 2026 UWB & LESP.
+// The UWB & LESP license this file to you under the BSD-3-Clause license.
+
+package cz.senslog.connector.app.config;
+
+import cz.senslog.connector.fetch.api.ConnectorFetchProvider;
+import cz.senslog.connector.push.api.ConnectorPushProvider;
+
+import java.util.function.Function;
+
+/**
+ * The class {@code ServiceProvider} represents a wrapper for
+ * fetch and push providers.
+ *
+ * @author Lukas Cerny
+ * @version 1.0
+ * @since 1.0
+ */
+public final class ServiceProvider {
+
+    /** Function provides fetcher instance of input class. */
+    private final Function<Class<?>, ConnectorFetchProvider> fetchProviderFnc;
+
+    /** Function provides pusher instance of input class. */
+    private final Function<Class<?>, ConnectorPushProvider> pushProviderFnc;
+
+    /**
+     * Constructor allows to set all attributes.
+     * @param fetchProviderFnc - function to provider fetch instance.
+     * @param pushProviderFnc - function to provide push instance.
+     */
+    public ServiceProvider(Function<Class<?>, ConnectorFetchProvider> fetchProviderFnc,
+                           Function<Class<?>, ConnectorPushProvider> pushProviderFnc
+    ) {
+        this.fetchProviderFnc = fetchProviderFnc;
+        this.pushProviderFnc = pushProviderFnc;
+    }
+
+    /**
+     * Returns fetch instance depends on input class.
+     * @param providerClass - class of fetch.
+     * @return instance of fetch.
+     */
+    public ConnectorFetchProvider getFetchProvider(Class<?> providerClass) {
+        return fetchProviderFnc.apply(providerClass);
+    }
+
+    /**
+     * Returns push instance depends on input class.
+     * @param providerClass - class of push.
+     * @return instance of push.
+     */
+    public ConnectorPushProvider getPushProvider(Class<?> providerClass) {
+        return pushProviderFnc.apply(providerClass);
+    }
+}

+ 44 - 0
connector-app/src/main/java/cz/senslog/connector/app/config/api/ConfigurationService.java

@@ -0,0 +1,44 @@
+// Copyright (c) 2026 UWB & LESP.
+// The UWB & LESP license this file to you under the BSD-3-Clause license.
+
+package cz.senslog.connector.app.config.api;
+
+import cz.senslog.connector.app.config.FileBuilderImpl;
+import cz.senslog.connector.model.config.ConnectorDescriptor;
+import cz.senslog.connector.model.config.DefaultConfig;
+
+import java.util.Set;
+
+/**
+ * The interface {@code ConfigurationService} provides a generic service for configuration.
+ * Configuration can be gotten from a file, database or anything else.
+ * Provides two crucial functionalities:
+ *  - returns set of connector descriptors
+ *  - returns configuration for a class
+ *
+ * @author Lukas Cerny
+ * @version 1.0
+ * @since 1.0
+ */
+public interface ConfigurationService {
+
+    /**
+     * Creates a builder for a configuration from a file.
+     * @return new instance of {@link FileBuilder}.
+     */
+    static FileBuilder newFileBuilder() {
+        return new FileBuilderImpl();
+    }
+
+    /**
+     * @return set of connector descriptors.
+     */
+    Set<ConnectorDescriptor> getConnectorDescriptors();
+
+    /**
+     * Returns a configuration depends on a provider id.
+     * @param providerId - identifier of provider.
+     * @return default configuration for an input provider id.
+     */
+    DefaultConfig getConfigForProviderId(String providerId);
+}

+ 28 - 0
connector-app/src/main/java/cz/senslog/connector/app/config/api/FileBuilder.java

@@ -0,0 +1,28 @@
+// Copyright (c) 2026 UWB & LESP.
+// The UWB & LESP license this file to you under the BSD-3-Clause license.
+
+package cz.senslog.connector.app.config.api;
+
+/**
+ * The interface {@code FileBuilder} provides a configuration
+ * to load a configuration file.
+ *
+ * @author Lukas Cerny
+ * @version 1.0
+ * @since 1.0
+ */
+public interface FileBuilder {
+
+    /**
+     * Sets name of file to the configuration.
+     * @param fileName - name of configuration file.
+     * @return instance of builder {@code FileBuilder}.
+     */
+    FileBuilder fileName(String fileName);
+
+    /**
+     * Creates a new instance with the configuration.
+     * @return new instance of {@link FileConfigurationService}.
+     */
+    FileConfigurationService build();
+}

+ 25 - 0
connector-app/src/main/java/cz/senslog/connector/app/config/api/FileConfigurationService.java

@@ -0,0 +1,25 @@
+// Copyright (c) 2026 UWB & LESP.
+// The UWB & LESP license this file to you under the BSD-3-Clause license.
+
+package cz.senslog.connector.app.config.api;
+
+import java.io.IOException;
+
+/**
+ * The interface {@code FileConfigurationService} provides functionality
+ * which is mandatory only for a file configuration.
+ *
+ * @author Lukas Cerny
+ * @version 1.0
+ * @since 1.0
+ */
+public interface FileConfigurationService extends ConfigurationService {
+
+    /**
+     * Loads and parses the configuration file.
+     * From the configuration file is loaded configuration for each class
+     * and also connector description which is used to create a new one.
+     * @throws IOException throws if the configuration file is not loaded correctly.
+     */
+    void load() throws IOException;
+}

+ 2 - 0
connector-app/src/main/resources/project.properties

@@ -0,0 +1,2 @@
+app.name=${project.parent.name}
+app.version=${project.parent.version}

+ 271 - 0
connector-app/src/test/java/cz/senslog/connector/app/config/ConnectorBuilderTest.java

@@ -0,0 +1,271 @@
+// Copyright (c) 2026 UWB & LESP.
+// The UWB & LESP license this file to you under the BSD-3-Clause license.
+
+package cz.senslog.connector.app.config;
+
+import cz.senslog.connector.app.config.api.ConfigurationService;
+import cz.senslog.connector.fetch.api.ConnectorFetchProvider;
+import cz.senslog.connector.fetch.api.ConnectorFetcher;
+import cz.senslog.connector.fetch.api.ExecutableFetcher;
+import cz.senslog.connector.model.api.AbstractModel;
+import cz.senslog.connector.model.api.Converter;
+import cz.senslog.connector.model.api.ConverterProvider;
+import cz.senslog.connector.model.api.ProxySessionModel;
+import cz.senslog.connector.model.config.ConnectorDescriptor;
+import cz.senslog.connector.model.config.DefaultConfig;
+import cz.senslog.connector.push.api.ConnectorPushProvider;
+import cz.senslog.connector.push.api.ConnectorPusher;
+import org.junit.jupiter.api.Test;
+
+import java.util.HashSet;
+import java.util.Optional;
+import java.util.Set;
+
+import static java.time.OffsetDateTime.MAX;
+import static java.time.OffsetDateTime.MIN;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+class ConnectorBuilderTest {
+
+    private static class InputModel extends AbstractModel{ InputModel() { super(MIN, MAX); }}
+    private static class OutputModel extends AbstractModel{ OutputModel() { super(MIN, MAX);}}
+
+    private static class BasicSessionModel extends ProxySessionModel {
+        public BasicSessionModel() { super(false); }
+    }
+
+    private static class TestingFetcher implements ConnectorFetcher<BasicSessionModel, OutputModel> {
+        @Override public void init() { }
+        @Override public OutputModel fetch(Optional<BasicSessionModel> session) { return new OutputModel(); }
+    }
+
+    private static class TestingPusher implements ConnectorPusher<InputModel> {
+        @Override public void init() {}
+        @Override public void push(InputModel model) {}
+    }
+
+    private final ConnectorFetchProvider defaultFetchProvider = config -> ExecutableFetcher.create(new TestingFetcher());
+
+    private final ConnectorPushProvider defaultPushProvider = config -> new TestingPusher();
+
+    @Test
+    void createConnectors_CreateTestConnector_CreatedOneConnector() {
+
+        ConverterProvider converterProvider = mock(ConverterProvider.class);
+        when(converterProvider.getConverter(OutputModel.class, InputModel.class)).thenReturn(
+                (Converter<OutputModel, InputModel>) model -> new InputModel());
+
+        ServiceProvider serviceProvider = new ServiceProvider(aClass -> defaultFetchProvider, aClass -> defaultPushProvider);
+
+        Set<ConnectorDescriptor> connectorDescriptors = new HashSet<>();
+        connectorDescriptors.add(new ConnectorDescriptor("Test", "FetcherId", "PusherId", 1, 2, null));
+
+        ConfigurationService configService = mock(ConfigurationService.class);
+        when(configService.getConnectorDescriptors()).thenReturn(connectorDescriptors);
+        when(configService.getConfigForProviderId("FetcherId")).thenReturn(mock(DefaultConfig.class));
+        when(configService.getConfigForProviderId("PusherId")).thenReturn(mock(DefaultConfig.class));
+
+        Set<Connector> connectors = ConnectorBuilder.init(serviceProvider, converterProvider, configService).createConnectors();
+
+        assertEquals(1, connectors.size());
+
+        Connector connector = connectors.iterator().next();
+        assertEquals("Test", connector.getName());
+        assertEquals(1_000, connector.getPeriod().orElse(0));
+        assertEquals(2_000, connector.getInitDelay().orElse(0));
+    }
+
+    @Test
+    void createConnectors_CreateTwoConnectors_CreatedTwoConnectors() {
+
+        ConverterProvider converterProvider = mock(ConverterProvider.class);
+        when(converterProvider.getConverter(OutputModel.class, InputModel.class)).thenReturn(
+                (Converter<OutputModel, InputModel>) model -> new InputModel());
+
+        ServiceProvider serviceProvider = new ServiceProvider(aClass -> defaultFetchProvider, aClass -> defaultPushProvider);
+
+        Set<ConnectorDescriptor> connectorDescriptors = new HashSet<>();
+        connectorDescriptors.add(new ConnectorDescriptor("Test1", "FetcherId", "PusherId", 1, 3, null));
+        connectorDescriptors.add(new ConnectorDescriptor("Test2", "FetcherId", "PusherId", 2, 3, null));
+
+        ConfigurationService configService = mock(ConfigurationService.class);
+        when(configService.getConnectorDescriptors()).thenReturn(connectorDescriptors);
+        when(configService.getConfigForProviderId("FetcherId")).thenReturn(mock(DefaultConfig.class));
+        when(configService.getConfigForProviderId("PusherId")).thenReturn(mock(DefaultConfig.class));
+
+        Set<Connector> connectors = ConnectorBuilder.init(serviceProvider, converterProvider, configService).createConnectors();
+
+        assertEquals(2, connectors.size());
+    }
+
+    @Test
+    void createConnectors_FetchProviderNull_ZeroConnectors() {
+
+        ConverterProvider converterProvider = mock(ConverterProvider.class);
+        when(converterProvider.getConverter(OutputModel.class, InputModel.class)).thenReturn(
+                (Converter<OutputModel, InputModel>) model -> new InputModel());
+
+        // fetch provider class does not exist -> null
+        ServiceProvider serviceProvider = new ServiceProvider(aClass -> null, aClass -> defaultPushProvider);
+
+        Set<ConnectorDescriptor> connectorDescriptors = new HashSet<>();
+        connectorDescriptors.add(new ConnectorDescriptor("Test", "FetcherId", "PusherId", 1, 2, null));
+
+        ConfigurationService configService = mock(ConfigurationService.class);
+        when(configService.getConnectorDescriptors()).thenReturn(connectorDescriptors);
+        when(configService.getConfigForProviderId("PusherId")).thenReturn(mock(DefaultConfig.class));
+
+        Set<Connector> connectors = ConnectorBuilder.init(serviceProvider, converterProvider, configService).createConnectors();
+
+        assertEquals(0, connectors.size());
+    }
+
+    @Test
+    void createConnectors_FetchConfigNull_ZeroConnectors() {
+
+        ConverterProvider converterProvider = mock(ConverterProvider.class);
+        when(converterProvider.getConverter(OutputModel.class, InputModel.class)).thenReturn(
+                (Converter<OutputModel, InputModel>) model -> new InputModel());
+
+        ServiceProvider serviceProvider = new ServiceProvider(aClass -> defaultFetchProvider, aClass -> defaultPushProvider);
+
+        Set<ConnectorDescriptor> connectorDescriptors = new HashSet<>();
+        connectorDescriptors.add(new ConnectorDescriptor("Test", "FetcherId", "PusherId", 1, 2, null));
+
+        ConfigurationService configService = mock(ConfigurationService.class);
+        when(configService.getConnectorDescriptors()).thenReturn(connectorDescriptors);
+        // fetch provider configuration does not exist -> null
+        when(configService.getConfigForProviderId("FetcherId")).thenReturn(null);
+        when(configService.getConfigForProviderId("PusherId")).thenReturn(mock(DefaultConfig.class));
+
+        Set<Connector> connectors = ConnectorBuilder.init(serviceProvider, converterProvider, configService).createConnectors();
+
+        assertEquals(0, connectors.size());
+    }
+
+    @Test
+    void createConnectors_PushProviderNull_ZeroConnectors() {
+
+        ConverterProvider converterProvider = mock(ConverterProvider.class);
+        when(converterProvider.getConverter(OutputModel.class, InputModel.class)).thenReturn(
+                (Converter<OutputModel, InputModel>) model -> new InputModel());
+
+        // push provider is set to null
+        ServiceProvider serviceProvider = new ServiceProvider(aClass -> defaultFetchProvider, aClass -> null);
+
+        Set<ConnectorDescriptor> connectorDescriptors = new HashSet<>();
+        connectorDescriptors.add(new ConnectorDescriptor("Test", "FetcherId", "PusherId", 1, 2, null));
+
+        ConfigurationService configService = mock(ConfigurationService.class);
+        when(configService.getConnectorDescriptors()).thenReturn(connectorDescriptors);
+        when(configService.getConfigForProviderId("FetcherId")).thenReturn(mock(DefaultConfig.class));
+
+        Set<Connector> connectors = ConnectorBuilder.init(serviceProvider, converterProvider, configService).createConnectors();
+
+        assertEquals(0, connectors.size());
+    }
+
+    @Test
+    void createConnectors_PushConfigNull_ZeroConnectors() {
+
+        ConverterProvider converterProvider = mock(ConverterProvider.class);
+        when(converterProvider.getConverter(OutputModel.class, InputModel.class)).thenReturn(
+                (Converter<OutputModel, InputModel>) model -> new InputModel());
+
+        ServiceProvider serviceProvider = new ServiceProvider(aClass -> defaultFetchProvider, aClass -> defaultPushProvider);
+
+        Set<ConnectorDescriptor> connectorDescriptors = new HashSet<>();
+        connectorDescriptors.add(new ConnectorDescriptor("Test", "FetcherId", "PusherId", 1, 2, null));
+
+        ConfigurationService configService = mock(ConfigurationService.class);
+        when(configService.getConnectorDescriptors()).thenReturn(connectorDescriptors);
+        when(configService.getConfigForProviderId("FetcherId")).thenReturn(mock(DefaultConfig.class));
+
+        // fetch provider configuration does not exist -> null
+        when(configService.getConfigForProviderId("PusherId")).thenReturn(null);
+
+        Set<Connector> connectors = ConnectorBuilder.init(serviceProvider, converterProvider, configService).createConnectors();
+
+        assertEquals(0, connectors.size());
+    }
+
+    @Test
+    void createConnectors_ConverterNull_CreatedZeroConnector() {
+
+        ConverterProvider converterProvider = mock(ConverterProvider.class);
+        // converter does not exist -> null
+        when(converterProvider.getConverter(OutputModel.class, InputModel.class)).thenReturn(null);
+
+        ServiceProvider serviceProvider = new ServiceProvider(aClass -> defaultFetchProvider, aClass -> defaultPushProvider);
+
+        Set<ConnectorDescriptor> connectorDescriptors = new HashSet<>();
+        connectorDescriptors.add(new ConnectorDescriptor("Test", "FetcherId", "PusherId", 1, 2, null));
+
+        ConfigurationService configService = mock(ConfigurationService.class);
+        when(configService.getConnectorDescriptors()).thenReturn(connectorDescriptors);
+        when(configService.getConfigForProviderId("FetcherId")).thenReturn(mock(DefaultConfig.class));
+        when(configService.getConfigForProviderId("PusherId")).thenReturn(mock(DefaultConfig.class));
+
+        Set<Connector> connectors = ConnectorBuilder.init(serviceProvider, converterProvider, configService).createConnectors();
+
+        assertEquals(0, connectors.size());
+    }
+
+    @Test
+    void createConnectors_IncompatibleFetchModelClass_CreatedZeroConnector() {
+
+        // ConnectorFetcher does not contain model class as a generic parameter
+         ConnectorFetchProvider fetchProvider = config -> ExecutableFetcher.create(new ConnectorFetcher() {
+            @Override public void init() {}
+            @Override public AbstractModel fetch(Optional session) {return new OutputModel();}
+        });
+
+        ConverterProvider converterProvider = mock(ConverterProvider.class);
+        when(converterProvider.getConverter(OutputModel.class, InputModel.class)).thenReturn(
+                (Converter<OutputModel, InputModel>) model -> new InputModel());
+
+        ServiceProvider serviceProvider = new ServiceProvider(aClass -> fetchProvider, aClass -> defaultPushProvider);
+
+        Set<ConnectorDescriptor> connectorDescriptors = new HashSet<>();
+        connectorDescriptors.add(new ConnectorDescriptor("Test", "FetcherId", "PusherId", 1, 2, null));
+
+        ConfigurationService configService = mock(ConfigurationService.class);
+        when(configService.getConnectorDescriptors()).thenReturn(connectorDescriptors);
+        when(configService.getConfigForProviderId("FetcherId")).thenReturn(mock(DefaultConfig.class));
+        when(configService.getConfigForProviderId("PusherId")).thenReturn(mock(DefaultConfig.class));
+
+        Set<Connector> connectors = ConnectorBuilder.init(serviceProvider, converterProvider, configService).createConnectors();
+
+        assertEquals(0, connectors.size());
+    }
+
+    @Test
+    void createConnectors_IncompatiblePushModelClass_CreatedZeroConnector() {
+
+        // ConnectorPusher does not contain model class as a generic parameter
+        ConnectorPushProvider pushProvider = config -> new ConnectorPusher() {
+            @Override public void init() {}
+            @Override public void push(AbstractModel model) {}
+        };
+
+        ConverterProvider converterProvider = mock(ConverterProvider.class);
+        when(converterProvider.getConverter(OutputModel.class, InputModel.class)).thenReturn(
+                (Converter<OutputModel, InputModel>) model -> new InputModel());
+
+        ServiceProvider serviceProvider = new ServiceProvider(aClass -> defaultFetchProvider, aClass -> pushProvider);
+
+        Set<ConnectorDescriptor> connectorDescriptors = new HashSet<>();
+        connectorDescriptors.add(new ConnectorDescriptor("Test", "FetcherId", "PusherId", 1, 2, null));
+
+        ConfigurationService configService = mock(ConfigurationService.class);
+        when(configService.getConnectorDescriptors()).thenReturn(connectorDescriptors);
+        when(configService.getConfigForProviderId("FetcherId")).thenReturn(mock(DefaultConfig.class));
+        when(configService.getConfigForProviderId("PusherId")).thenReturn(mock(DefaultConfig.class));
+
+        Set<Connector> connectors = ConnectorBuilder.init(serviceProvider, converterProvider, configService).createConnectors();
+
+        assertEquals(0, connectors.size());
+    }
+}

+ 21 - 0
connector-app/src/test/java/cz/senslog/connector/app/config/FileBuilderImplTest.java

@@ -0,0 +1,21 @@
+// Copyright (c) 2026 UWB & LESP.
+// The UWB & LESP license this file to you under the BSD-3-Clause license.
+
+package cz.senslog.connector.app.config;
+
+import cz.senslog.connector.app.config.api.ConfigurationService;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+class FileBuilderImplTest {
+
+    @Test
+    void build() {
+
+        ConfigurationService service = ConfigurationService.newFileBuilder()
+                .fileName("test.yaml").build();
+
+        assertEquals(FileConfigurationServiceImpl.class, service.getClass());
+    }
+}

+ 77 - 0
connector-app/src/test/java/cz/senslog/connector/app/config/FileConfigurationServiceImplTest.java

@@ -0,0 +1,77 @@
+// Copyright (c) 2026 UWB & LESP.
+// The UWB & LESP license this file to you under the BSD-3-Clause license.
+
+package cz.senslog.connector.app.config;
+
+import cz.senslog.connector.app.config.api.FileConfigurationService;
+import cz.senslog.connector.model.config.ConnectorDescriptor;
+import cz.senslog.connector.model.config.DefaultConfig;
+import cz.senslog.connector.model.config.HostConfig;
+import cz.senslog.connector.tools.exception.UnsupportedFileException;
+import org.junit.jupiter.api.Test;
+
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.nio.file.Paths;
+import java.util.Set;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+class FileConfigurationServiceImplTest {
+
+
+    @Test
+    void testParsingConfigFile() throws IOException, ClassNotFoundException, URISyntaxException {
+
+        URI uri = ClassLoader.getSystemResource("test_valid_config.yaml").toURI();
+        String configFileName = Paths.get(uri).toString();
+
+        FileConfigurationService configService = new FileConfigurationServiceImpl(configFileName);
+        configService.load();
+
+        Set<ConnectorDescriptor> descriptors = configService.getConnectorDescriptors();
+
+        assertEquals(1, descriptors.size());
+
+        ConnectorDescriptor descriptor = descriptors.iterator().next();
+        assertEquals("ConnectorName", descriptor.getName());
+        assertEquals("FetchProviderId", descriptor.getFetcherId());
+        assertEquals("PushProviderId", descriptor.getPusherId());
+        assertEquals(1, descriptor.getPeriod());
+
+        DefaultConfig fetchConfig = configService.getConfigForProviderId("FetchProviderId");
+        assertEquals("FetchProviderId", fetchConfig.getId());
+        assertEquals(TestFetchProviderClass.class, fetchConfig.getProvider());
+        assertEquals("<name>", fetchConfig.getStringProperty("name"));
+        HostConfig fetchHost = new HostConfig(fetchConfig.getPropertyConfig("host"));
+        assertEquals("<fetcher_api_domain>", fetchHost.getDomain());
+        assertEquals("<path>", fetchHost.getPath());
+
+        DefaultConfig pushConfig = configService.getConfigForProviderId("PushProviderId");
+        assertEquals("PushProviderId", pushConfig.getId());
+        assertEquals(TestPushProviderClass.class, pushConfig.getProvider());
+        assertEquals("<name>", pushConfig.getStringProperty("name"));
+        HostConfig pushHost = new HostConfig(pushConfig.getPropertyConfig("host"));
+        assertEquals("<pusher_api_domain>", pushHost.getDomain());
+        assertEquals("<path>", pushHost.getPath());
+    }
+
+    @Test
+    void load_WrongFileNameExtension_UnsupportedFileException() {
+
+        FileConfigurationService service = new FileConfigurationServiceImpl("test.txt");
+
+        assertThrows(UnsupportedFileException.class, service::load);
+    }
+
+    @Test
+    void load_FileDoesNotExist_FileNotFoundException() {
+
+        FileConfigurationService service = new FileConfigurationServiceImpl("test.yaml");
+
+        assertThrows(FileNotFoundException.class, service::load);
+    }
+}

+ 56 - 0
connector-app/src/test/java/cz/senslog/connector/app/config/ParametersTest.java

@@ -0,0 +1,56 @@
+// Copyright (c) 2026 UWB & LESP.
+// The UWB & LESP license this file to you under the BSD-3-Clause license.
+
+package cz.senslog.connector.app.config;
+
+import com.beust.jcommander.ParameterException;
+import org.junit.jupiter.api.Test;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+class ParametersTest {
+
+    @Test
+    void parse_FileParams_True() throws Exception {
+
+        File yamlConfigFile = File.createTempFile("config_test_", ".yaml");
+        String [] params = new String[] {"-cf", yamlConfigFile.getAbsolutePath()};
+
+        AppConfig appConfig = mock(AppConfig.class);
+        when(appConfig.getName()).thenReturn("Test application.");
+
+        Parameters parameters = Parameters.parse(appConfig, params);
+
+        assertEquals(yamlConfigFile.getAbsolutePath(), parameters.getConfigFileName());
+    }
+
+    @Test
+    void parse_WrongParamName_ParameterException() throws Exception {
+
+        File yamlConfigFile = File.createTempFile("config_test_", ".yaml");
+        // 'cf' was changed to wrong parameter 'cp'
+        String [] params = new String[] {"-cp", yamlConfigFile.getAbsolutePath()};
+
+        AppConfig appConfig = mock(AppConfig.class);
+        when(appConfig.getName()).thenReturn("Test application.");
+
+        assertThrows(ParameterException.class, () -> Parameters.parse(appConfig, params));
+    }
+
+    @Test
+    void parse_FileDoesNotExist_FileNotFoundException() {
+
+        // Config file 'config.yaml' does not exist
+        String [] params = new String[] {"-cf", "config.yaml"};
+        AppConfig appConfig = mock(AppConfig.class);
+        when(appConfig.getName()).thenReturn("Test application.");
+
+        assertThrows(FileNotFoundException.class, () -> Parameters.parse(appConfig, params));
+    }
+}

+ 7 - 0
connector-app/src/test/java/cz/senslog/connector/app/config/TestFetchProviderClass.java

@@ -0,0 +1,7 @@
+// Copyright (c) 2026 UWB & LESP.
+// The UWB & LESP license this file to you under the BSD-3-Clause license.
+
+package cz.senslog.connector.app.config;
+
+public class TestFetchProviderClass {
+}

+ 7 - 0
connector-app/src/test/java/cz/senslog/connector/app/config/TestPushProviderClass.java

@@ -0,0 +1,7 @@
+// Copyright (c) 2026 UWB & LESP.
+// The UWB & LESP license this file to you under the BSD-3-Clause license.
+
+package cz.senslog.connector.app.config;
+
+public class TestPushProviderClass {
+}

+ 26 - 0
connector-app/src/test/resources/test_valid_config.yaml

@@ -0,0 +1,26 @@
+feMainDomain: &fetcherApiDomain
+  domain: "<fetcher_api_domain>"
+
+puMainDomain: &pusherApiDomain
+  domain: "<pusher_api_domain>"
+
+settings:
+  - FetchProviderId:
+      name: "<name>"
+      provider: "cz.senslog.connector.app.config.TestFetchProviderClass"
+      host:
+        <<: *fetcherApiDomain
+        path: "<path>"
+
+  - PushProviderId:
+      name: "<name>"
+      provider: "cz.senslog.connector.app.config.TestPushProviderClass"
+      host:
+        <<: *pusherApiDomain
+        path: "<path>"
+
+connectors:
+  - ConnectorName:
+      fetcher: "FetchProviderId"
+      pusher: "PushProviderId"
+      period: 1

+ 33 - 0
connector-fetch-api/license.txt

@@ -0,0 +1,33 @@
+BSD-3-Clause
+
+Copyright (c) 2020 University of West Bohemia (UWB) https://www.zcu.cz/en, LESPROJEKT-SLUŽBY s.r.o. (LESP) https://www.lesprojekt.cz/en
+
+Copyright 2026 <COPYRIGHT HOLDER>
+
+Redistribution and use in source and binary
+forms, with or without modification, are permitted provided that the following
+conditions are met:
+
+1. Redistributions of source code must retain the above
+copyright notice, this list of conditions and the following disclaimer.
+
+2.
+Redistributions in binary form must reproduce the above copyright notice, this
+list of conditions and the following disclaimer in the documentation and/or
+other materials provided with the distribution.
+
+3. Neither the name of the
+copyright holder nor the names of its contributors may be used to endorse or
+promote products derived from this software without specific prior written
+permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+“AS IS” AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
+TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

+ 33 - 0
connector-fetch-api/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">
+    <parent>
+        <artifactId>connector-period</artifactId>
+        <groupId>cz.senslog</groupId>
+        <version>1.0-SNAPSHOT</version>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+
+    <artifactId>connector-fetch-api</artifactId>
+    <name>fetch-api</name>
+    <packaging>jar</packaging>
+
+    <dependencies>
+        <dependency>
+            <groupId>cz.senslog</groupId>
+            <artifactId>connector-model</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>

+ 55 - 0
connector-fetch-api/src/main/java/cz/senslog/connector/fetch/ConnectorFetch.java

@@ -0,0 +1,55 @@
+// Copyright (c) 2026 UWB & LESP.
+// The UWB & LESP license this file to you under the BSD-3-Clause license.
+
+package cz.senslog.connector.fetch;
+
+import cz.senslog.connector.fetch.api.ConnectorFetchProvider;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+import java.util.*;
+
+/**
+ * The class {@code ConnectorFetch} represents a loader for classes implement {@link ConnectorFetchProvider}.
+ * For this is used technology Java Service Provider Interface (SPI).
+ *
+ * @author Lukas Cerny
+ * @version 1.0
+ * @since 1.0
+ */
+public final class ConnectorFetch {
+
+    private static final Logger logger = LogManager.getLogger(ConnectorFetch.class);
+
+    /** Map of implementations. */
+    private static final Map<Class<? extends ConnectorFetchProvider>, ConnectorFetchProvider> services;
+
+    static {
+        services = loadAll();
+    }
+
+    /**
+     * Loads and saves all available implementations of {@link ConnectorFetchProvider}.
+     * @return Map of all implementations.
+     */
+    private static Map<Class<? extends ConnectorFetchProvider>, ConnectorFetchProvider> loadAll() {
+        logger.debug("Getting all implementation of the class {}.", ConnectorFetchProvider.class);
+        Map<Class<? extends ConnectorFetchProvider>, ConnectorFetchProvider> services = new HashMap<>();
+        ServiceLoader<ConnectorFetchProvider> loader = ServiceLoader.load(ConnectorFetchProvider.class);
+        for (ConnectorFetchProvider connectorProvider : loader) {
+            logger.debug("Loaded the class {}.", connectorProvider.getClass());
+            services.put(connectorProvider.getClass(), connectorProvider);
+        }
+        logger.info("Successfully loaded {} class of the {}.",  services.size(), ConnectorFetchProvider.class);
+        return services;
+    }
+
+    /**
+     * Returns an implementation of provider according to input class.
+     * @param providerClass - class which is find an implementation.
+     * @return implementation type to {@link ConnectorFetchProvider}.
+     */
+    public static ConnectorFetchProvider getProvider(Class<?> providerClass) {
+        return services.get(providerClass);
+    }
+}

+ 25 - 0
connector-fetch-api/src/main/java/cz/senslog/connector/fetch/api/ConnectorFetchProvider.java

@@ -0,0 +1,25 @@
+// Copyright (c) 2026 UWB & LESP.
+// The UWB & LESP license this file to you under the BSD-3-Clause license.
+
+package cz.senslog.connector.fetch.api;
+
+import cz.senslog.connector.model.api.AbstractModel;
+import cz.senslog.connector.model.config.DefaultConfig;
+
+/**
+ * The interface {@code ConnectorFetchProvider} provides a generic communication interface to create a new fetcher.
+ *
+ * @author Lukas Cerny
+ * @version 1.0
+ * @since 1.0
+ */
+public interface ConnectorFetchProvider {
+
+    /**
+     * Creates a new instance of {@link ExecutableFetcher}. This method receive default
+     * configuration {@link DefaultConfig} which is used to configure the new instance of a fetcher executor.
+     * @param defaultConfig - default configuration.
+     * @return new instance of {@link ConnectorFetcher}.
+     */
+    ExecutableFetcher<? extends AbstractModel> createExecutableFetcher(DefaultConfig defaultConfig);
+}

+ 33 - 0
connector-fetch-api/src/main/java/cz/senslog/connector/fetch/api/ConnectorFetcher.java

@@ -0,0 +1,33 @@
+// Copyright (c) 2026 UWB & LESP.
+// The UWB & LESP license this file to you under the BSD-3-Clause license.
+
+package cz.senslog.connector.fetch.api;
+
+import cz.senslog.connector.model.api.AbstractModel;
+import cz.senslog.connector.model.api.ProxySessionModel;
+
+import java.util.Optional;
+
+/**
+ * The interface {@code ConnectorFetcher} provides a generic communication interface for fetchers.
+ *
+ * @param <T> generic parameter of model which the method 'fetch' sens as an output.
+ *
+ * @author Lukas Cerny
+ * @version 1.0
+ * @since 1.0
+ */
+public interface ConnectorFetcher<S extends ProxySessionModel, T extends AbstractModel> {
+
+    /**
+     * Initialization of fetcher. Method is called only once when is created a new connector.
+     * @throws Exception throws when initialization is not successful.
+     */
+    void init() throws Exception;
+
+    /**
+     * Method is periodically scheduled and contains logic of fetcher.
+     * @return model of data which was fetched.
+     */
+    T fetch(Optional<S> session);
+}

+ 48 - 0
connector-fetch-api/src/main/java/cz/senslog/connector/fetch/api/ExecutableFetcher.java

@@ -0,0 +1,48 @@
+// Copyright (c) 2026 UWB & LESP.
+// The UWB & LESP license this file to you under the BSD-3-Clause license.
+
+package cz.senslog.connector.fetch.api;
+
+import cz.senslog.connector.model.api.AbstractModel;
+import cz.senslog.connector.model.api.ProxySessionModel;
+
+import java.util.Optional;
+
+/**
+ * The class contains methods to create executable fetcher (i.e., {@link ConnectorFetcher})
+ *
+ * @param <T> model representing the fetched data as an inherited class of the {@link AbstractModel}
+ */
+public class ExecutableFetcher<T extends AbstractModel> {
+
+    private final ConnectorFetcher<? extends ProxySessionModel, T> rawFetcher;
+
+    private final ConnectorFetcher<? extends ProxySessionModel, T> fetcher;
+
+    public static <S extends ProxySessionModel, M extends AbstractModel> ExecutableFetcher<M> create(
+            ConnectorFetcher<S, M> rawFetcher
+    ) {
+        return new ExecutableFetcher<>(rawFetcher, rawFetcher);
+    }
+
+    /**
+     * Private constructor sets fetchers. If the session is disabled, then 'rawFetcher' and 'fetcher' are the same instances.
+     * @param rawFetcher fetcher that retrieves data
+     * @param fetcher fetcher that is wrap by session if enabled.
+     */
+    private ExecutableFetcher(
+            ConnectorFetcher<? extends ProxySessionModel, T> rawFetcher,
+            ConnectorFetcher<? extends ProxySessionModel, T> fetcher
+    ) {
+        this.rawFetcher = rawFetcher;
+        this.fetcher = fetcher;
+    }
+
+    public T execute() {
+        return fetcher.fetch(Optional.empty());
+    }
+
+    public ConnectorFetcher<? extends ProxySessionModel, T> getRawFetcher() {
+        return rawFetcher;
+    }
+}

+ 33 - 0
connector-fetch-openmeteo/license.txt

@@ -0,0 +1,33 @@
+BSD-3-Clause
+
+Copyright (c) 2020 University of West Bohemia (UWB) https://www.zcu.cz/en, LESPROJEKT-SLUŽBY s.r.o. (LESP) https://www.lesprojekt.cz/en
+
+Copyright 2026 <COPYRIGHT HOLDER>
+
+Redistribution and use in source and binary
+forms, with or without modification, are permitted provided that the following
+conditions are met:
+
+1. Redistributions of source code must retain the above
+copyright notice, this list of conditions and the following disclaimer.
+
+2.
+Redistributions in binary form must reproduce the above copyright notice, this
+list of conditions and the following disclaimer in the documentation and/or
+other materials provided with the distribution.
+
+3. Neither the name of the
+copyright holder nor the names of its contributors may be used to endorse or
+promote products derived from this software without specific prior written
+permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+“AS IS” AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
+TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

+ 32 - 0
connector-fetch-openmeteo/pom.xml

@@ -0,0 +1,32 @@
+<?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-openmeteo</artifactId>
+    <name>fetch-openmeteo</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>

+ 38 - 0
connector-fetch-openmeteo/src/main/java/cz/senslog/connector/fetch/openmeteo/ConnectorFetchOpenMeteoProvider.java

@@ -0,0 +1,38 @@
+// Copyright (c) 2026 UWB & LESP.
+// The UWB & LESP license this file to you under the BSD-3-Clause license.
+
+package cz.senslog.connector.fetch.openmeteo;
+
+import cz.senslog.connector.fetch.api.ConnectorFetchProvider;
+import cz.senslog.connector.fetch.api.ExecutableFetcher;
+import cz.senslog.connector.model.config.DefaultConfig;
+import cz.senslog.connector.model.openmeteo.OpenMeteoModel;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+import static cz.senslog.connector.tools.http.HttpClient.newHttpClient;
+
+public class ConnectorFetchOpenMeteoProvider implements ConnectorFetchProvider  {
+
+    private static final Logger logger = LogManager.getLogger(ConnectorFetchOpenMeteoProvider.class);
+
+
+    @Override
+    public ExecutableFetcher<OpenMeteoModel> createExecutableFetcher(DefaultConfig defaultConfig) {
+        logger.info("Initialization a new fetch provider {}.", ConnectorFetchOpenMeteoProvider.class);
+
+        logger.debug("Creating a new configuration.");
+        OpenMeteoConfig config = new OpenMeteoConfig(defaultConfig);
+        logger.info("Configuration for {} was created successfully.", OpenMeteoFetcher.class);
+
+
+        logger.debug("Creating a new instance of {}.", OpenMeteoFetcher.class);
+        OpenMeteoFetcher fetcher = new OpenMeteoFetcher(config, newHttpClient());
+        logger.info("Fetcher for {} was created successfully.", OpenMeteoFetcher.class);
+
+        ExecutableFetcher<OpenMeteoModel> executor = ExecutableFetcher.create(fetcher);
+        logger.info("Fetcher executor for {} was created successfully.", OpenMeteoFetcher.class);
+
+        return executor;
+    }
+}

+ 157 - 0
connector-fetch-openmeteo/src/main/java/cz/senslog/connector/fetch/openmeteo/OpenMeteoConfig.java

@@ -0,0 +1,157 @@
+// Copyright (c) 2026 UWB & LESP.
+// The UWB & LESP license this file to you under the BSD-3-Clause license.
+
+package cz.senslog.connector.fetch.openmeteo;
+
+import cz.senslog.connector.model.config.DefaultConfig;
+import cz.senslog.connector.model.config.PropertyConfig;
+import cz.senslog.connector.tools.util.ClassUtils;
+
+import java.time.LocalDate;
+import java.util.*;
+import java.util.function.Supplier;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class OpenMeteoConfig {
+
+    private static final Pattern pattern = Pattern.compile("\\$\\{(?<name>\\w+)}", Pattern.CASE_INSENSITIVE);
+
+    public static final class AllowedStation {
+        private final String url;
+        private final Map<String, Param> params;
+        private final int minId;
+        private final int maxId;
+
+        public AllowedStation(String url, Map<String, Param> params, int minId, int maxId) {
+            this.url = url;
+            this.params = params;
+            this.minId = minId;
+            this.maxId = maxId;
+        }
+
+        public String getUrl() {
+            return url;
+        }
+
+        public Map<String, Param> getParams() {
+            return params;
+        }
+
+        public int getMinId() {
+            return minId;
+        }
+
+        public int getMaxId() {
+            return maxId;
+        }
+    }
+
+    public static final class Param {
+        private final boolean isDynamic;
+        private final String value;
+
+        public Param(Object value) {
+            if (value instanceof String) {
+                Matcher matcher = pattern.matcher((String) value);
+                if (matcher.matches()) {
+                    this.isDynamic = true;
+                    this.value = matcher.group("name");
+                } else {
+                    this.isDynamic = false;
+                    this.value = (String) value;
+                }
+            } else if (value instanceof List) {
+                Collection<?> list = (Collection<?>) value;
+                List<String> res = new ArrayList<>(list.size());
+                for (Object o : list) {
+                    res.add(ClassUtils.cast(o, String.class));
+                }
+                this.isDynamic = false;
+                this.value = String.join(",", res);
+            } else if (value instanceof Integer) {
+                this.isDynamic = false;
+                this.value = ((Integer) value).toString();
+            } else {
+                this.isDynamic = false;
+                this.value = null;
+            }
+        }
+
+        public boolean isDynamic() {
+            return isDynamic;
+        }
+
+        public String getValue() {
+            return value;
+        }
+    }
+
+    private final String url;
+    private final TimeZone timeZone;
+    private final Supplier<LocalDate> startDateSupplier;
+    private LocalDate startDate;
+    private final LocalDate endDate;
+    private final int period;
+    private final AllowedStation allowedStation;
+
+    OpenMeteoConfig(DefaultConfig defaultConfig) {
+        this.url = defaultConfig.getStringProperty("baseUrl");
+        String startDateString = defaultConfig.getStringProperty("startDate");
+        if (startDateString.equalsIgnoreCase("now")) {
+            this.startDateSupplier = LocalDate::now;
+            this.startDate = null;
+        } else {
+            this.startDateSupplier = null;
+            this.startDate = LocalDate.parse(startDateString);
+        }
+        if (defaultConfig.containsProperty("endDate")) {
+            this.endDate = LocalDate.parse(defaultConfig.getStringProperty("endDate"));
+        } else {
+            this.endDate = null;
+        }
+        this.period = defaultConfig.getIntegerProperty("period");
+        this.timeZone = TimeZone.getTimeZone(defaultConfig.getStringProperty("timeZone"));
+
+        PropertyConfig allowedStation = defaultConfig.getPropertyConfig("allowedStation");
+        String url = allowedStation.getStringProperty("url");
+        PropertyConfig paramsConfig = allowedStation.getPropertyConfig("params");
+        Set<String> attributes = paramsConfig.getAttributes();
+        Map<String, Param> params = new HashMap<>(attributes.size());
+        for (String attr : attributes) {
+            params.put(attr, new Param(paramsConfig.getProperty(attr)));
+        }
+
+        int minId = allowedStation.containsProperty("minId") ? allowedStation.getIntegerProperty("minId") :  Integer.MIN_VALUE;
+        int maxId = allowedStation.containsProperty("maxId") ? allowedStation.getIntegerProperty("maxId") :  Integer.MAX_VALUE;
+
+        this.allowedStation = new AllowedStation(url, params, minId, maxId);
+    }
+
+    public int getPeriod() {
+        return period;
+    }
+
+    public String getUrl() {
+        return url;
+    }
+
+    public TimeZone getTimeZone() {
+        return timeZone;
+    }
+
+    public LocalDate getStartDate() {
+        if (startDate == null) {
+            startDate = startDateSupplier.get();
+        }
+        return  startDate;
+    }
+
+    public LocalDate getEndDate() {
+        return endDate;
+    }
+
+    public AllowedStation getAllowedStation() {
+        return allowedStation;
+    }
+}

+ 199 - 0
connector-fetch-openmeteo/src/main/java/cz/senslog/connector/fetch/openmeteo/OpenMeteoFetcher.java

@@ -0,0 +1,199 @@
+// Copyright (c) 2026 UWB & LESP.
+// The UWB & LESP license this file to you under the BSD-3-Clause license.
+
+package cz.senslog.connector.fetch.openmeteo;
+
+import com.google.gson.JsonArray;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import cz.senslog.connector.fetch.api.ConnectorFetcher;
+import cz.senslog.connector.model.api.VoidSession;
+import cz.senslog.connector.model.openmeteo.OpenMeteoModel;
+import cz.senslog.connector.tools.exception.ModuleInterruptedException;
+import cz.senslog.connector.tools.exception.SyntaxException;
+import cz.senslog.connector.tools.http.HttpClient;
+import cz.senslog.connector.tools.http.HttpRequest;
+import cz.senslog.connector.tools.http.HttpResponse;
+import cz.senslog.connector.tools.http.URLBuilder;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+import java.time.*;
+import java.util.*;
+import java.util.function.Supplier;
+
+import static cz.senslog.connector.tools.json.BasicJson.jsonToObject;
+import static java.time.format.DateTimeFormatter.ofPattern;
+
+public class OpenMeteoFetcher implements ConnectorFetcher<VoidSession, OpenMeteoModel> {
+
+    private static final Logger logger = LogManager.getLogger(OpenMeteoFetcher.class);
+
+    private final HttpClient httpClient;
+    private final OpenMeteoConfig config;
+
+    private final Set<Station> stations;
+    private LocalDateTime currentDateTime;
+
+
+    public OpenMeteoFetcher(OpenMeteoConfig config, HttpClient httpClient) {
+        this.httpClient = httpClient;
+        this.config = config;
+        this.stations = new HashSet<>();
+    }
+
+    @Override
+    public void init() {}
+
+
+    @Override
+    public OpenMeteoModel fetch(Optional<VoidSession> session) {
+
+        currentDateTime = currentDateTime != null ? currentDateTime : LocalDateTime.of(config.getStartDate(), LocalTime.now());
+        stations.clear();
+
+
+        if (config.getEndDate() != null && currentDateTime.toLocalDate().isAfter(config.getEndDate())) {
+            throw new ModuleInterruptedException(String.format("The connector reached the end: %s", config.getEndDate()));
+        }
+
+        LocalDateTime now = LocalDateTime.now();
+        if (currentDateTime.isAfter(now)) {
+            logger.warn("The current execution can't be in the future: {} > {}", currentDateTime, now);
+            LocalDateTime futureExecution = currentDateTime.plusHours(config.getPeriod());
+            logger.info("Waiting for current date. Execution in {} minutes", Duration.between(LocalDateTime.now(), futureExecution).toMinutes());
+            return new OpenMeteoModel(null, null, Collections.emptyList());
+        }
+
+        {
+            OpenMeteoConfig.AllowedStation allowedStation = config.getAllowedStation();
+            HttpRequest.Builder req = HttpRequest.newBuilder().GET()
+                    .url(URLBuilder.newBuilder(allowedStation.getUrl()).build());
+
+            HttpResponse res = httpClient.send(req.build());
+
+            if (res.isError()) {
+                logger.error("Request error <{}> with reason: {}", res.getStatus(), res.getBody());
+                logger.error("Error while getting the stations from <{}>.", allowedStation.getUrl());
+                return new OpenMeteoModel(null, null, Collections.emptyList());
+            }
+
+            JsonArray listJSON = jsonToObject(res.getBody(), JsonArray.class);
+            for (JsonElement element : listJSON) {
+                int id = element.getAsJsonObject().get("id").getAsInt();
+                JsonObject stationJSON = element.getAsJsonObject();
+                JsonObject geometryJSON = stationJSON.get("geometry").getAsJsonObject();
+                String geometryType = geometryJSON.get("type").getAsString();
+                if (id >= allowedStation.getMinId() && id <= allowedStation.getMaxId()) {
+                    if (geometryType.equals("Point")) {
+                        JsonArray coordinatesJSON = geometryJSON.get("coordinates").getAsJsonArray();
+                        stations.add(new Station(coordinatesJSON.get(0).getAsDouble(), coordinatesJSON.get(1).getAsDouble()));
+                    }
+                }
+            }
+        }
+
+        OpenMeteoConfig.AllowedStation allowedStation = config.getAllowedStation();
+        List<JsonObject> results = new ArrayList<>(stations.size());
+        for (Station station : stations) {
+
+            Map<String, Supplier<String>> dynamicParamMapping = new HashMap<>();
+            dynamicParamMapping.put("latitude", () -> Double.toString(station.getLatitude()));
+            dynamicParamMapping.put("longitude", () -> Double.toString(station.getLongitude()));
+            dynamicParamMapping.put("currentDate", () -> currentDateTime.format(ofPattern("yyyy-MM-dd")));
+
+            URLBuilder urlBuilder = URLBuilder.newBuilder(config.getUrl());
+
+            for (Map.Entry<String, OpenMeteoConfig.Param> paramEntry : allowedStation.getParams().entrySet()) {
+                String paramName = paramEntry.getKey();
+                OpenMeteoConfig.Param param = paramEntry.getValue();
+
+                if (param.isDynamic()) {
+                    Supplier<String> valueSupplier = dynamicParamMapping.get(param.getValue());
+                    if (valueSupplier != null) {
+                        urlBuilder.addParam(paramName, valueSupplier.get());
+                    } else {
+                        logger.warn("Dynamic param {} not found.", paramName);
+                        return new OpenMeteoModel(null, null, Collections.emptyList());
+                    }
+                } else {
+                    urlBuilder.addParam(paramName, param.getValue());
+                }
+            }
+
+            HttpRequest.Builder req = HttpRequest.newBuilder().GET().url(urlBuilder.build());
+            HttpResponse res = httpClient.send(req.build());
+
+            if (res.isError()) {
+                logger.warn("Open Meteo station lat: {}, lon: {} failed to fetch. Error: {}", station.getLatitude(), station.getLongitude(), res.getBody());
+                continue;
+            }
+
+            String jsonBody = res.getBody();
+            try {
+                JsonObject predictionJSON = jsonToObject(jsonBody, JsonObject.class);
+                JsonObject filteredJSON = removeNullPredictions(predictionJSON);
+                if (filteredJSON != null) {
+                    results.add(filteredJSON);
+                }
+            } catch (SyntaxException e) {
+                logger.error("Error while parsing JSON: {}", e.getLocalizedMessage());
+                logger.error(jsonBody);
+            }
+        }
+
+        OffsetDateTime currentDateTime = ZonedDateTime.of(this.currentDateTime, config.getTimeZone().toZoneId()).toOffsetDateTime();
+
+        if (results.isEmpty()) {
+            logger.warn("No results found for the date: {}", currentDateTime);
+        }
+
+        // do not move by period step if period < 0
+        this.currentDateTime = config.getPeriod() < 0 ? LocalDateTime.now() : this.currentDateTime.plusHours(config.getPeriod());
+
+        return new OpenMeteoModel(currentDateTime, currentDateTime, results);
+    }
+
+    private static JsonObject removeNullPredictions(JsonObject json) {
+        if (json == null) {
+            return null;
+        }
+
+        final String propertyTag = "hourly_units";
+        final String valueTag = "hourly";
+
+        JsonObject resultJSON = json.deepCopy();
+        if (json.has(propertyTag) && json.has(valueTag)) {
+            JsonObject hourlyUnitsJSON = json.get(propertyTag).getAsJsonObject();
+            JsonObject hourlyJSON = json.get(valueTag).getAsJsonObject();
+            for (String paramName : hourlyUnitsJSON.keySet()) {
+                if (hourlyJSON.has(paramName)) {
+                    JsonElement element = hourlyJSON.get(paramName);
+                    if (element.isJsonArray() && isAllNull(element.getAsJsonArray())) {
+                        resultJSON.getAsJsonObject(propertyTag).remove(paramName);
+                        resultJSON.getAsJsonObject(valueTag).remove(paramName);
+                    }
+                }
+            }
+        }
+
+        if (resultJSON.getAsJsonObject(propertyTag).isEmpty()) {
+            return null;
+        }
+
+        return resultJSON;
+    }
+
+    private static boolean isAllNull(JsonArray array) {
+        if (array == null || array.isEmpty()) {
+            return true;
+        }
+        boolean allNull = false;
+        for (JsonElement el : array) {
+            if (el.isJsonNull()) {
+                allNull = true;
+            }
+        }
+        return allNull;
+    }
+}

+ 22 - 0
connector-fetch-openmeteo/src/main/java/cz/senslog/connector/fetch/openmeteo/Station.java

@@ -0,0 +1,22 @@
+// Copyright (c) 2026 UWB & LESP.
+// The UWB & LESP license this file to you under the BSD-3-Clause license.
+
+package cz.senslog.connector.fetch.openmeteo;
+
+public class Station {
+
+    private final double latitude, longitude;
+
+    public Station(double longitudeX, double latitudeY) {
+        this.latitude = latitudeY;
+        this.longitude = longitudeX;
+    }
+
+    public double getLatitude() {
+        return latitude;
+    }
+
+    public double getLongitude() {
+        return longitude;
+    }
+}

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

@@ -0,0 +1 @@
+cz.senslog.connector.fetch.openmeteo.ConnectorFetchOpenMeteoProvider

+ 33 - 0
connector-model/license.txt

@@ -0,0 +1,33 @@
+BSD-3-Clause
+
+Copyright (c) 2020 University of West Bohemia (UWB) https://www.zcu.cz/en, LESPROJEKT-SLUŽBY s.r.o. (LESP) https://www.lesprojekt.cz/en
+
+Copyright 2026 <COPYRIGHT HOLDER>
+
+Redistribution and use in source and binary
+forms, with or without modification, are permitted provided that the following
+conditions are met:
+
+1. Redistributions of source code must retain the above
+copyright notice, this list of conditions and the following disclaimer.
+
+2.
+Redistributions in binary form must reproduce the above copyright notice, this
+list of conditions and the following disclaimer in the documentation and/or
+other materials provided with the distribution.
+
+3. Neither the name of the
+copyright holder nor the names of its contributors may be used to endorse or
+promote products derived from this software without specific prior written
+permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+“AS IS” AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
+TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

+ 22 - 0
connector-model/pom.xml

@@ -0,0 +1,22 @@
+<?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">
+    <parent>
+        <artifactId>connector-period</artifactId>
+        <groupId>cz.senslog</groupId>
+        <version>1.0-SNAPSHOT</version>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+
+    <artifactId>connector-model</artifactId>
+    <name>model</name>
+
+    <dependencies>
+        <dependency>
+            <groupId>cz.senslog</groupId>
+            <artifactId>connector-tools</artifactId>
+            <version>${project.parent.version}</version>
+        </dependency>
+    </dependencies>
+</project>

+ 33 - 0
connector-model/src/main/java/cz/senslog/connector/model/api/AbstractModel.java

@@ -0,0 +1,33 @@
+// Copyright (c) 2026 UWB & LESP.
+// The UWB & LESP license this file to you under the BSD-3-Clause license.
+
+package cz.senslog.connector.model.api;
+
+import java.time.OffsetDateTime;
+
+/**
+ * The abstract class {@code AbstractModel} represents a base class
+ * for all models which want to be used as a transfer model for a connector.
+ *
+ * @author Lukas Cerny
+ * @version 1.0
+ * @since 1.0
+ */
+public abstract class AbstractModel {
+
+    /** Time range from until were gotten the data. */
+    private final OffsetDateTime from, to;
+
+    protected AbstractModel(OffsetDateTime from, OffsetDateTime to) {
+        this.from = from;
+        this.to = to;
+    }
+
+    public OffsetDateTime getFrom() {
+        return from;
+    }
+
+    public OffsetDateTime getTo() {
+        return to;
+    }
+}

+ 27 - 0
connector-model/src/main/java/cz/senslog/connector/model/api/Converter.java

@@ -0,0 +1,27 @@
+// Copyright (c) 2026 UWB & LESP.
+// The UWB & LESP license this file to you under the BSD-3-Clause license.
+
+package cz.senslog.connector.model.api;
+
+/**
+ * The interface {@code Converter} provides a generic functionality
+ * for converter. Each class which implements which interface can be registered
+ * as a converter for a connector. For converter can be used only classes which extend {@link AbstractModel}.
+ *
+ *
+ * @param <IN> which type of class will be used as an input of a converter.
+ * @param <OUT> which type of class will be used as an output of a converter.
+ *
+ * @author Lukas Cerny
+ * @version 1.0
+ * @since 1.0
+ */
+public interface Converter<IN extends AbstractModel, OUT extends AbstractModel> {
+
+    /**
+     * Provides an interface for converting from input to output model.
+     * @param model - model which is converted to output.
+     * @return converted input model.
+     */
+    OUT convert(IN model);
+}

+ 66 - 0
connector-model/src/main/java/cz/senslog/connector/model/api/ConverterProvider.java

@@ -0,0 +1,66 @@
+// Copyright (c) 2026 UWB & LESP.
+// The UWB & LESP license this file to you under the BSD-3-Clause license.
+
+package cz.senslog.connector.model.api;
+
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+import java.lang.reflect.ParameterizedType;
+import java.util.ArrayList;
+import java.util.List;
+
+public abstract class ConverterProvider {
+
+    private static final Logger logger = LogManager.getLogger(ConverterProvider.class);
+
+    private static class Wrapper {
+        private final Class<?> input;
+        private final Class<?> output;
+        private final Converter<?,?> converter;
+        private Wrapper(Class<?> input, Class<?> output, Converter<?,?> converter) {
+            this.input = input;
+            this.output = output;
+            this.converter = converter;
+        }
+    }
+
+    private final List<Wrapper> CONVERTERS = new ArrayList<>();
+
+    protected ConverterProvider() {
+        config();
+    }
+
+    protected abstract void config();
+
+    protected void register(Class<? extends Converter<?,?>> converterClass) {
+        logger.debug("Registering a new converter {}", converterClass);
+        try {
+            logger.debug("Getting a generic parameters from the class {}.", converterClass);
+            ParameterizedType converterTypes = (ParameterizedType) converterClass.getGenericInterfaces()[0];
+            Class<?> fetchModel = (Class<?>) converterTypes.getActualTypeArguments()[0];
+            Class<?> pushModel = (Class<?>) converterTypes.getActualTypeArguments()[1];
+
+            logger.debug("Creating a new instance of the class {}.", converterClass);
+            Converter<?,?> converter = converterClass.newInstance();
+
+            CONVERTERS.add(new Wrapper(fetchModel, pushModel, converter));
+            logger.info("Registered a new converter {} for {} -> {}.", converterClass.getSimpleName(), fetchModel.getSimpleName(), pushModel.getSimpleName());
+        } catch (InstantiationException | IllegalAccessException e) {
+            logger.error("Can not create an instance for {}.", converterClass);
+            logger.catching(e);
+        }
+
+    }
+
+    public Converter getConverter(Class<? extends AbstractModel> fetchModel, Class<? extends AbstractModel> pushModel) {
+        logger.info("Getting a converter for {} -> {}. ", fetchModel.getSimpleName(), pushModel.getSimpleName());
+        for (Wrapper item : CONVERTERS) {
+            if (item.input.equals(fetchModel) && item.output.equals(pushModel)) {
+                return item.converter;
+            }
+        }
+        logger.warn("The converter for {} -> {} was not found.", fetchModel, pushModel);
+        return null;
+    }
+}

+ 22 - 0
connector-model/src/main/java/cz/senslog/connector/model/api/ProxySessionModel.java

@@ -0,0 +1,22 @@
+// Copyright (c) 2026 UWB & LESP.
+// The UWB & LESP license this file to you under the BSD-3-Clause license.
+
+package cz.senslog.connector.model.api;
+
+public abstract class ProxySessionModel {
+
+    private final boolean isActive;
+
+    public ProxySessionModel(boolean isActive) {
+        this.isActive = isActive;
+    }
+
+    public boolean isActive() {
+        return isActive;
+    }
+
+    @Override
+    public String toString() {
+        return String.format("%s.%s", getClass().getSimpleName(), (isActive() ? "active" : "disable"));
+    }
+}

+ 14 - 0
connector-model/src/main/java/cz/senslog/connector/model/api/VoidSession.java

@@ -0,0 +1,14 @@
+// Copyright (c) 2026 UWB & LESP.
+// The UWB & LESP license this file to you under the BSD-3-Clause license.
+
+package cz.senslog.connector.model.api;
+
+/**
+ * The class signalizes that the session is not active and no data is expecting.
+ */
+public class VoidSession extends ProxySessionModel {
+
+    public VoidSession() {
+        super(false);
+    }
+}

+ 82 - 0
connector-model/src/main/java/cz/senslog/connector/model/config/ConnectorDescriptor.java

@@ -0,0 +1,82 @@
+// Copyright (c) 2026 UWB & LESP.
+// The UWB & LESP license this file to you under the BSD-3-Clause license.
+
+package cz.senslog.connector.model.config;
+
+import java.time.LocalTime;
+
+import static cz.senslog.connector.tools.json.BasicJson.objectToJson;
+
+/**
+ * The class {@code ConnectorDescriptor} represents a configuration class for a connector.
+ * According to this descriptor is created a new connector.
+ *
+ * @author Lukas Cerny
+ * @version 1.0
+ * @since 1.0
+ */
+public class ConnectorDescriptor {
+
+    /** Name of a connector. */
+    private final String name;
+
+    /** Class of a fetcher. */
+    private final String fetcherId;
+
+    /** class of a pusher. */
+    private final String pusherId;
+
+    /** Period for scheduling. */
+    private final Integer period;
+
+    /** Initialization delay for scheduling. */
+    private final Integer delay;
+
+    private final LocalTime startAt;
+
+    /**
+     * Constructor sets all attributes.
+     * @param name - name of a connector.
+     * @param fetcherId - class of a fetcher.
+     * @param pusherId - class of a pusher.
+     * @param period - period for scheduling.
+     * @param delay - initialization delay for scheduling.
+     */
+    public ConnectorDescriptor(String name, String fetcherId, String pusherId, Integer period, Integer delay, LocalTime startAt) {
+        this.name = name;
+        this.fetcherId = fetcherId;
+        this.pusherId = pusherId;
+        this.period = period;
+        this.delay = delay;
+        this.startAt = startAt;
+    }
+
+    public String getName() {
+        return name;
+    }
+
+    public String getFetcherId() {
+        return fetcherId;
+    }
+
+    public String getPusherId() {
+        return pusherId;
+    }
+
+    public Integer getPeriod() {
+        return period;
+    }
+
+    public Integer getDelay() {
+        return delay;
+    }
+
+    public LocalTime getStartAt() {
+        return startAt;
+    }
+
+    @Override
+    public String toString() {
+        return objectToJson(this);
+    }
+}

+ 45 - 0
connector-model/src/main/java/cz/senslog/connector/model/config/DefaultConfig.java

@@ -0,0 +1,45 @@
+// Copyright (c) 2026 UWB & LESP.
+// The UWB & LESP license this file to you under the BSD-3-Clause license.
+
+package cz.senslog.connector.model.config;
+
+/**
+ * The class {@code DefaultConfig} represents a major configuration class for all providers.
+ * Represents a root node of configuration for a class defines as a {@see #provider}.
+ *
+ * @author Lukas Cerny
+ * @version 1.0
+ * @since 1.0
+ */
+public class DefaultConfig extends PropertyConfig {
+
+    /** Provider for which is gotten a configuration. */
+    private final Class<?> provider;
+
+    /**
+     * Constructors sets root name (id) and provider class.
+     * @param id - identifier of root node.
+     * @param provider - class provider.
+     */
+    public DefaultConfig(String id, Class<?> provider) {
+        super(id);
+        this.provider = provider;
+    }
+
+    public Class<?> getProvider() {
+        return provider;
+    }
+
+    @Override
+    public int hashCode() {
+        return provider.hashCode();
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+        DefaultConfig that = (DefaultConfig) o;
+        return hashCode() == that.hashCode();
+    }
+}

+ 55 - 0
connector-model/src/main/java/cz/senslog/connector/model/config/HostConfig.java

@@ -0,0 +1,55 @@
+// Copyright (c) 2026 UWB & LESP.
+// The UWB & LESP license this file to you under the BSD-3-Clause license.
+
+package cz.senslog.connector.model.config;
+
+import static cz.senslog.connector.tools.json.BasicJson.objectToJson;
+
+/**
+ * The class {@code HostConfig} represents a configuration class.
+ * Contains basic information which are needed to create an url.
+ *
+ * @author Lukas Cerny
+ * @version 1.0
+ * @since 1.0
+ */
+public class HostConfig {
+
+    /** Domain of the host. */
+    private final String domain;
+
+    /** Path of the host. */
+    private final String path;
+
+    /**
+     * Constructor sets all attributes.
+     * @param domain - domain of the host.
+     * @param path - path of the host.
+     */
+    public HostConfig(String domain, String path) {
+        this.domain = domain;
+        this.path = path;
+    }
+
+    /**
+     * Constructor sets all attributes from generic property class {@link PropertyConfig}.
+     * @param config - generic configuration.
+     */
+    public HostConfig(PropertyConfig config) {
+        this.domain = config.getStringProperty("domain");
+        this.path = config.getStringProperty("path");
+    }
+
+    public String getDomain() {
+        return domain;
+    }
+
+    public String getPath() {
+        return path;
+    }
+
+    @Override
+    public String toString() {
+        return objectToJson(this);
+    }
+}

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

@@ -0,0 +1,291 @@
+// Copyright (c) 2026 UWB & LESP.
+// The UWB & LESP license this file to you under the BSD-3-Clause license.
+
+package cz.senslog.connector.model.config;
+
+import cz.senslog.connector.tools.exception.PropertyNotFoundException;
+import cz.senslog.connector.tools.util.ClassUtils;
+
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.LocalTime;
+import java.time.ZoneOffset;
+import java.time.format.DateTimeFormatter;
+import java.util.*;
+
+import static java.lang.String.format;
+import static java.util.Collections.emptyList;
+import static java.util.Collections.emptySet;
+import static java.util.Optional.ofNullable;
+
+/**
+ * The class {@code PropertyConfig} represents a general configuration class.
+ * Contains map of properties which represents a tree of configuration.
+ * Each node is a {@code PropertyConfig} which contains {@see #id}
+ * and could be generally located. Each leave can be represented
+ * as {@see Integer}, {@see String} or {@see LocalDateTime}.
+ *
+ * @author Lukas Cerny
+ * @version 1.0
+ * @since 1.0
+ */
+public class PropertyConfig {
+
+    /** Path delimiter separates nodes. */
+    private static final String PATH_DELIMITER = ".";
+
+    /** Identifier of path. */
+    private final String id;
+
+    /** Map of properties. */
+    private final Map<String, Object> properties;
+
+    /**
+     * Constructor sets new identifier of node.
+     * @param id - identifier of node.
+     */
+    protected PropertyConfig(String id) {
+        this.id = id;
+        this.properties = new HashMap<>();
+    }
+
+    /**
+     * Adds new property to properties.
+     * @param name - name of new property.
+     * @param value - value of new property.
+     */
+    public boolean setProperty(String name, Object value) {
+        Object res = properties.put(name, value);
+        return res == value;
+    }
+
+    /**
+     * Returns value. It could be anything.
+     * @param name - name of property.
+     * @return object of value.
+     */
+    public Object getProperty(String name) {
+        if (properties.containsKey(name)) {
+            return properties.get(name);
+        }
+
+        throw new PropertyNotFoundException(format(
+                "Property '%s' does not exist.", getNewPropertyId(name))
+        );
+    }
+
+    /**
+     * Checks if property key is presents in properties.
+     * @param name - name of property
+     * @return boolean
+     */
+    public boolean containsProperty(String name) {
+        return properties.containsKey(name);
+    }
+
+    /**
+     * Returns optional value. It could be anything.
+     * @param name - name of property.
+     * @return optional object
+     */
+    public Optional<Object> getOptionalProperty(String name) {
+        return ofNullable(properties.get(name));
+    }
+
+    /**
+     * Returns property as a String.
+     * @param name - name of property.
+     * @return string value.
+     */
+    public String getStringProperty(String name) {
+        Object object = getProperty(name);
+        if (object instanceof String) {
+            return (String) object;
+        } else {
+            return object.toString();
+        }
+    }
+
+    /**
+     * Returns property as an Integer.
+     * @param name - name of property.
+     * @return integer value.
+     */
+    public Integer getIntegerProperty(String name) {
+        return ClassUtils.cast(getProperty(name), Integer.class);
+    }
+
+    /**
+     * Returns property as a Double object.
+     * @param name - name of property.
+     * @return double value.
+     */
+    public Double getDoubleProperty(String name) {
+        return ClassUtils.cast(getProperty(name), Double.class);
+    }
+
+    /**
+     * Returns property as a LocalDateTime.
+     * @param name - name of property.
+     * @return localDateTime value.
+     */
+    public LocalDateTime getLocalDateTimeProperty(String name) {
+        Object object = getProperty(name);
+
+        if (object instanceof LocalDateTime) {
+            return (LocalDateTime) object;
+        } else if (object instanceof Date) {
+            Date date = (Date) object;
+            return date.toInstant().atZone(ZoneOffset.systemDefault()).toLocalDateTime();
+        } else if (object instanceof String) {
+            return LocalDateTime.parse((String)object, DateTimeFormatter.ISO_DATE_TIME);
+
+        } else {
+            throw new ClassCastException(format(
+                    "Property '%s' can not be cast to %s", getNewPropertyId(name), LocalDateTime.class)
+            );
+        }
+    }
+
+    /**
+     * Returns property as a LocalTime.
+     * @param name - name of property.
+     * @return localTime value.
+     */
+    public LocalTime getLocalTimeProperty(String name) {
+        Object object = getProperty(name);
+
+        if (object instanceof LocalTime) {
+            return (LocalTime) object;
+        } else if (object instanceof Date) {
+            Date date = (Date) object;
+            return date.toInstant().atZone(ZoneOffset.systemDefault()).toLocalTime();
+        } else if (object instanceof String) {
+            return LocalTime.parse((String)object, DateTimeFormatter.ISO_TIME);
+
+        } else {
+            throw new ClassCastException(format(
+                    "Property '%s' can not be cast to %s", getNewPropertyId(name), LocalTime.class)
+            );
+        }
+    }
+
+    /**
+     * Returns property as a LocalTime.
+     * @param name - name of property.
+     * @return localTime value.
+     */
+    public LocalDate getLocalDateProperty(String name) {
+        Object object = getProperty(name);
+
+        if (object instanceof LocalDate) {
+            return (LocalDate) object;
+        } else if (object instanceof Date) {
+            Date date = (Date) object;
+            return date.toInstant().atZone(ZoneOffset.systemDefault()).toLocalDate();
+        } else if (object instanceof String) {
+            return LocalDate.parse((String)object, DateTimeFormatter.ISO_DATE);
+
+        } else {
+            throw new ClassCastException(format(
+                    "Property '%s' can not be cast to %s", getNewPropertyId(name), LocalTime.class)
+            );
+        }
+    }
+
+    /**
+     * Returns property as a optional of LocalDateTime
+     * @param name - name of property.
+     * @return optional of localDateTime value.
+     */
+    public Optional<LocalDateTime> getOptionalLocalDateTimeProperty(String name) {
+        return properties.containsKey(name) ? Optional.of(getLocalDateTimeProperty(name)) : Optional.empty();
+    }
+
+    /**
+     * Returns property as a set of the 'type'.
+     * @param name - name of property.
+     * @param type - type of attributes
+     * @param <T> - generic type of attribute
+     * @return Set of attributes defined by type
+     */
+    public <T> Set<T> getSetProperty(String name, Class<T> type) {
+        Object value = properties.get(name);
+        if (value instanceof  Collection) {
+            Collection<?> list = (Collection<?>) value;
+            Set<T> res = new HashSet<>(list.size());
+            for (Object o : list) {
+                res.add(ClassUtils.cast(o, type));
+            }
+            return res;
+        }
+
+        return emptySet();
+    }
+
+    public <T> List<T> getListProperty(String name, Class<T> type) {
+        Object object = getProperty(name);
+        if (object instanceof Collection) {
+            Collection<?> list = (Collection<?>) object;
+            List<T> res = new ArrayList<>(list.size());
+            for (Object o : list) {
+                res.add(ClassUtils.cast(o, type));
+            }
+            return res;
+        }
+        return emptyList();
+    }
+
+    private static PropertyConfig mapMapObjectoToPropertyConfig(String id, Object mapObject) {
+        PropertyConfig config = new PropertyConfig(id);
+        if (mapObject instanceof Map) {
+            Map<?, ?> properties = (Map<?, ?>) mapObject;
+            for (Map.Entry<?, ?> propertyEntry : properties.entrySet()) {
+                config.setProperty(propertyEntry.getKey().toString(), propertyEntry.getValue());
+            }
+        }
+        return config;
+    }
+
+    public Set<PropertyConfig> getSetProperty(String name) {
+        Object value = properties.get(name);
+        if (value instanceof  Collection) {
+            Collection<?> collection = (Collection<?>) value;
+            Iterator<?> iter = collection.iterator();
+            Set<PropertyConfig> res = new HashSet<>(collection.size());
+            int index = 0;
+            while (iter.hasNext()) {
+                res.add(mapMapObjectoToPropertyConfig(getNewPropertyId(name, index++), iter.next()));
+            }
+            return res;
+        }
+
+        return emptySet();
+    }
+
+    /**
+     * Returns new node of configuration.
+     * @param name - name of property.
+     * @return node of configuration.
+     */
+    public PropertyConfig getPropertyConfig(String name) {
+        Object property = getProperty(name);
+        return mapMapObjectoToPropertyConfig(getNewPropertyId(name), property);
+    }
+
+    public Set<String> getAttributes() {
+        return properties.keySet();
+    }
+
+    private String getNewPropertyId(String name) {
+        return id + PATH_DELIMITER + name;
+    }
+
+    private String getNewPropertyId(String name, int index) {
+        return getNewPropertyId(name) + PATH_DELIMITER + index;
+    }
+
+    public String getId() {
+        return id;
+    }
+}

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

@@ -0,0 +1,24 @@
+// Copyright (c) 2026 UWB & LESP.
+// The UWB & LESP license this file to you under the BSD-3-Clause license.
+
+package cz.senslog.connector.model.converter;
+
+import cz.senslog.connector.model.api.ConverterProvider;
+
+/**
+ * The class {@see ModelConverterProvider} represents a configuration class
+ * for all converters which can be used for models between fetchers and pushers.
+ * The method {@see ModelConverterProvider#config} provides the registration
+ * for a new converter.
+ *
+ * @author Lukas Cerny
+ * @version 1.0
+ * @since 1.0
+ */
+public class ModelConverterProvider extends ConverterProvider {
+
+    @Override
+    protected void config() {
+        register(OpenMeteoToSensLogConverter.class);
+    }
+}

+ 34 - 0
connector-model/src/main/java/cz/senslog/connector/model/converter/OpenMeteoToSensLogConverter.java

@@ -0,0 +1,34 @@
+// Copyright (c) 2026 UWB & LESP.
+// The UWB & LESP license this file to you under the BSD-3-Clause license.
+
+package cz.senslog.connector.model.converter;
+
+import com.google.gson.JsonObject;
+import cz.senslog.connector.model.api.Converter;
+import cz.senslog.connector.model.openmeteo.OpenMeteoModel;
+import cz.senslog.connector.model.senslog.SensLogModel;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+public class OpenMeteoToSensLogConverter implements Converter<OpenMeteoModel, SensLogModel> {
+
+    @Override
+    public SensLogModel convert(OpenMeteoModel model) {
+        if (model == null) {
+            return null;
+        }
+
+        if (model.getData() == null || model.getData().isEmpty()) {
+            return new SensLogModel(model.getFrom(), model.getTo(), Collections.emptyList());
+        }
+
+        List<SensLogModel.PassingData> data = new ArrayList<>(model.getData().size());
+        for (JsonObject prediction : model.getData()) {
+            data.add(new SensLogModel.PassingData(prediction.toString()));
+        }
+
+        return new SensLogModel(model.getFrom(), model.getTo(), data);
+    }
+}

+ 24 - 0
connector-model/src/main/java/cz/senslog/connector/model/openmeteo/OpenMeteoModel.java

@@ -0,0 +1,24 @@
+// Copyright (c) 2026 UWB & LESP.
+// The UWB & LESP license this file to you under the BSD-3-Clause license.
+
+package cz.senslog.connector.model.openmeteo;
+
+import com.google.gson.JsonObject;
+import cz.senslog.connector.model.api.AbstractModel;
+
+import java.time.OffsetDateTime;
+import java.util.List;
+
+public class OpenMeteoModel extends AbstractModel {
+
+    private final List<JsonObject> data;
+
+    public OpenMeteoModel(OffsetDateTime from, OffsetDateTime to, List<JsonObject> data) {
+        super(from, to);
+        this.data = data;
+    }
+
+    public List<JsonObject> getData() {
+        return data;
+    }
+}

+ 50 - 0
connector-model/src/main/java/cz/senslog/connector/model/senslog/SensLogModel.java

@@ -0,0 +1,50 @@
+// Copyright (c) 2026 UWB & LESP.
+// The UWB & LESP license this file to you under the BSD-3-Clause license.
+
+package cz.senslog.connector.model.senslog;
+
+import cz.senslog.connector.model.api.AbstractModel;
+
+import java.time.OffsetDateTime;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+
+public class SensLogModel extends AbstractModel {
+
+    public static final class PassingData {
+        private final Map<String, String> params;
+        private final String payload;
+
+        public PassingData(Map<String, String> params, String payload) {
+            Objects.requireNonNull(params);
+            Objects.requireNonNull(payload);
+            this.params = params;
+            this.payload = payload;
+        }
+
+        public PassingData(String payload) {
+            this(Collections.emptyMap(), payload);
+        }
+
+        public Map<String, String> getParams() {
+            return params;
+        }
+
+        public String getPayload() {
+            return payload;
+        }
+    }
+
+    private final List<PassingData> passThroughData;
+
+    public SensLogModel(OffsetDateTime from, OffsetDateTime to, List<PassingData> passThroughData) {
+        super(from, to);
+        this.passThroughData = passThroughData;
+    }
+
+    public List<PassingData> getPassThroughData() {
+        return passThroughData;
+    }
+}

+ 71 - 0
connector-model/src/test/java/cz/senslog/connector/model/api/ConverterProviderTest.java

@@ -0,0 +1,71 @@
+// Copyright (c) 2026 UWB & LESP.
+// The UWB & LESP license this file to you under the BSD-3-Clause license.
+
+package cz.senslog.connector.model.api;
+
+import org.junit.jupiter.api.Test;
+
+import static java.time.OffsetDateTime.MAX;
+import static java.time.OffsetDateTime.MIN;
+import static org.junit.jupiter.api.Assertions.*;
+
+class ConverterProviderTest {
+
+    static class InputModel extends AbstractModel {
+        InputModel() {
+            super(MIN, MAX);
+        }
+    }
+    static class OutputModel extends AbstractModel {
+        OutputModel() {
+            super(MIN, MAX);
+        }
+    }
+
+    static class TestConverter implements Converter<InputModel, OutputModel> {
+        @Override public OutputModel convert(InputModel model) {return new OutputModel();}
+    }
+
+    @Test
+    void registration_AddedConverter_True() {
+
+        ConverterProvider provider = new ConverterProvider() {
+            @Override
+            protected void config() {
+                register(TestConverter.class);
+            }
+        };
+
+        Converter converter = provider.getConverter(InputModel.class, OutputModel.class);
+
+        assertNotNull(converter);
+        assertEquals(TestConverter.class, converter.getClass());
+    }
+
+    @Test
+    void registration_NotRegisteredConverter_Null() {
+
+        ConverterProvider provider = new ConverterProvider() {
+            @Override protected void config() {}
+        };
+
+        Converter converter = provider.getConverter(InputModel.class, OutputModel.class);
+
+        assertNull(converter);
+    }
+
+    @Test
+    void registration_NotFoundConverter_Null() {
+
+        ConverterProvider provider = new ConverterProvider() {
+            @Override
+            protected void config() {
+                register(TestConverter.class);
+            }
+        };
+
+        Converter converter = provider.getConverter(OutputModel.class, InputModel.class);
+
+        assertNull(converter);
+    }
+}

+ 25 - 0
connector-model/src/test/java/cz/senslog/connector/model/config/ConnectorDescriptorTest.java

@@ -0,0 +1,25 @@
+// Copyright (c) 2026 UWB & LESP.
+// The UWB & LESP license this file to you under the BSD-3-Clause license.
+
+package cz.senslog.connector.model.config;
+
+import org.junit.jupiter.api.Test;
+
+import static cz.senslog.connector.tools.json.BasicJson.jsonToObject;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+class ConnectorDescriptorTest {
+
+    @Test
+    void toString_ConvertJson_True() {
+
+        String jsonDescriptor = new ConnectorDescriptor("test", "FetcherId", "PusherId", 42, 2, null).toString();
+        ConnectorDescriptor descriptor = jsonToObject(jsonDescriptor, ConnectorDescriptor.class);
+
+        assertEquals("test", descriptor.getName());
+        assertEquals("FetcherId", descriptor.getFetcherId());
+        assertEquals("PusherId", descriptor.getPusherId());
+        assertEquals(42, descriptor.getPeriod());
+        assertEquals(2, descriptor.getDelay());
+    }
+}

+ 20 - 0
connector-model/src/test/java/cz/senslog/connector/model/config/DefaultConfigTest.java

@@ -0,0 +1,20 @@
+// Copyright (c) 2026 UWB & LESP.
+// The UWB & LESP license this file to you under the BSD-3-Clause license.
+
+package cz.senslog.connector.model.config;
+
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+class DefaultConfigTest {
+
+    @Test
+    void hashCode_ProviderEqual_True() {
+
+        DefaultConfig config1 = new DefaultConfig("1", DefaultConfig.class);
+        DefaultConfig config2 = new DefaultConfig("2", DefaultConfig.class);
+
+        assertEquals(config1, config2);
+    }
+}

+ 22 - 0
connector-model/src/test/java/cz/senslog/connector/model/config/HostConfigTest.java

@@ -0,0 +1,22 @@
+// Copyright (c) 2026 UWB & LESP.
+// The UWB & LESP license this file to you under the BSD-3-Clause license.
+
+package cz.senslog.connector.model.config;
+
+import cz.senslog.connector.tools.json.BasicJson;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+class HostConfigTest {
+
+    @Test
+    void toString_ConvertJson_True() {
+
+        String jsonConfig = new HostConfig("http://test.com", "test").toString();
+        HostConfig config = BasicJson.jsonToObject(jsonConfig, HostConfig.class);
+
+        assertEquals("http://test.com", config.getDomain());
+        assertEquals("test", config.getPath());
+    }
+}

+ 86 - 0
connector-model/src/test/java/cz/senslog/connector/model/config/PropertyConfigTest.java

@@ -0,0 +1,86 @@
+// Copyright (c) 2026 UWB & LESP.
+// The UWB & LESP license this file to you under the BSD-3-Clause license.
+
+package cz.senslog.connector.model.config;
+
+import cz.senslog.connector.tools.exception.PropertyNotFoundException;
+import org.junit.jupiter.api.Test;
+
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.Month;
+import java.time.ZoneOffset;
+import java.time.format.DateTimeFormatter;
+import java.util.Date;
+import java.util.HashMap;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+class PropertyConfigTest {
+
+    @Test
+    void property_basicDataTypes_True() {
+
+        PropertyConfig config = new PropertyConfig("test");
+        config.setProperty("string", "testString");
+        config.setProperty("integer", 42);
+
+        LocalDateTime localDateTime = LocalDateTime.of(1970, Month.JANUARY, 1, 0,0, 0);
+        config.setProperty("localDateTime", localDateTime);
+
+        String date = localDateTime.format(DateTimeFormatter.ISO_DATE_TIME);
+        config.setProperty("date", date);
+
+        assertEquals("test", config.getId());
+        assertEquals("testString", config.getStringProperty("string"));
+        assertEquals(42, config.getIntegerProperty("integer"));
+        assertEquals(localDateTime, config.getLocalDateTimeProperty("localDateTime"));
+        assertEquals(localDateTime, config.getLocalDateTimeProperty("date"));
+    }
+
+    @Test
+    void property_configProperty_True() {
+
+        PropertyConfig config = new PropertyConfig("test");
+        config.setProperty("values", new HashMap<String, Integer>(){{put("integer", 42);}});
+
+        PropertyConfig valuesConfig = config.getPropertyConfig("values");
+
+        assertEquals("test.values", valuesConfig.getId());
+        assertEquals(42, valuesConfig.getIntegerProperty("integer"));
+    }
+
+    @Test
+    void property_NotFound_PropertyNotFoundException() {
+
+        PropertyConfig config = new PropertyConfig("test");
+
+        assertThrows(PropertyNotFoundException.class, () -> config.getProperty("unknown"));
+    }
+
+    @Test
+    void property_InvalidDataType_ClassCastException() {
+
+        PropertyConfig config = new PropertyConfig("test");
+        config.setProperty("date", 42);
+
+        assertThrows(ClassCastException.class, () -> config.getLocalDateTimeProperty("date"));
+    }
+
+    @Test
+    void property_OptionalLocalDateTime_EmptyOptional() {
+
+        PropertyConfig config = new PropertyConfig("test");
+
+        assertFalse(config.getOptionalLocalDateTimeProperty("date").isPresent());
+    }
+
+    @Test
+    void property_LocalDateTime_True() {
+
+        PropertyConfig config = new PropertyConfig("test");
+        config.setProperty("date", LocalDateTime.MIN);
+
+        assertEquals(LocalDateTime.MIN, config.getOptionalLocalDateTimeProperty("date").orElse(null));
+    }
+}

+ 33 - 0
connector-push-api/license.txt

@@ -0,0 +1,33 @@
+BSD-3-Clause
+
+Copyright (c) 2020 University of West Bohemia (UWB) https://www.zcu.cz/en, LESPROJEKT-SLUŽBY s.r.o. (LESP) https://www.lesprojekt.cz/en
+
+Copyright 2026 <COPYRIGHT HOLDER>
+
+Redistribution and use in source and binary
+forms, with or without modification, are permitted provided that the following
+conditions are met:
+
+1. Redistributions of source code must retain the above
+copyright notice, this list of conditions and the following disclaimer.
+
+2.
+Redistributions in binary form must reproduce the above copyright notice, this
+list of conditions and the following disclaimer in the documentation and/or
+other materials provided with the distribution.
+
+3. Neither the name of the
+copyright holder nor the names of its contributors may be used to endorse or
+promote products derived from this software without specific prior written
+permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+“AS IS” AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
+TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

+ 33 - 0
connector-push-api/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">
+    <parent>
+        <artifactId>connector-period</artifactId>
+        <groupId>cz.senslog</groupId>
+        <version>1.0-SNAPSHOT</version>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+
+    <artifactId>connector-push-api</artifactId>
+    <name>push-api</name>
+    <packaging>jar</packaging>
+
+    <dependencies>
+        <dependency>
+            <groupId>cz.senslog</groupId>
+            <artifactId>connector-model</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>

+ 56 - 0
connector-push-api/src/main/java/cz/senslog/connector/push/ConnectorPush.java

@@ -0,0 +1,56 @@
+// Copyright (c) 2026 UWB & LESP.
+// The UWB & LESP license this file to you under the BSD-3-Clause license.
+
+package cz.senslog.connector.push;
+
+import cz.senslog.connector.push.api.ConnectorPushProvider;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+import java.util.*;
+
+/**
+ * The class {@code ConnectorPush} represents a loader for classes implement {@link ConnectorPushProvider}.
+ * For this is used technology Java Service Provider Interface (SPI).
+ *
+ * @author Lukas Cerny
+ * @version 1.0
+ * @since 1.0
+ */
+public final class ConnectorPush {
+
+    private static final Logger logger = LogManager.getLogger(ConnectorPush.class);
+
+    /** Map of implementations. */
+    private static final Map<Class<? extends ConnectorPushProvider>, ConnectorPushProvider> services;
+
+    static {
+        services = loadAll();
+    }
+
+    /**
+     * Loads and saves all available implementations of {@link ConnectorPushProvider}.
+     * @return Map of all implementations.
+     */
+    private static Map<Class<? extends ConnectorPushProvider>, ConnectorPushProvider> loadAll() {
+        logger.debug("Getting all implementation of the class {}.", ConnectorPushProvider.class);
+        Map<Class<? extends ConnectorPushProvider>, ConnectorPushProvider> services = new HashMap<>();
+        ServiceLoader<ConnectorPushProvider> loader = ServiceLoader.load(ConnectorPushProvider.class);
+        for (ConnectorPushProvider connectorProvider : loader) {
+            logger.debug("Loaded the class {}.", connectorProvider.getClass());
+            services.put(connectorProvider.getClass(), connectorProvider);
+        }
+        logger.info("Successfully loaded {} class of the {}.",  services.size(), ConnectorPushProvider.class);
+        return services;
+    }
+
+    /**
+     * Returns an implementation of provider according to input class.
+     * @param providerClass - class which is find an implementation.
+     * @return implementation type to {@link ConnectorPushProvider}.
+     */
+    public static ConnectorPushProvider getProvider(Class<?> providerClass) {
+        return services.get(providerClass);
+    }
+
+}

+ 25 - 0
connector-push-api/src/main/java/cz/senslog/connector/push/api/ConnectorPushProvider.java

@@ -0,0 +1,25 @@
+// Copyright (c) 2026 UWB & LESP.
+// The UWB & LESP license this file to you under the BSD-3-Clause license.
+
+package cz.senslog.connector.push.api;
+
+import cz.senslog.connector.model.config.DefaultConfig;
+import cz.senslog.connector.push.ConnectorPush;
+
+/**
+ * The interface {@code ConnectorPushProvider} provides a generic communication interface to create a new pusher.
+ *
+ * @author Lukas Cerny
+ * @version 1.0
+ * @since 1.0
+ */
+public interface ConnectorPushProvider {
+
+    /**
+     * Creates a new instance of {@link ConnectorPush}. This method receive default
+     * configuration {@link DefaultConfig} which is used to configure the new instance of pusher.
+     * @param config - default configuration.
+     * @return new instance of {@link ConnectorPush}.
+     */
+    ConnectorPusher createPusher(DefaultConfig config);
+}

+ 30 - 0
connector-push-api/src/main/java/cz/senslog/connector/push/api/ConnectorPusher.java

@@ -0,0 +1,30 @@
+// Copyright (c) 2026 UWB & LESP.
+// The UWB & LESP license this file to you under the BSD-3-Clause license.
+
+package cz.senslog.connector.push.api;
+
+import cz.senslog.connector.model.api.AbstractModel;
+
+/**
+ * The interface {@code ConnectorPusher} provides a generic communication interface for pushers.
+ *
+ * @param <T> generic parameter of model which the method 'push' receives as an input.
+ *
+ * @author Lukas Cerny
+ * @version 1.0
+ * @since 1.0
+ */
+public interface ConnectorPusher<T extends AbstractModel> {
+
+    /**
+     * Initialization of pusher. Method is called only once when is created a new connector.
+     * @throws Exception throws when initialization is not successful.
+     */
+    void init() throws Exception;
+
+    /**
+     * Method is periodically scheduled and contains logic of pusher.
+     * @param model - input model contains information to be send.
+     */
+    void push(T model);
+}

+ 33 - 0
connector-push-senslog/license.txt

@@ -0,0 +1,33 @@
+BSD-3-Clause
+
+Copyright (c) 2020 University of West Bohemia (UWB) https://www.zcu.cz/en, LESPROJEKT-SLUŽBY s.r.o. (LESP) https://www.lesprojekt.cz/en
+
+Copyright 2026 <COPYRIGHT HOLDER>
+
+Redistribution and use in source and binary
+forms, with or without modification, are permitted provided that the following
+conditions are met:
+
+1. Redistributions of source code must retain the above
+copyright notice, this list of conditions and the following disclaimer.
+
+2.
+Redistributions in binary form must reproduce the above copyright notice, this
+list of conditions and the following disclaimer in the documentation and/or
+other materials provided with the distribution.
+
+3. Neither the name of the
+copyright holder nor the names of its contributors may be used to endorse or
+promote products derived from this software without specific prior written
+permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+“AS IS” AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
+TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

+ 39 - 0
connector-push-senslog/pom.xml

@@ -0,0 +1,39 @@
+<?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-senslog</artifactId>
+    <name>push-senslog</name>
+    <packaging>jar</packaging>
+
+    <dependencies>
+        <dependency>
+            <groupId>cz.senslog</groupId>
+            <artifactId>connector-push-api</artifactId>
+            <version>${project.parent.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>cz.senslog</groupId>
+            <artifactId>connector-model</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>

+ 32 - 0
connector-push-senslog/src/main/java/cz/senslog/connector/push/senslog/ConnectorPushSensLogProvider.java

@@ -0,0 +1,32 @@
+// Copyright (c) 2026 UWB & LESP.
+// The UWB & LESP license this file to you under the BSD-3-Clause license.
+
+package cz.senslog.connector.push.senslog;
+
+import cz.senslog.connector.model.config.DefaultConfig;
+import cz.senslog.connector.push.api.ConnectorPushProvider;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+import static cz.senslog.connector.tools.http.HttpClient.newHttpClient;
+
+public class ConnectorPushSensLogProvider implements ConnectorPushProvider {
+
+    private static final Logger logger = LogManager.getLogger(ConnectorPushSensLogProvider.class);
+
+
+    @Override
+    public SensLogPusher createPusher(DefaultConfig config) {
+        logger.info("Initialization a new push provider {}.", ConnectorPushSensLogProvider.class);
+
+        logger.debug("Creating a new configuration.");
+        SensLogConfig defaultConfig = new SensLogConfig(config);
+        logger.info("Configuration for {} was created successfully.", SensLogPusher.class);
+
+        logger.debug("Creating a new instance of {}.", SensLogPusher.class);
+        SensLogPusher pusher = new SensLogPusher(defaultConfig, newHttpClient());
+        logger.info("Pusher for {} was created successfully.", SensLogPusher.class);
+
+        return pusher;
+    }
+}

+ 19 - 0
connector-push-senslog/src/main/java/cz/senslog/connector/push/senslog/SensLogConfig.java

@@ -0,0 +1,19 @@
+// Copyright (c) 2026 UWB & LESP.
+// The UWB & LESP license this file to you under the BSD-3-Clause license.
+
+package cz.senslog.connector.push.senslog;
+
+import cz.senslog.connector.model.config.DefaultConfig;
+
+public class SensLogConfig {
+
+    private final String baseUrl;
+
+    SensLogConfig(DefaultConfig defaultConfig) {
+        this.baseUrl = defaultConfig.getStringProperty("baseUrl");
+    }
+
+    public String getBaseUrl() {
+        return baseUrl;
+    }
+}

+ 85 - 0
connector-push-senslog/src/main/java/cz/senslog/connector/push/senslog/SensLogPusher.java

@@ -0,0 +1,85 @@
+// Copyright (c) 2026 UWB & LESP.
+// The UWB & LESP license this file to you under the BSD-3-Clause license.
+
+package cz.senslog.connector.push.senslog;
+
+import cz.senslog.connector.model.senslog.SensLogModel;
+import cz.senslog.connector.push.api.ConnectorPusher;
+import cz.senslog.connector.tools.http.*;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Queue;
+import java.util.concurrent.LinkedTransferQueue;
+
+import static cz.senslog.connector.tools.http.HttpContentType.APPLICATION_JSON;
+
+public class SensLogPusher implements ConnectorPusher<SensLogModel> {
+
+    private static final Logger logger = LogManager.getLogger(SensLogPusher.class);
+
+    private final SensLogConfig config;
+    private final HttpClient httpClient;
+
+    private final List<HttpRequest> failedRequests;
+    private final Queue<HttpRequest> requestQueue;
+
+    public SensLogPusher(SensLogConfig config, HttpClient httpClient) {
+        this.config = config;
+        this.httpClient = httpClient;
+        this.failedRequests = new ArrayList<>();
+        this.requestQueue = new LinkedTransferQueue<>();
+    }
+
+    @Override
+    public void init() {}
+
+    @Override
+    public void push(SensLogModel model) {
+        if (model == null || model.getPassThroughData() == null || model.getPassThroughData().isEmpty()) {
+            logger.warn("Model has no observations."); return;
+        }
+
+        for (SensLogModel.PassingData data : model.getPassThroughData()) {
+
+            URLBuilder urlBuilder = URLBuilder.newBuilder(config.getBaseUrl());
+            data.getParams().forEach(urlBuilder::addParam);
+
+            HttpRequest.Builder req = HttpRequest.newBuilder().POST()
+                    .url(urlBuilder.build())
+                    .contentType(APPLICATION_JSON)
+                    .body(data.getPayload());
+
+            requestQueue.add(req.build());
+        }
+
+        if (!failedRequests.isEmpty()) {
+            logger.info("Adding <{}> failed requests to the queue.", failedRequests.size());
+            requestQueue.addAll(failedRequests);
+            failedRequests.clear();
+        }
+
+        int totalToPush = requestQueue.size();
+        int successfullyPushed = 0;
+        while (!requestQueue.isEmpty()) {
+            HttpRequest request = requestQueue.remove();
+            HttpResponse res = httpClient.send(request);
+
+            if (res.isError()) {
+                logger.error("Request error <{}> with reason: {}", res.getStatus(), res.getBody());
+                logger.error(request.getBody());
+                failedRequests.add(request);
+                continue;
+            }
+
+            if (res.isOk()) {
+                logger.debug("Pushed <{}> successfully: {}", res.getStatus(), res.getBody());
+                successfullyPushed++;
+            }
+        }
+
+        logger.info("Pushed <{}/{}> payloads. For the time <{} - {}>", successfullyPushed, totalToPush, model.getFrom(), model.getTo());
+    }
+}

+ 1 - 0
connector-push-senslog/src/main/resources/META-INF/services/cz.senslog.connector.push.api.ConnectorPushProvider

@@ -0,0 +1 @@
+cz.senslog.connector.push.senslog.ConnectorPushSensLogProvider

+ 33 - 0
connector-tools/license.txt

@@ -0,0 +1,33 @@
+BSD-3-Clause
+
+Copyright (c) 2020 University of West Bohemia (UWB) https://www.zcu.cz/en, LESPROJEKT-SLUŽBY s.r.o. (LESP) https://www.lesprojekt.cz/en
+
+Copyright 2026 <COPYRIGHT HOLDER>
+
+Redistribution and use in source and binary
+forms, with or without modification, are permitted provided that the following
+conditions are met:
+
+1. Redistributions of source code must retain the above
+copyright notice, this list of conditions and the following disclaimer.
+
+2.
+Redistributions in binary form must reproduce the above copyright notice, this
+list of conditions and the following disclaimer in the documentation and/or
+other materials provided with the distribution.
+
+3. Neither the name of the
+copyright holder nor the names of its contributors may be used to endorse or
+promote products derived from this software without specific prior written
+permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+“AS IS” AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
+TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

+ 88 - 0
connector-tools/pom.xml

@@ -0,0 +1,88 @@
+<?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">
+    <parent>
+        <artifactId>connector-period</artifactId>
+        <groupId>cz.senslog</groupId>
+        <version>1.0-SNAPSHOT</version>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+
+    <artifactId>connector-tools</artifactId>
+    <name>tools</name>
+
+    <dependencies>
+        <dependency>
+            <groupId>com.google.code.gson</groupId>
+            <artifactId>gson</artifactId>
+            <version>2.10.1</version>
+        </dependency>
+
+        <dependency>
+            <groupId>org.apache.httpcomponents.client5</groupId>
+            <artifactId>httpclient5</artifactId>
+            <version>5.2.1</version>
+        </dependency>
+
+        <dependency>
+            <groupId>org.yaml</groupId>
+            <artifactId>snakeyaml</artifactId>
+            <version>2.0</version>
+        </dependency>
+
+        <dependency>
+            <groupId>org.apache.logging.log4j</groupId>
+            <artifactId>log4j-api</artifactId>
+            <version>2.20.0</version>
+        </dependency>
+
+        <dependency>
+            <groupId>org.apache.logging.log4j</groupId>
+            <artifactId>log4j-core</artifactId>
+            <version>2.20.0</version>
+        </dependency>
+
+        <dependency>
+            <groupId>org.apache.logging.log4j</groupId>
+            <artifactId>log4j-slf4j-impl</artifactId>
+            <version>2.20.0</version>
+        </dependency>
+
+        <dependency>
+            <groupId>com.github.erosb</groupId>
+            <artifactId>everit-json-schema</artifactId>
+            <version>1.14.2</version>
+        </dependency>
+
+        <dependency>
+            <groupId>org.junit.jupiter</groupId>
+            <artifactId>junit-jupiter-api</artifactId>
+            <version>5.9.2</version>
+<!--            <scope>test</scope>-->
+        </dependency>
+
+        <dependency>
+            <groupId>org.junit.jupiter</groupId>
+            <artifactId>junit-jupiter</artifactId>
+            <version>5.9.2</version>
+<!--            <scope>test</scope>-->
+        </dependency>
+
+        <dependency>
+            <groupId>org.mockito</groupId>
+            <artifactId>mockito-core</artifactId>
+            <version>4.11.0</version>
+<!--            <scope>test</scope>-->
+        </dependency>
+
+    </dependencies>
+
+    <repositories>
+        <repository>
+            <id>jitpack.io</id>
+            <url>https://jitpack.io</url>
+        </repository>
+    </repositories>
+
+</project>

+ 11 - 0
connector-tools/src/main/java/cz/senslog/connector/tools/exception/ModuleInterruptedException.java

@@ -0,0 +1,11 @@
+// Copyright (c) 2026 UWB & LESP.
+// The UWB & LESP license this file to you under the BSD-3-Clause license.
+
+package cz.senslog.connector.tools.exception;
+
+public class ModuleInterruptedException extends RuntimeException{
+
+    public ModuleInterruptedException(String message) {
+        super(message);
+    }
+}

+ 11 - 0
connector-tools/src/main/java/cz/senslog/connector/tools/exception/ParseException.java

@@ -0,0 +1,11 @@
+// Copyright (c) 2026 UWB & LESP.
+// The UWB & LESP license this file to you under the BSD-3-Clause license.
+
+package cz.senslog.connector.tools.exception;
+
+public class ParseException extends RuntimeException {
+
+    public ParseException(String message) {
+        super(message);
+    }
+}

+ 11 - 0
connector-tools/src/main/java/cz/senslog/connector/tools/exception/PropertyNotFoundException.java

@@ -0,0 +1,11 @@
+// Copyright (c) 2026 UWB & LESP.
+// The UWB & LESP license this file to you under the BSD-3-Clause license.
+
+package cz.senslog.connector.tools.exception;
+
+public class PropertyNotFoundException extends RuntimeException {
+
+    public PropertyNotFoundException(String message) {
+        super(message);
+    }
+}

+ 11 - 0
connector-tools/src/main/java/cz/senslog/connector/tools/exception/SyntaxException.java

@@ -0,0 +1,11 @@
+// Copyright (c) 2026 UWB & LESP.
+// The UWB & LESP license this file to you under the BSD-3-Clause license.
+
+package cz.senslog.connector.tools.exception;
+
+public class SyntaxException extends RuntimeException {
+
+    public SyntaxException(String message) {
+        super(message);
+    }
+}

+ 13 - 0
connector-tools/src/main/java/cz/senslog/connector/tools/exception/UnsupportedFileException.java

@@ -0,0 +1,13 @@
+// Copyright (c) 2026 UWB & LESP.
+// The UWB & LESP license this file to you under the BSD-3-Clause license.
+
+package cz.senslog.connector.tools.exception;
+
+import java.io.IOException;
+
+public class UnsupportedFileException extends IOException {
+
+    public UnsupportedFileException(String message) {
+        super(message);
+    }
+}

+ 246 - 0
connector-tools/src/main/java/cz/senslog/connector/tools/http/HttpClient.java

@@ -0,0 +1,246 @@
+// Copyright (c) 2026 UWB & LESP.
+// The UWB & LESP license this file to you under the BSD-3-Clause license.
+
+package cz.senslog.connector.tools.http;
+
+import cz.senslog.connector.tools.util.StringUtils;
+import org.apache.hc.client5.http.classic.methods.HttpGet;
+import org.apache.hc.client5.http.classic.methods.HttpPost;
+import org.apache.hc.client5.http.classic.methods.HttpUriRequestBase;
+import org.apache.hc.client5.http.config.ConnectionConfig;
+import org.apache.hc.client5.http.config.RequestConfig;
+import org.apache.hc.client5.http.cookie.BasicCookieStore;
+import org.apache.hc.client5.http.cookie.CookieStore;
+import org.apache.hc.client5.http.cookie.StandardCookieSpec;
+import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
+import org.apache.hc.client5.http.impl.classic.HttpClients;
+import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManager;
+import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManagerBuilder;
+import org.apache.hc.core5.http.Header;
+import org.apache.hc.core5.http.HttpEntity;
+import org.apache.hc.core5.http.HttpMessage;
+import org.apache.hc.core5.http.io.SocketConfig;
+import org.apache.hc.core5.http.io.entity.StringEntity;
+import org.apache.hc.core5.pool.PoolConcurrencyPolicy;
+import org.apache.hc.core5.pool.PoolReusePolicy;
+import org.apache.hc.core5.util.TimeValue;
+import org.apache.hc.core5.util.Timeout;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.nio.charset.Charset;
+import java.util.HashMap;
+import java.util.Map;
+
+import static org.apache.hc.core5.http.HttpHeaders.*;
+
+
+/**
+ * The class {@code HttpClient} represents a wrapper for {@link org.apache.hc.client5.http.classic.HttpClient}.
+ * Provides functionality of sending GET and POST request. Otherwise is returned response with {@see #BAD_REQUEST}.
+ *
+ * @author Lukas Cerny
+ * @version 1.0
+ * @since 1.0
+ */
+public class HttpClient {
+
+    /**
+     * Instance of http client.
+     */
+    private final org.apache.hc.client5.http.classic.HttpClient client;
+    private final CookieStore cookieStore;
+
+    private static HttpClient newClient() {
+
+        PoolingHttpClientConnectionManager connectionManager = PoolingHttpClientConnectionManagerBuilder.create()
+//                    .setSSLSocketFactory(SSLConnectionSocketFactoryBuilder.create()
+//                            .setSslContext(SSLContexts.createSystemDefault())
+////                            .setSslContext(sslContext)
+//                            .setTlsVersions(TLS.V_1_3)
+//                            .build())
+                .setDefaultSocketConfig(SocketConfig.custom()
+                        .setSoTimeout(Timeout.ofMinutes(1))
+                        .build())
+                .setPoolConcurrencyPolicy(PoolConcurrencyPolicy.STRICT)
+                .setConnPoolPolicy(PoolReusePolicy.LIFO)
+                .setDefaultConnectionConfig(ConnectionConfig.custom()
+                        .setSocketTimeout(Timeout.ofMinutes(1))
+                        .setConnectTimeout(Timeout.ofMinutes(1))
+                        .setTimeToLive(TimeValue.ofMinutes(10))
+                        .build())
+                .build();
+
+        CookieStore cookieStore = new BasicCookieStore();
+        CloseableHttpClient client = HttpClients.custom()
+                .setConnectionManager(connectionManager)
+                .setDefaultCookieStore(cookieStore)
+                .setDefaultRequestConfig(RequestConfig.custom()
+                        .setCookieSpec(StandardCookieSpec.STRICT)
+                        .build())
+                .build();
+
+        return new HttpClient(client, cookieStore);
+    }
+
+    /**
+     * Factory method to create a new instance of client.
+     *
+     * @return new instance of {@code HttpClient}.
+     */
+    public static HttpClient newHttpClient() {
+        return newClient();
+    }
+
+    /**
+     * Private constructors sets http client.
+     */
+    private HttpClient(org.apache.hc.client5.http.classic.HttpClient httpClient, CookieStore cookieStore) {
+        this.client = httpClient;
+        this.cookieStore = cookieStore;
+    }
+
+    /**
+     * 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, charset);
+                case POST:
+                    return sendPost(request, charset);
+                default:
+                    return HttpResponse.newBuilder()
+                            .body("Request does not contain method definition.")
+                            .status(HttpCode.METHOD_NOT_ALLOWED).build();
+            }
+        } catch (URISyntaxException e) {
+            return HttpResponse.newBuilder()
+                    .body(e.getMessage()).status(HttpCode.BAD_REQUEST)
+                    .build();
+        } catch (IOException e) {
+            return HttpResponse.newBuilder()
+                    .body(e.getMessage()).status(HttpCode.SERVER_ERROR)
+                    .build();
+        }
+    }
+
+    /**
+     * 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.
+     */
+    private HttpResponse sendGet(HttpRequest request, Charset charset) throws IOException, URISyntaxException {
+
+        URI uri = request.getUrl().toURI();
+        HttpGet requestGet = new HttpGet(uri);
+        setBasicHeaders(request, requestGet);
+
+        cookieStore.clear();
+        for (HttpCookie cookie : request.getCookies()) {
+            cookieStore.addCookie(cookie.get());
+        }
+
+        return client.execute(requestGet, res -> HttpResponse.newBuilder()
+                .status(res.getCode())
+                .headers(getHeaders(res))
+                .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.
+     */
+    private HttpResponse sendPost(HttpRequest request, Charset charset) throws URISyntaxException, IOException {
+
+        URI uri = request.getUrl().toURI();
+        HttpPost requestPost = new HttpPost(uri);
+        setBasicHeaders(request, requestPost);
+
+        if (StringUtils.isNotBlank(request.getContentType())) {
+            requestPost.setHeader(CONTENT_TYPE, request.getContentType());
+        }
+
+        requestPost.setEntity(new StringEntity(request.getBody()));
+
+        return client.execute(requestPost, res -> HttpResponse.newBuilder()
+                .status(res.getCode())
+                .headers(getHeaders(res))
+                .body(getBody(res.getEntity(), charset))
+                .build());
+    }
+
+    /**
+     * Sets basic headers to each request.
+     *
+     * @param userRequest - virtual request.
+     * @param httpRequest - real request prepared to send.
+     */
+    private void setBasicHeaders(HttpRequest userRequest, HttpUriRequestBase httpRequest) {
+
+        httpRequest.setHeader(USER_AGENT, "SenslogConnector/1.0");
+        httpRequest.setHeader(CACHE_CONTROL, "no-cache");
+
+        for (Map.Entry<String, String> headerEntry : userRequest.getHeaders().entrySet()) {
+            httpRequest.setHeader(headerEntry.getKey(), headerEntry.getValue());
+        }
+    }
+
+    /**
+     * Returns map of headers from the response.
+     *
+     * @param response - response message.
+     * @return map of headers.
+     */
+    private Map<String, String> getHeaders(HttpMessage response) {
+        Map<String, String> headers = new HashMap<>();
+        for (Header header : response.getHeaders()) {
+            headers.put(header.getName(), header.getValue());
+        }
+        return headers;
+    }
+
+    /**
+     * 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, charset);
+        BufferedReader rd = new BufferedReader(bodyStream);
+        StringBuilder bodyBuffer = new StringBuilder();
+        String line;
+        while ((line = rd.readLine()) != null) {
+            bodyBuffer.append(line);
+        }
+        return bodyBuffer.toString();
+    }
+}

+ 25 - 0
connector-tools/src/main/java/cz/senslog/connector/tools/http/HttpCode.java

@@ -0,0 +1,25 @@
+// Copyright (c) 2026 UWB & LESP.
+// The UWB & LESP license this file to you under the BSD-3-Clause license.
+
+package cz.senslog.connector.tools.http;
+
+public class HttpCode {
+
+    public static final int OK = 200;
+
+    public static final int NO_RESULT = 204;
+
+    public static final int NO_CONTENT = 204;
+
+    public static final int BAD_REQUEST = 400;
+
+    public static final int UNAUTHORIZED = 401;
+
+    public static final int FORBIDDEN = 403;
+
+    public static final int NOT_FOUND = 404;
+
+    public static final int METHOD_NOT_ALLOWED = 405;
+
+    public static final int SERVER_ERROR = 500;
+}

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

@@ -0,0 +1,20 @@
+// Copyright (c) 2026 UWB & LESP.
+// The UWB & LESP license this file to you under the BSD-3-Clause license.
+
+package cz.senslog.connector.tools.http;
+
+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_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";
+    public final static String APPLICATION_XML              = "application/xml";
+    public final static String MULTIPART_FORM_DATA          = "multipart/form-data";
+    public final static String TEXT_HTML                    = "text/html";
+    public final static String TEXT_PLAIN                   = "text/plain";
+    public final static String TEXT_XML                     = "text/xml";
+    public final static String WILDCARD                     = "*/*";
+}

+ 53 - 0
connector-tools/src/main/java/cz/senslog/connector/tools/http/HttpCookie.java

@@ -0,0 +1,53 @@
+// Copyright (c) 2026 UWB & LESP.
+// The UWB & LESP license this file to you under the BSD-3-Clause license.
+
+package cz.senslog.connector.tools.http;
+
+
+import org.apache.hc.client5.http.impl.cookie.BasicClientCookie;
+
+public class HttpCookie {
+
+    private final BasicClientCookie cookie;
+
+    public static HttpCookie empty() {
+        HttpCookie cookie = new HttpCookie("", "", "", "");
+        cookie.cookie.setSecure(false);
+        return cookie;
+    }
+
+    public HttpCookie(String name, String value, String domain, String path) {
+        this.cookie = new BasicClientCookie(name, value);
+        this.cookie.setDomain(domain);
+        this.cookie.setPath(path);
+        this.cookie.setSecure(true);
+    }
+
+    public String getName() {
+        return this.cookie.getName();
+    }
+
+    public String getValue() {
+        return this.cookie.getValue();
+    }
+
+    public String getDomain() {
+        return this.cookie.getDomain();
+    }
+
+    public String getPath() {
+        return this.cookie.getPath();
+    }
+
+    public boolean isSecure() {
+        return this.cookie.isSecure();
+    }
+
+    public boolean isNotSecure() {
+        return !this.cookie.isSecure();
+    }
+
+    public BasicClientCookie get() {
+        return this.cookie;
+    }
+}

+ 14 - 0
connector-tools/src/main/java/cz/senslog/connector/tools/http/HttpHeader.java

@@ -0,0 +1,14 @@
+// Copyright (c) 2026 UWB & LESP.
+// The UWB & LESP license this file to you under the BSD-3-Clause license.
+
+package cz.senslog.connector.tools.http;
+
+
+import org.apache.hc.core5.http.HttpHeaders;
+
+public final class HttpHeader {
+    public static final String AUTHORIZATION = HttpHeaders.AUTHORIZATION;
+    public static final String DATE = HttpHeaders.DATE;
+    public static final String ACCEPT = HttpHeaders.ACCEPT;
+    public static final String CONTENT_TYPE = HttpHeaders.CONTENT_TYPE;
+}

+ 8 - 0
connector-tools/src/main/java/cz/senslog/connector/tools/http/HttpMethod.java

@@ -0,0 +1,8 @@
+// Copyright (c) 2026 UWB & LESP.
+// The UWB & LESP license this file to you under the BSD-3-Clause license.
+
+package cz.senslog.connector.tools.http;
+
+public enum  HttpMethod {
+    GET, POST
+}

+ 105 - 0
connector-tools/src/main/java/cz/senslog/connector/tools/http/HttpRequest.java

@@ -0,0 +1,105 @@
+// Copyright (c) 2026 UWB & LESP.
+// The UWB & LESP license this file to you under the BSD-3-Clause license.
+
+package cz.senslog.connector.tools.http;
+
+import java.net.URL;
+import java.util.Map;
+
+/**
+ * The class {@code HttpRequest} represents a wrapper for a http request.
+ *
+ * @author Lukas Cerny
+ * @version 1.0
+ * @since 1.0
+ */
+public class HttpRequest {
+
+    public interface Builder {
+        Builder header(String name, String value);
+        Builder url(URL url);
+        Builder POST();
+        Builder GET();
+        Builder contentType(String contentType);
+        Builder body(String body);
+        Builder addCookie(HttpCookie cookie);
+        HttpRequest build();
+    }
+
+    /**
+     * Factory method to create a new builder for {@link HttpRequest}.
+     * @return new instance of builder.
+     */
+    public static Builder newBuilder() {
+        return new HttpRequestBuilder();
+    }
+
+    /**
+     * Factory method to create a new builder for {@link HttpRequest}.
+     * @param url - host url.
+     * @return new instance of builder.
+     */
+    public static Builder newBuilder(URL url) {
+        HttpRequestBuilder builder = new HttpRequestBuilder();
+        builder.url(url);
+        return builder;
+    }
+
+    /** Request url. */
+    private final URL url;
+
+    /** Request headers. */
+    private final Map<String, String> headers;
+
+    /** Request body. */
+    private final String body;
+
+    /** Request method. */
+    private final HttpMethod method;
+
+    /** Request content type. */
+    private final String contentType;
+
+    private final HttpCookie [] cookies;
+
+    /**
+     * Constructors sets all attributes.
+     * @param url - url.
+     * @param headers - headers.
+     * @param body - body.
+     * @param method - method.
+     * @param contentType - content type.
+     */
+    HttpRequest(URL url, Map<String, String> headers, String body, HttpMethod method, String contentType, HttpCookie [] cookies) {
+        this.url = url;
+        this.headers = headers;
+        this.body = body;
+        this.method = method;
+        this.contentType = contentType;
+        this.cookies = cookies;
+    }
+
+    public URL getUrl() {
+        return url;
+    }
+
+    public String getBody() {
+        return body;
+    }
+
+    public HttpMethod getMethod() {
+        return method;
+    }
+
+    public Map<String, String> getHeaders() {
+        return headers;
+    }
+
+    public String getContentType() {
+        return contentType;
+    }
+
+    public HttpCookie[] getCookies() {
+        return cookies;
+    }
+}

+ 82 - 0
connector-tools/src/main/java/cz/senslog/connector/tools/http/HttpRequestBuilder.java

@@ -0,0 +1,82 @@
+// Copyright (c) 2026 UWB & LESP.
+// The UWB & LESP license this file to you under the BSD-3-Clause license.
+
+package cz.senslog.connector.tools.http;
+
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * The class {@code HttpRequestBuilder} represents a builder for the {@link HttpRequest}.
+ *
+ * @author Lukas Cerny
+ * @version 1.0
+ * @since 1.0
+ */
+final class HttpRequestBuilder implements HttpRequest.Builder {
+
+    private URL url;
+    private Map<String, String> headers;
+    private final List<HttpCookie> cookies;
+    private String body;
+    private HttpMethod method;
+    private String contentType;
+
+    HttpRequestBuilder() {
+        this.headers = new HashMap<>();
+        this.cookies = new ArrayList<>();
+        this.method = HttpMethod.GET;
+        this.body = "";
+    }
+
+
+    @Override
+    public HttpRequest.Builder header(String name, String value) {
+        this.headers.put(name, value);
+        return this;
+    }
+
+    @Override
+    public HttpRequest.Builder url(URL url) {
+        this.url = url;
+        return this;
+    }
+
+    @Override
+    public HttpRequest.Builder POST() {
+        this.method = HttpMethod.POST;
+        return this;
+    }
+
+    @Override
+    public HttpRequest.Builder GET() {
+        this.method = HttpMethod.GET;
+        return this;
+    }
+
+    @Override
+    public HttpRequest.Builder contentType(String contentType) {
+        this.contentType = contentType;
+        return this;
+    }
+
+    @Override
+    public HttpRequest.Builder body(String body) {
+        this.body = body;
+        return this;
+    }
+
+    @Override
+    public HttpRequest.Builder addCookie(HttpCookie cookie) {
+        this.cookies.add(cookie);
+        return this;
+    }
+
+    @Override
+    public HttpRequest build() {
+        return new HttpRequest(url, headers, body, method, contentType, cookies.toArray(new HttpCookie[0]));
+    }
+}

+ 80 - 0
connector-tools/src/main/java/cz/senslog/connector/tools/http/HttpResponse.java

@@ -0,0 +1,80 @@
+// Copyright (c) 2026 UWB & LESP.
+// The UWB & LESP license this file to you under the BSD-3-Clause license.
+
+package cz.senslog.connector.tools.http;
+
+import cz.senslog.connector.tools.json.BasicJson;
+
+import java.util.Map;
+
+/**
+ * The class {@code HttpResponse} represents a wrapper for a http response.
+ * Contains basic information like status, headers and body.
+ *
+ * @author Lukas Cerny
+ * @version 1.0
+ * @since 1.0
+ */
+public class HttpResponse {
+
+    public interface Builder {
+        Builder body(String body);
+        Builder headers(Map<String, String> headers);
+        Builder status(int status);
+        HttpResponse build();
+    }
+
+    /**
+     * Factory method to create a new builder for {@link HttpResponse}.
+     * @return new instance of builder.
+     */
+    public static Builder newBuilder() {
+        return new HttpResponseBuilder();
+    }
+
+    /** Response body. */
+    private final String body;
+
+    /** Response headers. */
+    private final Map<String, String> headers;
+
+    /** Response status. */
+    private final int status;
+
+    /**
+     * Constructors sets all attributes.
+     * @param body - body.
+     * @param headers - headers.
+     * @param status - status.
+     */
+    HttpResponse(String body, Map<String, String> headers, int status) {
+        this.body = body;
+        this.headers = headers;
+        this.status = status;
+    }
+
+    public String getBody() {
+        return body;
+    }
+
+    public String getHeader(String value) {
+        return headers.get(value);
+    }
+
+    public int getStatus() {
+        return status;
+    }
+
+    public boolean isOk() {
+        return status == HttpCode.OK;
+    }
+
+    public boolean isError() {
+        return !isOk();
+    }
+
+    @Override
+    public String toString() {
+        return BasicJson.objectToJson(this);
+    }
+}

+ 45 - 0
connector-tools/src/main/java/cz/senslog/connector/tools/http/HttpResponseBuilder.java

@@ -0,0 +1,45 @@
+// Copyright (c) 2026 UWB & LESP.
+// The UWB & LESP license this file to you under the BSD-3-Clause license.
+
+package cz.senslog.connector.tools.http;
+
+import java.util.Map;
+
+/**
+ * The class {@code HttpResponseBuilder} represents a builder for the {@link HttpResponse}.
+ *
+ * @author Lukas Cerny
+ * @version 1.0
+ * @since 1.0
+ */
+class HttpResponseBuilder implements HttpResponse.Builder {
+
+    private String body;
+    private Map<String, String> headers;
+    private int status;
+
+    HttpResponseBuilder(){}
+
+    @Override
+    public HttpResponse.Builder body(String body) {
+        this.body = body;
+        return this;
+    }
+
+    @Override
+    public HttpResponse.Builder headers(Map<String, String> headers) {
+        this.headers = headers;
+        return this;
+    }
+
+    @Override
+    public HttpResponse.Builder status(int status) {
+        this.status = status;
+        return this;
+    }
+
+    @Override
+    public HttpResponse build() {
+        return new HttpResponse(body, headers, status);
+    }
+}

+ 114 - 0
connector-tools/src/main/java/cz/senslog/connector/tools/http/URLBuilder.java

@@ -0,0 +1,114 @@
+// Copyright (c) 2026 UWB & LESP.
+// The UWB & LESP license this file to you under the BSD-3-Clause license.
+
+package cz.senslog.connector.tools.http;
+
+import java.io.UnsupportedEncodingException;
+import java.net.MalformedURLException;
+import java.net.URL;
+
+import static java.net.URLEncoder.encode;
+
+/**
+ * The class {@code URLBuilder} represents a builder to create a new instance of {@link URL}.
+ * Provides a creating a url from domain and path and adding a parameter.
+ *
+ * @author Lukas Cerny
+ * @version 1.0
+ * @since 1.0
+ */
+public final class URLBuilder {
+
+    /**
+     * Factory method to create a new instance of {@code URLBuilder} from base url.
+     * @param baseURL - host url.
+     * @return new instance of {@code URLBuilder}.
+     */
+    public static URLBuilder newBuilder(String baseURL) {
+        return new URLBuilder(baseURL);
+    }
+
+    /**
+     * Factory method to create a new instance of {@code URLBuilder} from domain and path.
+     * Normalizes domain and path to the form:
+     * domain: http://domain.com/
+     * path: /host
+     * -> url: http://domain.com/host
+     * domain: http://domain.com
+     * path: host
+     * -> url: http://domain.com/host
+     *
+     * @param domain - domain of host.
+     * @param path - path of host.
+     * @return new instance of {@code URLBuilder}.
+     */
+    public static URLBuilder newBuilder(String domain, String path) {
+        boolean domainSlash = domain.endsWith("/");
+        boolean pathSlash = path.startsWith("/");
+
+        if ((domainSlash && !pathSlash) || (!domainSlash && pathSlash)) {
+            return new URLBuilder(domain + path);
+        } else if (domainSlash) {
+            return new URLBuilder(domain + path.substring(1));
+        } else {
+            return new URLBuilder(domain + "/" + path);
+        }
+    }
+
+    /** String builder for url. */
+    private StringBuilder urlBuilder;
+
+    /** String builder for parameters. */
+    private StringBuilder paramsBuilder;
+
+    /**
+     * Private constructor initializes builders and normalizes url.
+     * If the url ends with slash '/', it is removed.
+     * @param baseURL - host url.
+     */
+    private URLBuilder(String baseURL) {
+        String url = baseURL.endsWith("/") ? baseURL.substring(0, baseURL.length() - 1) : baseURL;
+        this.urlBuilder = new StringBuilder(url);
+        this.paramsBuilder = new StringBuilder();
+    }
+
+    /**
+     * Adds a new parameter to the url.
+     * @param name - name of parameter.
+     * @param value - value of parameter.
+     * @return instance of {@code URLBuilder}.
+     */
+    public URLBuilder addParam(String name, String value) {
+        try {
+            paramsBuilder.append("&").append(name).append("=").append(encode(value, "UTF-8"));
+        } catch (UnsupportedEncodingException e) {
+            throw new AssertionError(e.getMessage());
+        }
+        return this;
+    }
+
+    /**
+     * Adds a new parameter to the url.
+     * @param name - name of parameter.
+     * @param value - value of parameter.
+     * @return instance of {@code URLBuilder}.
+     */
+    public URLBuilder addParam(String name, Object value) {
+        if (value == null) return this;
+        return addParam(name, value.toString());
+    }
+
+    /**
+     * Creates a new instance of {@link URL}.
+     * @return new instance of {@link URL}.
+     */
+    public URL build() {
+        try {
+            String params = paramsBuilder.replace(0, 1, "").toString();
+            return new URL(urlBuilder.append(params.isEmpty() ? "" : ("?" + params)).toString());
+        } catch (MalformedURLException e) {
+            throw new IllegalArgumentException(e.getMessage(), e);
+        }
+    }
+}
+

+ 196 - 0
connector-tools/src/main/java/cz/senslog/connector/tools/json/BasicJson.java

@@ -0,0 +1,196 @@
+// Copyright (c) 2026 UWB & LESP.
+// The UWB & LESP license this file to you under the BSD-3-Clause license.
+
+package cz.senslog.connector.tools.json;
+
+import com.google.gson.*;
+import cz.senslog.connector.tools.exception.ParseException;
+import cz.senslog.connector.tools.exception.SyntaxException;
+import cz.senslog.connector.tools.util.Tuple;
+
+import java.lang.reflect.ParameterizedType;
+import java.lang.reflect.Type;
+import java.time.LocalDateTime;
+import java.time.LocalTime;
+import java.time.OffsetDateTime;
+import java.time.ZonedDateTime;
+import java.util.Optional;
+
+import static java.time.format.DateTimeFormatter.*;
+
+/**
+ * The class {@code BasicJson} represents a basic wrapper for {@link Gson} library.
+ * Provides basic converter from object to string and string to object.
+
+ * Configuration contains basic formatters for {@see LocalDateTime}, {@see ZonedDateTime} and {@see Class}.
+
+
+ * Both time classes are formatter to ISO format e.q. '2011-12-03T10:15:30',
+ * '2011-12-03T10:15:30+01:00' or '2011-12-03T10:15:30+01:00[Europe/Paris]'.
+
+ * Class is formatted as the full name of the class.
+ *
+ * @author Lukas Cerny
+ * @version 1.0
+ * @since 1.0
+ */
+public class BasicJson {
+
+    /** Instance of json converter. */
+    private static final Gson gson = new GsonBuilder()
+            .registerTypeAdapter(LocalDateTime.class, new LocalDateTimeAdapter())
+            .registerTypeAdapter(LocalTime.class, new LocalTimeAdapter())
+            .registerTypeAdapter(ZonedDateTime.class, new ZonedDateTimeAdapter())
+            .registerTypeAdapter(OffsetDateTime.class, new OffsetDateTimeAdapter())
+            .registerTypeAdapter(Class.class, new ClassAdapter())
+            .registerTypeAdapter(Optional.class, new OptionalAdapter())
+            .create();
+
+    /** Formatter for {@see LocalDateTime}. */
+    private static class LocalDateTimeAdapter implements JsonSerializer<LocalDateTime>, JsonDeserializer<LocalDateTime> {
+
+        @Override
+        public JsonElement serialize(LocalDateTime localDateTime, Type type, JsonSerializationContext jsonSerializationContext) {
+            return new JsonPrimitive(localDateTime.format(ISO_DATE_TIME));
+        }
+
+        @Override
+        public LocalDateTime deserialize(JsonElement jsonElement, Type type, JsonDeserializationContext jsonDeserializationContext) throws JsonParseException {
+            return LocalDateTime.parse(jsonElement.getAsString(), ISO_DATE_TIME);
+        }
+    }
+
+    /** Formatter for {@see LocalDateTime}. */
+    private static class LocalTimeAdapter implements JsonSerializer<LocalTime>, JsonDeserializer<LocalTime> {
+
+        @Override
+        public JsonElement serialize(LocalTime localTime, Type type, JsonSerializationContext jsonSerializationContext) {
+            return new JsonPrimitive(localTime.format(ISO_TIME));
+        }
+
+        @Override
+        public LocalTime deserialize(JsonElement jsonElement, Type type, JsonDeserializationContext jsonDeserializationContext) throws JsonParseException {
+            return LocalTime.parse(jsonElement.getAsString(), ISO_TIME);
+        }
+    }
+
+    /** Formatter for {@see ZonedDateTime}. */
+    private static class ZonedDateTimeAdapter implements JsonSerializer<ZonedDateTime>, JsonDeserializer<ZonedDateTime> {
+
+        @Override
+        public JsonElement serialize(ZonedDateTime zonedDateTime, Type type, JsonSerializationContext jsonSerializationContext) {
+            return new JsonPrimitive(zonedDateTime.format(ISO_DATE_TIME));
+        }
+
+        @Override
+        public ZonedDateTime deserialize(JsonElement jsonElement, Type type, JsonDeserializationContext jsonDeserializationContext) throws JsonParseException {
+            return ZonedDateTime.parse(jsonElement.getAsString(), ISO_DATE_TIME);
+        }
+    }
+
+    /** Formatter for {@see OffsetDateTime}. */
+    private static class OffsetDateTimeAdapter implements JsonSerializer<OffsetDateTime>, JsonDeserializer<OffsetDateTime> {
+
+        @Override
+        public JsonElement serialize(OffsetDateTime offsetDateTime, Type type, JsonSerializationContext jsonSerializationContext) {
+            return new JsonPrimitive(offsetDateTime.format(ISO_OFFSET_DATE_TIME));
+        }
+
+        @Override
+        public OffsetDateTime deserialize(JsonElement jsonElement, Type type, JsonDeserializationContext jsonDeserializationContext) throws JsonParseException {
+            return OffsetDateTime.parse(jsonElement.getAsString(), ISO_OFFSET_DATE_TIME);
+        }
+    }
+
+    /** Formatter for {@see Class}. */
+    private static class ClassAdapter implements JsonSerializer<Class<?>>, JsonDeserializer<Class<?>> {
+
+        @Override
+        public JsonElement serialize(Class<?> aClass, Type type, JsonSerializationContext jsonSerializationContext) {
+            return new JsonPrimitive(aClass.getName());
+        }
+
+        @Override
+        public Class<?> deserialize(JsonElement jsonElement, Type type, JsonDeserializationContext jsonDeserializationContext) throws JsonParseException {
+            try {
+                return Class.forName(jsonElement.getAsString());
+            } catch (ClassNotFoundException e) {
+                return null;
+            }
+        }
+    }
+
+    private static class OptionalAdapter implements JsonSerializer<Optional<?>>, JsonDeserializer<Optional<?>> {
+
+        @Override
+        public JsonElement serialize(Optional<?> optional, Type type, JsonSerializationContext jsonSerializationContext) {
+            return new JsonPrimitive(optional.isPresent() ? optional.get().toString() : "null");
+        }
+
+        @Override
+        public Optional<?> deserialize(JsonElement jsonElement, Type type, JsonDeserializationContext jsonDeserializationContext) throws JsonParseException {
+            Type actualType = ((ParameterizedType) type).getActualTypeArguments()[0];
+            return jsonElement.getAsString().equals("null") ? Optional.empty() : Optional.ofNullable(jsonToObject(jsonElement.getAsString(), actualType));
+        }
+    }
+
+    /**
+     * Deserialize json to a typed object according to class.
+     * @param jsonString - json string.
+     * @param aClass - class of the object.
+     * @param <T> - generic type object.
+     * @return new instance of the input class.
+     */
+    public static <T> T jsonToObject(String jsonString, Class<T> aClass) {
+        try {
+            return gson.fromJson(jsonString, aClass);
+        } catch (JsonSyntaxException e) {
+            throw new SyntaxException(e.getMessage());
+        }
+    }
+
+    /**
+     * Deserialize json to a typed object according to type.
+     * @param jsonString - json string.
+     * @param type - type of the object.
+     * @param <T> - generic type object.
+     * @return new instance of the input type.
+     */
+    public static <T> T jsonToObject(String jsonString, Type type) {
+        try {
+            return gson.fromJson(jsonString, type);
+        } catch (JsonSyntaxException e) {
+            throw new SyntaxException(e.getMessage());
+        }
+    }
+
+    /**
+     * Serialize object to string json.
+     * @param object - input object.
+     * @param <T> - generic type of object.
+     * @return string json.
+     */
+    public static <T> String objectToJson(T object) {
+        try {
+            return gson.toJson(object);
+        } catch (JsonSyntaxException e) {
+            throw new SyntaxException(e.getMessage());
+        }
+    }
+
+    @SafeVarargs
+    public static <R, E> R jsonToObject(String json, Type type, Tuple<Class<E>, FormatFunction<E>>... formatters) {
+        GsonBuilder gsonBuilder = new GsonBuilder();
+        for (Tuple<Class<E>, FormatFunction<E>> formatter : formatters) {
+            gsonBuilder.registerTypeAdapter(formatter.getItem1(), new BasicJsonDeserializer<>(formatter.getItem2()));
+        }
+        try {
+            Gson gson = gsonBuilder.create();
+            return gson.fromJson(json, type);
+        } catch (JsonSyntaxException e) {
+            throw new SyntaxException(e.getMessage());
+        } catch (RuntimeException e) {
+            throw new ParseException(e.getMessage());
+        }
+    }
+}

+ 25 - 0
connector-tools/src/main/java/cz/senslog/connector/tools/json/BasicJsonDeserializer.java

@@ -0,0 +1,25 @@
+// Copyright (c) 2026 UWB & LESP.
+// The UWB & LESP license this file to you under the BSD-3-Clause license.
+
+package cz.senslog.connector.tools.json;
+
+import com.google.gson.JsonDeserializationContext;
+import com.google.gson.JsonDeserializer;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonParseException;
+
+import java.lang.reflect.Type;
+
+public class BasicJsonDeserializer<T> implements JsonDeserializer<T> {
+
+    private final FormatFunction<T> formatter;
+
+    public BasicJsonDeserializer(FormatFunction<T> formatter) {
+        this.formatter = formatter;
+    }
+
+    @Override
+    public T deserialize(JsonElement jsonElement, Type type, JsonDeserializationContext jsonDeserializationContext) throws JsonParseException {
+        return formatter.apply(jsonElement.getAsString());
+    }
+}

+ 9 - 0
connector-tools/src/main/java/cz/senslog/connector/tools/json/FormatFunction.java

@@ -0,0 +1,9 @@
+// Copyright (c) 2026 UWB & LESP.
+// The UWB & LESP license this file to you under the BSD-3-Clause license.
+
+package cz.senslog.connector.tools.json;
+
+@FunctionalInterface
+public interface FormatFunction<T> {
+    T apply(String element);
+}

+ 34 - 0
connector-tools/src/main/java/cz/senslog/connector/tools/util/ClassUtils.java

@@ -0,0 +1,34 @@
+// Copyright (c) 2026 UWB & LESP.
+// The UWB & LESP license this file to you under the BSD-3-Clause license.
+
+package cz.senslog.connector.tools.util;
+
+import static java.lang.String.format;
+
+/**
+ * The class {@code ClassUtils} represents set of tools for classes.
+ *
+ * @author Lukas Cerny
+ * @version 1.0
+ * @since 1.0
+ */
+public class ClassUtils {
+
+    /**
+     * Provides type safe functionality of casting. Can be used only in case,
+     * if is cast a value which is direct type of the class (inheritance is not supported).
+     *
+     * @param value - value to be cast.
+     * @param castClass - class for casting.
+     * @param <T> - generic type of casting.
+     * @return casted value.
+     */
+    public static <T> T cast(Object value, Class<T> castClass) {
+        if (value == null) { return null; }
+        if (value.getClass().equals(castClass) ) {
+            return (T) value;
+        } else {
+            throw new ClassCastException(format("Value '%s' can not be cast to %s", value, castClass));
+        }
+    }
+}

+ 34 - 0
connector-tools/src/main/java/cz/senslog/connector/tools/util/Next.java

@@ -0,0 +1,34 @@
+// Copyright (c) 2026 UWB & LESP.
+// The UWB & LESP license this file to you under the BSD-3-Clause license.
+
+package cz.senslog.connector.tools.util;
+
+import java.util.function.Consumer;
+import java.util.function.Function;
+
+/**
+ * The interface {@code Next} provides right side of pipeline.
+ * Pipeline can be ended by calling {@code Next#end} or can be chained by calling {@code Next#next}.
+ *
+ * @param <T> - generic type of consuming data
+ *
+ * @author Lukas Cerny
+ * @version 1.0
+ * @since 1.0
+ */
+public interface Next<T> {
+
+    /**
+     * Ends pipeline and consumes received data.
+     * @param end - consumer of received data.
+     */
+    void end(Consumer<T> end);
+
+    /**
+     * Provides chain for the pipeline. Parameter is function received data and provides new data.
+     * @param next - function consumes data and provides new data.
+     * @param <R> - generic type of new data.
+     * @return - new instance of {@link Pipe}.
+     */
+    <R> Pipe<R> next(Function<? super T, ? extends R> next);
+}

+ 36 - 0
connector-tools/src/main/java/cz/senslog/connector/tools/util/NextImpl.java

@@ -0,0 +1,36 @@
+// Copyright (c) 2026 UWB & LESP.
+// The UWB & LESP license this file to you under the BSD-3-Clause license.
+
+package cz.senslog.connector.tools.util;
+
+import java.util.function.Consumer;
+import java.util.function.Function;
+
+/**
+ * The class {@code NextImpl} represents implementation of {@link Next}.
+ *
+ * @param <R> - generic type of data.
+ *
+ * @author Lukas Cerny
+ * @version 1.0
+ * @since 1.0
+ */
+class NextImpl<R> implements Next<R> {
+
+    /** Received data from pipeline */
+    private final R data;
+
+    NextImpl(R data) {
+        this.data = data;
+    }
+
+    @Override
+    public void end(Consumer<R> end) {
+        end.accept(data);
+    }
+
+    @Override
+    public <T> Pipe<T> next(Function<? super R, ? extends T> next) {
+        return new PipeImpl<>(next.apply(data));
+    }
+}

+ 23 - 0
connector-tools/src/main/java/cz/senslog/connector/tools/util/Pipe.java

@@ -0,0 +1,23 @@
+// Copyright (c) 2026 UWB & LESP.
+// The UWB & LESP license this file to you under the BSD-3-Clause license.
+
+package cz.senslog.connector.tools.util;
+
+import java.util.function.Function;
+
+/**
+ * The interface {@code Pipe} provides functionality of pipeline {@see Pipeline}.
+ * The method {@code Pipe#pipe} represents character '|'.
+ *
+ * @param <T> - generic type of input data.
+ */
+public interface Pipe<T> {
+
+    /**
+     * Converter represents '|' of idea of pipeline.
+     * @param mapper - converter for pipeline's flow.
+     * @param <R> - generic type of output data.
+     * @return new instance of {@link Next}.
+     */
+    <R> Next<R> pipe(Function<? super T, ? extends R> mapper);
+}

+ 30 - 0
connector-tools/src/main/java/cz/senslog/connector/tools/util/PipeImpl.java

@@ -0,0 +1,30 @@
+// Copyright (c) 2026 UWB & LESP.
+// The UWB & LESP license this file to you under the BSD-3-Clause license.
+
+package cz.senslog.connector.tools.util;
+
+import java.util.function.Function;
+
+/**
+ * The class {@code PipeImpl} represents implementation of {@link Pipe}.
+ *
+ * @param <T> - generic type of data.
+ *
+ * @author Lukas Cerny
+ * @version 1.0
+ * @since 1.0
+ */
+class PipeImpl<T> implements Pipe<T> {
+
+    /** Received data from pipeline */
+    private final T data;
+
+    PipeImpl(T data) {
+        this.data = data;
+    }
+
+    @Override
+    public <R> Next<R> pipe(Function<? super T, ? extends R> mapper) {
+        return new NextImpl<>(mapper.apply(data));
+    }
+}

+ 35 - 0
connector-tools/src/main/java/cz/senslog/connector/tools/util/Pipeline.java

@@ -0,0 +1,35 @@
+// Copyright (c) 2026 UWB & LESP.
+// The UWB & LESP license this file to you under the BSD-3-Clause license.
+
+package cz.senslog.connector.tools.util;
+
+import java.util.function.Supplier;
+
+/**
+ * The interface {@code Pipeline} provides generic functionality of pipeline.
+ * This interfaces creates start of a pipeline and provides {@link Pipe}.
+ * Idea of pipeline:
+ *  of | end
+ *  of | next | end
+ *  of | next | next | end
+ * Basic example:
+ *  Pipeline.of(<provide_data>).pipe(<convert_data>).end(<consume_data>);
+ *  Advanced example:
+ *  Pipeline.of(<provide_data>).pipe(<convert_data>).next(<consume_and_provide_new_data>).pipe(<convert_data>).end(<consume_data>);
+ *
+ * @author Lukas Cerny
+ * @version 1.0
+ * @since 1.0
+ */
+public interface Pipeline {
+
+    /**
+     * Start method of pipeline.
+     * @param start - supplier provides data.
+     * @param <T> - generic type of data.
+     * @return new instance of {@link Pipe}.
+     */
+    static <T> Pipe<T> of(Supplier<T> start) {
+        return new PipeImpl<>(start.get());
+    }
+}

+ 56 - 0
connector-tools/src/main/java/cz/senslog/connector/tools/util/StringUtils.java

@@ -0,0 +1,56 @@
+// Copyright (c) 2026 UWB & LESP.
+// The UWB & LESP license this file to you under the BSD-3-Clause license.
+
+package cz.senslog.connector.tools.util;
+
+/**
+ * The class {@code StringUtils} represents set of tools for strings.
+ *
+ * @author Lukas Cerny
+ * @version 1.0
+ * @since 1.0
+ */
+public final class StringUtils {
+
+    /**
+     * Test if input string is null or empty.
+     * Examples:
+     *          null -> true
+     *          ""   -> true
+     *          " "  -> false
+     *          "a"  -> false
+     *
+     * @param string - input string to test.
+     * @return boolean value.
+     */
+    public static boolean isEmpty(String string) {
+        return string == null || string.isEmpty();
+    }
+
+    /**
+     * Tests if input string is null, empty or contains white characters.
+     * Examples:
+     *          null   -> true
+     *          ""     -> true
+     *          " "    -> true
+     *          "    " -> true
+     *          "a"    -> false
+     *
+     * @param string - input string to test.
+     * @return boolean value.
+     */
+    public static boolean isBlank(String string) {
+        return string == null || string.replaceAll("\\s+","").isEmpty();
+    }
+
+
+    /**
+     *  Tests negative functionality of {@see StringUtils#isBlank}.
+     * @param string - input string to test.
+     * @return boolean value.
+     */
+    public static boolean isNotBlank(String string) {
+        return !isBlank(string);
+    }
+
+}

Some files were not shown because too many files changed in this diff