diff --git a/.code/docker-compose/docker-compose.yml b/.code/docker-compose/docker-compose.yml index 2d65b821..b158e6d5 100644 --- a/.code/docker-compose/docker-compose.yml +++ b/.code/docker-compose/docker-compose.yml @@ -28,8 +28,26 @@ services: # - MQTT_PORT=1883 # - MQTT_COMMAND_TOPIC_NAME=cmnd/rika2mqtt # - MQTT_TELEMETRY_REPORT_TOPIC_NAME=tele/rika2mqtt +# - PLUGINS=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar + influxdb: + container_name: influxdb + image: influxdb:2.7 + ports: + - 8086:8086 +# volumes: +# - $PWD/data:/var/lib/influxdb2 +# - $PWD/config:/etc/influxdb2 + environment: + - DOCKER_INFLUXDB_INIT_MODE=setup + - DOCKER_INFLUXDB_INIT_USERNAME=admin + - DOCKER_INFLUXDB_INIT_PASSWORD=adminadmin + - DOCKER_INFLUXDB_INIT_ORG=my-org + - DOCKER_INFLUXDB_INIT_RETENTION=1w + - DOCKER_INFLUXDB_INIT_BUCKET=rika2mqtt + - DOCKER_INFLUXDB_INIT_ADMIN_TOKEN=admin-token + volumes: data: name: "mqtt-broker-data" diff --git a/.docker/init.sh b/.docker/init.sh index b9471f54..8e68ce8e 100644 --- a/.docker/init.sh +++ b/.docker/init.sh @@ -23,9 +23,10 @@ # # Init script for rika2mqtt +export PLUGINS_DIR=/opt/rika2mqtt/plugins # Run it java --add-opens=java.base/java.net=ALL-UNNAMED \ --add-opens=java.base/java.io=ALL-UNNAMED \ --add-opens=java.base/java.nio=ALL-UNNAMED \ - -jar rika2mqtt.jar \ No newline at end of file + -jar rika2mqtt.jar diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e80671f8..6b9dae89 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -56,5 +56,10 @@ jobs: bridge/target mqtt/target rika-firenet/target + plugins-api/target + plugins-internal/target + rika2mqtt-example-plugin/target + rika2mqtt-example-plugin-using-config/target + rika2mqtt-flux-metrics-plugin/target retention-days: 1 if-no-files-found: error diff --git a/DEV.md b/DEV.md index 0c24dd2e..14953f91 100644 --- a/DEV.md +++ b/DEV.md @@ -20,6 +20,11 @@ To ensure the code style is applied, mvn will automatically format the files at You can if desired install the auto formatter in your IDE: https://github.com/google/google-java-format/blob/master/README.md#using-the-formatter +A few useful command lines to run before opening a PR (otherwise the CI will fail): + +`mvn com.spotify.fmt:fmt-maven-plugin:format` - Autofix java files so that they follow google code style +`mvn license:format` - Autofix missing headers in files + ## Local run There is a docker-compose.yml @@ -45,7 +50,7 @@ Then you can run the project and enjoy :) If you want to run the project in pure CLI there is a `Makefile` available. You can run: - +* `make codestyle` * `make jar` * `make test` * `make docker` diff --git a/Dockerfile b/Dockerfile index 89747cc3..3f6ac08a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -23,7 +23,7 @@ FROM eclipse-temurin:21.0.1_12-jre-jammy -RUN mkdir -p /opt/rika2mqtt +RUN mkdir -p /opt/rika2mqtt/plugins WORKDIR /opt/rika2mqtt COPY .docker/ . diff --git a/Makefile b/Makefile index 4ccbdaf9..5913de0a 100644 --- a/Makefile +++ b/Makefile @@ -10,5 +10,8 @@ test: ## execute tests docker: jar ## build docker image @./scripts/docker.sh +codestyle: ## format code, add missing headers, etc. + @./scripts/codestyle.sh + help: ## print this help - @awk 'BEGIN {FS = ":.*?## "} /^[a-zA-Z0-9_-]+:.*?## / {gsub("\\\\n",sprintf("\n%22c",""), $$2);printf "\033[36m%-20s\033[0m %s\n", $$1, $$2}' $(MAKEFILE_LIST) \ No newline at end of file + @awk 'BEGIN {FS = ":.*?## "} /^[a-zA-Z0-9_-]+:.*?## / {gsub("\\\\n",sprintf("\n%22c",""), $$2);printf "\033[36m%-20s\033[0m %s\n", $$1, $$2}' $(MAKEFILE_LIST) diff --git a/bridge/pom.xml b/bridge/pom.xml index 002a3593..fefc8d66 100644 --- a/bridge/pom.xml +++ b/bridge/pom.xml @@ -123,6 +123,12 @@ compile 1.1.0 + + dev.cookiecode + plugins-internal + 1.1.0 + compile + org.glassfish.expressly diff --git a/bridge/src/main/java/dev/cookiecode/rika2mqtt/bridge/Bridge.java b/bridge/src/main/java/dev/cookiecode/rika2mqtt/bridge/Bridge.java index b02da1e6..f5911afc 100644 --- a/bridge/src/main/java/dev/cookiecode/rika2mqtt/bridge/Bridge.java +++ b/bridge/src/main/java/dev/cookiecode/rika2mqtt/bridge/Bridge.java @@ -25,6 +25,9 @@ import com.google.common.annotations.VisibleForTesting; import com.google.gson.Gson; import dev.cookiecode.rika2mqtt.bridge.misc.EmailObfuscator; +import dev.cookiecode.rika2mqtt.plugins.internal.v1.Rika2MqttPluginService; +import dev.cookiecode.rika2mqtt.plugins.internal.v1.event.PolledStoveStatusEvent; +import dev.cookiecode.rika2mqtt.plugins.internal.v1.mapper.StoveStatusMapper; import dev.cookiecode.rika2mqtt.rika.firenet.RikaFirenetService; import dev.cookiecode.rika2mqtt.rika.firenet.exception.CouldNotAuthenticateToRikaFirenetException; import dev.cookiecode.rika2mqtt.rika.firenet.exception.InvalidStoveIdException; @@ -43,6 +46,7 @@ import lombok.RequiredArgsConstructor; import lombok.extern.flogger.Flogger; import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.event.EventListener; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; @@ -76,6 +80,11 @@ public class Bridge { private final EmailObfuscator emailObfuscator; private final Gson gson; + private final StoveStatusMapper stoveStatusMapper; + + private final Rika2MqttPluginService pluginManager; + + private final ApplicationEventPublisher applicationEventPublisher; private final List stoveIds = new ArrayList<>(); @@ -85,6 +94,7 @@ void init() { initStoves(rikaFirenetService.getStoves()); printStartupMessages(); + pluginManager.start(); publishToMqtt(); } @@ -123,7 +133,13 @@ void publishToMqtt() { try { status = rikaFirenetService.getStatus(stoveId); - mqttService.publish(gson.toJson(status)); + final var jsonStatus = gson.toJson(status); + mqttService.publish(jsonStatus); + + applicationEventPublisher.publishEvent( + PolledStoveStatusEvent.builder() + .stoveStatus(stoveStatusMapper.toApiStoveStatus(status)) + .build()); } catch (InvalidStoveIdException e) { // TODO: could occurs if a stove is added later (after deployment of this rika2mqtt // instance, might be valuable to perform a reload of stoves "periodically") -> should diff --git a/bridge/src/test/java/dev/cookiecode/rika2mqtt/bridge/AbstractBaseIntegrationTest.java b/bridge/src/test/java/dev/cookiecode/rika2mqtt/bridge/AbstractBaseIntegrationTest.java index f5df471c..36667bfb 100644 --- a/bridge/src/test/java/dev/cookiecode/rika2mqtt/bridge/AbstractBaseIntegrationTest.java +++ b/bridge/src/test/java/dev/cookiecode/rika2mqtt/bridge/AbstractBaseIntegrationTest.java @@ -69,13 +69,13 @@ public static void stopMqttBrokerTestContainer() { } /** - * @implNote The DynamicPropertyRegistry overides application-test.properties in the resource + * @implNote The DynamicPropertyRegistry overrides application-test.properties in the resource * folder, with value in the container static methods. */ @DynamicPropertySource static void overrideTestProperties(DynamicPropertyRegistry registry) { registry.add("mqtt.host", MQTT_SERVER::getHost); - registry.add("mqtt.port", MQTT_SERVER::getFirstMappedPort); // TODO: not safe IMHO + registry.add("mqtt.port", MQTT_SERVER::getFirstMappedPort); } @DynamicPropertySource @@ -269,7 +269,7 @@ public static void initStoveStatusMock(final StoveId stoveId) { "convectionFan2Area": 7, "frostProtectionActive": true, "frostProtectionTemperature": "4", - "temperatureOffset": "0", + "temperatureOffset": "-0.5", "RoomPowerRequest": 3, "debug0": 0, "debug1": 0, diff --git a/bridge/src/test/java/dev/cookiecode/rika2mqtt/bridge/BridgeTest.java b/bridge/src/test/java/dev/cookiecode/rika2mqtt/bridge/BridgeTest.java index ae764a26..99f0d744 100644 --- a/bridge/src/test/java/dev/cookiecode/rika2mqtt/bridge/BridgeTest.java +++ b/bridge/src/test/java/dev/cookiecode/rika2mqtt/bridge/BridgeTest.java @@ -36,6 +36,8 @@ import com.google.gson.Gson; import dev.cookiecode.rika2mqtt.bridge.misc.EmailObfuscator; +import dev.cookiecode.rika2mqtt.plugins.internal.v1.Rika2MqttPluginService; +import dev.cookiecode.rika2mqtt.plugins.internal.v1.mapper.StoveStatusMapper; import dev.cookiecode.rika2mqtt.rika.firenet.RikaFirenetService; import dev.cookiecode.rika2mqtt.rika.firenet.model.StoveId; import dev.cookiecode.rika2mqtt.rika.mqtt.MqttService; @@ -52,6 +54,7 @@ import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.boot.test.system.CapturedOutput; import org.springframework.boot.test.system.OutputCaptureExtension; +import org.springframework.context.ApplicationEventPublisher; /** * Test class @@ -66,6 +69,11 @@ class BridgeTest { @Mock MqttService mqttService; @Mock EmailObfuscator emailObfuscator; @Mock Gson gson; + @Mock StoveStatusMapper stoveStatusMapper; + + @Mock Rika2MqttPluginService pluginManager; + + @Mock ApplicationEventPublisher applicationEventPublisher; @InjectMocks @Spy Bridge bridge; @BeforeEach diff --git a/docs.json b/docs.json index 8fa833bb..527b8a4a 100644 --- a/docs.json +++ b/docs.json @@ -13,7 +13,8 @@ ["Getting Started", [ ["Overview", "/"], ["Getting Started", "/getting-started"], - ["Configuration", "/configuration"] + ["Configuration", "/configuration"], + ["Plugin development", "/write-a-plugin"] ]], ["Examples", [ ["Publish stove status to MQTT", "/example-telemetry-mqtt"], diff --git a/docs/write-a-plugin.mdx b/docs/write-a-plugin.mdx new file mode 100644 index 00000000..1efeb94c --- /dev/null +++ b/docs/write-a-plugin.mdx @@ -0,0 +1,44 @@ +# Write a plugin for RIKA2MQTT + +> API for plugins is currently under development and is marked `@Beta`. Be cautious that it can have breaking changes between future releases. + +RIKA2MQTT provide an API for plugin creators. + +A few modules in the project are demonstrating plugins: + +* rika2mqtt-example-plugin +* rika2mqtt-example-plugin-using-config + +And an official plugin `rika2mqtt-flux-metrics-plugin` that exports RIKA stove status to InfluxDB. + + +More documentation will follow in the future when the API is more stable and heavily tested. + + +## Make a plugin configurable by the end user +Make your plugin main class implement `ConfigurablePlugin` + +### Define plugin parameters +Implement: `List declarePluginConfigurationParameters()` +Parameters can be built using RequiredPluginConfigurationParameter.builder() or OptionalPluginConfigurationParameter.builder() that provide guided assistance. + +### Retrieve user defined parameters for the plugin + +```java +class YourPlugin extends Rika2MqttPlugin { + + // some code ... + + start(){ + + var influxPort = getPluginConfigurationParameter(INFLUXDB_PORT); + + } + + +} +``` + +> Note: If a parameter is named: INFLUXDB_PORT, an ENV property will have to be passed to the rika2mqtt docker named `PLUGIN_INFLUXDB_PORT=8086` (it must add an extra `PLUGIN_` this help to keep plugin properties separated.) + +Voila! diff --git a/plugins-api/README.md b/plugins-api/README.md new file mode 100644 index 00000000..27ec20c0 --- /dev/null +++ b/plugins-api/README.md @@ -0,0 +1,22 @@ +# Module plugins-api + +Expose API that can be extended by 3rd parties plugins to let developers connect and bring extra behaviours to rika2mqtt. + +## Architecture of this module + +### Responsibilities + +Loads external plugins at startup +Ensure provides some entry points extendable by plugins. + +### Opinionated choices + +**Question:** Why PF4J ? + +**Answer:** I did some projects with it and it is actuvely maintained. I really like PF4J is very easy to use, +no XML configuration and let the job done! + +### How does it work ? + +https://pf4j.org/ + diff --git a/plugins-api/pom.xml b/plugins-api/pom.xml new file mode 100644 index 00000000..32901483 --- /dev/null +++ b/plugins-api/pom.xml @@ -0,0 +1,75 @@ + + + + 4.0.0 + + dev.cookiecode + rika2mqtt-parent + 1.1.0 + + + plugins-api + + + ${basedir}/.. + ${java.sdk.version} + ${java.sdk.version} + ${source.encoding} + + ${project.sonar.root.projectKey}-${project.groupId}-${project.artifactId} + + + + + org.pf4j + pf4j + ${pf4j.version} + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + ${java.sdk.version} + ${java.sdk.version} + + + org.projectlombok + lombok + ${lombok.version} + + + true + + + + + diff --git a/plugins-api/src/main/java/dev/cookiecode/rika2mqtt/plugins/api/Beta.java b/plugins-api/src/main/java/dev/cookiecode/rika2mqtt/plugins/api/Beta.java new file mode 100644 index 00000000..3ed540b3 --- /dev/null +++ b/plugins-api/src/main/java/dev/cookiecode/rika2mqtt/plugins/api/Beta.java @@ -0,0 +1,31 @@ +/* + * The MIT License + * Copyright © 2022 Sebastien Vermeille + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package dev.cookiecode.rika2mqtt.plugins.api; + +/** + * Document beta features that might be removed/updated with breaking changes use it carefully being + * aware of this + * + * @author Sebastien Vermeille + */ +public @interface Beta {} diff --git a/plugins-api/src/main/java/dev/cookiecode/rika2mqtt/plugins/api/v1/PluginConfiguration.java b/plugins-api/src/main/java/dev/cookiecode/rika2mqtt/plugins/api/v1/PluginConfiguration.java new file mode 100644 index 00000000..fd706afc --- /dev/null +++ b/plugins-api/src/main/java/dev/cookiecode/rika2mqtt/plugins/api/v1/PluginConfiguration.java @@ -0,0 +1,52 @@ +/* + * The MIT License + * Copyright © 2022 Sebastien Vermeille + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package dev.cookiecode.rika2mqtt.plugins.api.v1; + +import static java.util.Optional.ofNullable; + +import java.util.Map; +import java.util.Optional; +import lombok.*; + +/** + * @author Sebastien Vermeille + */ +@Builder +@Getter +@ToString +@EqualsAndHashCode +public class PluginConfiguration { + + private Map parameters; // paramName, value + + public String getParameter(@NonNull String parameter) { + return getOptionalParameter(parameter) + .orElseThrow(); // TODO: if this happens it's at plugin side :/ the config should have been + // pre validated by rika2mqtt earlier should we keep this method or not ? + // provide more context to devs ? + } + + public Optional getOptionalParameter(@NonNull String parameter) { + return ofNullable(parameters.get(parameter)); + } +} diff --git a/plugins-api/src/main/java/dev/cookiecode/rika2mqtt/plugins/api/v1/Rika2MqttPlugin.java b/plugins-api/src/main/java/dev/cookiecode/rika2mqtt/plugins/api/v1/Rika2MqttPlugin.java new file mode 100644 index 00000000..23502c38 --- /dev/null +++ b/plugins-api/src/main/java/dev/cookiecode/rika2mqtt/plugins/api/v1/Rika2MqttPlugin.java @@ -0,0 +1,48 @@ +/* + * The MIT License + * Copyright © 2022 Sebastien Vermeille + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package dev.cookiecode.rika2mqtt.plugins.api.v1; + +import dev.cookiecode.rika2mqtt.plugins.api.Beta; +import lombok.Getter; +import lombok.NonNull; +import org.pf4j.Plugin; + +/** + * Base class for Rika2Mqtt plugins + * + * @author Sebastien Vermeille + */ +@Beta +@Getter +public abstract class Rika2MqttPlugin extends Plugin { + + private PluginConfiguration pluginConfiguration; + + public void preStart(@NonNull PluginConfiguration pluginConfiguration) { + this.pluginConfiguration = pluginConfiguration; + } + + public String getPluginConfigurationParameter(@NonNull String parameterName) { + return pluginConfiguration.getParameter(parameterName); + } +} diff --git a/plugins-api/src/main/java/dev/cookiecode/rika2mqtt/plugins/api/v1/StoveStatusExtension.java b/plugins-api/src/main/java/dev/cookiecode/rika2mqtt/plugins/api/v1/StoveStatusExtension.java new file mode 100644 index 00000000..5d389f2a --- /dev/null +++ b/plugins-api/src/main/java/dev/cookiecode/rika2mqtt/plugins/api/v1/StoveStatusExtension.java @@ -0,0 +1,42 @@ +/* + * The MIT License + * Copyright © 2022 Sebastien Vermeille + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package dev.cookiecode.rika2mqtt.plugins.api.v1; + +import dev.cookiecode.rika2mqtt.plugins.api.Beta; +import dev.cookiecode.rika2mqtt.plugins.api.v1.model.StoveStatus; +import org.pf4j.ExtensionPoint; + +/** + * @author Sebastien Vermeille + */ +@Beta +public interface StoveStatusExtension extends ExtensionPoint { + + /** + * RIKA stove status is regularly polled by Rika2Mqtt. Each time a scheduled poll succeed this + * hook will be invoked and forwarded to plugins. + * + * @param stoveStatus the status retrieved from rika-firenet + */ + void onPollStoveStatusSucceed(StoveStatus stoveStatus); +} diff --git a/plugins-api/src/main/java/dev/cookiecode/rika2mqtt/plugins/api/v1/annotations/ConfigurablePlugin.java b/plugins-api/src/main/java/dev/cookiecode/rika2mqtt/plugins/api/v1/annotations/ConfigurablePlugin.java new file mode 100644 index 00000000..7b0c3860 --- /dev/null +++ b/plugins-api/src/main/java/dev/cookiecode/rika2mqtt/plugins/api/v1/annotations/ConfigurablePlugin.java @@ -0,0 +1,51 @@ +/* + * The MIT License + * Copyright © 2022 Sebastien Vermeille + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package dev.cookiecode.rika2mqtt.plugins.api.v1.annotations; + +import dev.cookiecode.rika2mqtt.plugins.api.v1.model.plugins.OptionalPluginConfigurationParameter; +import dev.cookiecode.rika2mqtt.plugins.api.v1.model.plugins.PluginConfigurationParameter; +import dev.cookiecode.rika2mqtt.plugins.api.v1.model.plugins.RequiredPluginConfigurationParameter; +import java.util.List; + +/** + * Plugins that require external configuration should implement this interface. This way, they + * benefit from Rika2Mqtt configuration check at startup time. The bridge will verify that the + * required parameters are provided and then only start the plugin. + * + * @author Sebastien Vermeille + */ +public interface ConfigurablePlugin { + + /** + * Declare parameters supported by a plugin. This allows to define some mandatory parameters, + * specify their types and default values. At startup when loading plugins Rika2Mqtt will check + * for availability of required parameters and display log errors in case of failure. + * + *

These parameters can be build using fluent coding via the following builders: {@link + * RequiredPluginConfigurationParameter#builder()}, {@link + * OptionalPluginConfigurationParameter#builder()} + * + * @return a list of plugin configuration parameters + */ + List declarePluginConfigurationParameters(); +} diff --git a/plugins-api/src/main/java/dev/cookiecode/rika2mqtt/plugins/api/v1/annotations/Nullable.java b/plugins-api/src/main/java/dev/cookiecode/rika2mqtt/plugins/api/v1/annotations/Nullable.java new file mode 100644 index 00000000..f2481c60 --- /dev/null +++ b/plugins-api/src/main/java/dev/cookiecode/rika2mqtt/plugins/api/v1/annotations/Nullable.java @@ -0,0 +1,30 @@ +/* + * The MIT License + * Copyright © 2022 Sebastien Vermeille + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package dev.cookiecode.rika2mqtt.plugins.api.v1.annotations; + +/** + * Explicitly document that a parameter can be null + * + * @author Sebastien Vermeille + */ +public @interface Nullable {} diff --git a/plugins-api/src/main/java/dev/cookiecode/rika2mqtt/plugins/api/v1/exceptions/InvalidPluginConfigurationException.java b/plugins-api/src/main/java/dev/cookiecode/rika2mqtt/plugins/api/v1/exceptions/InvalidPluginConfigurationException.java new file mode 100644 index 00000000..67817874 --- /dev/null +++ b/plugins-api/src/main/java/dev/cookiecode/rika2mqtt/plugins/api/v1/exceptions/InvalidPluginConfigurationException.java @@ -0,0 +1,36 @@ +/* + * The MIT License + * Copyright © 2022 Sebastien Vermeille + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package dev.cookiecode.rika2mqtt.plugins.api.v1.exceptions; + +import dev.cookiecode.rika2mqtt.plugins.api.v1.annotations.ConfigurablePlugin; +import lombok.experimental.StandardException; + +/** + * Exception thrown by Rika2Mqtt when a plugin specify some configuration via {@link + * ConfigurablePlugin#declarePluginConfigurationParameters()} method and the provided configuration + * is not satisfying it. + * + * @author Sebastien Vermeille + */ +@StandardException +public class InvalidPluginConfigurationException extends PluginException {} diff --git a/plugins-api/src/main/java/dev/cookiecode/rika2mqtt/plugins/api/v1/exceptions/PluginException.java b/plugins-api/src/main/java/dev/cookiecode/rika2mqtt/plugins/api/v1/exceptions/PluginException.java new file mode 100644 index 00000000..64e54e07 --- /dev/null +++ b/plugins-api/src/main/java/dev/cookiecode/rika2mqtt/plugins/api/v1/exceptions/PluginException.java @@ -0,0 +1,33 @@ +/* + * The MIT License + * Copyright © 2022 Sebastien Vermeille + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package dev.cookiecode.rika2mqtt.plugins.api.v1.exceptions; + +import lombok.experimental.StandardException; + +/** + * Base exception regarding to Rika2Mqtt plugins + * + * @author Sebastien Vermeille + */ +@StandardException +public class PluginException extends Exception {} diff --git a/plugins-api/src/main/java/dev/cookiecode/rika2mqtt/plugins/api/v1/model/Controls.java b/plugins-api/src/main/java/dev/cookiecode/rika2mqtt/plugins/api/v1/model/Controls.java new file mode 100644 index 00000000..dfb738ef --- /dev/null +++ b/plugins-api/src/main/java/dev/cookiecode/rika2mqtt/plugins/api/v1/model/Controls.java @@ -0,0 +1,150 @@ +/* + * The MIT License + * Copyright © 2022 Sebastien Vermeille + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package dev.cookiecode.rika2mqtt.plugins.api.v1.model; + +import static lombok.AccessLevel.NONE; + +import dev.cookiecode.rika2mqtt.plugins.api.Beta; +import java.time.DayOfWeek; +import java.util.List; +import java.util.Map; +import lombok.Builder; +import lombok.Data; +import lombok.Getter; + +/** + * @author Sebastien Vermeille + */ +@Data +@Builder +@Beta +public class Controls { + + private long revision; + private boolean on; + private int operatingMode; + private int heatingPower; + private int targetTemperature; + private int bakeTemperature; + private boolean ecoModeEnabled; + + // region HeatingTime + // TODO: consider removing these properties handled in getHeatingTimes() + @Getter(NONE) + private TimeRange heatingTimeMon1; + + @Getter(NONE) + private TimeRange heatingTimeMon2; + + @Getter(NONE) + private TimeRange heatingTimeTue1; + + @Getter(NONE) + private TimeRange heatingTimeTue2; + + @Getter(NONE) + private TimeRange heatingTimeWed1; + + @Getter(NONE) + private TimeRange heatingTimeWed2; + + @Getter(NONE) + private TimeRange heatingTimeThu1; + + @Getter(NONE) + private TimeRange heatingTimeThu2; + + @Getter(NONE) + private TimeRange heatingTimeFri1; + + @Getter(NONE) + private TimeRange heatingTimeFri2; + + @Getter(NONE) + private TimeRange heatingTimeSat1; + + @Getter(NONE) + private TimeRange heatingTimeSat2; + + @Getter(NONE) + private TimeRange heatingTimeSun1; + + @Getter(NONE) + private TimeRange heatingTimeSun2; + + // endregion + private Map> heatingTimes; + + private Boolean heatingTimesActiveForComfort; + private Integer setBackTemperature; + + // region Fan + // TODO: consider removing these properties handled in getFans() + @Getter(NONE) + private boolean convectionFan1Active; + + @Getter(NONE) + private int convectionFan1Level; + + @Getter(NONE) + private int convectionFan1Area; + + @Getter(NONE) + private boolean convectionFan2Active; + + @Getter(NONE) + private int convectionFan2Level; + + @Getter(NONE) + private int convectionFan2Area; + + // endregion + private List fans; + + private boolean frostProtectionActive; + private int frostProtectionTemperature; + private double temperatureOffset; + + // @SerializedName("RoomPowerRequest") // for coherence (the rest of the api is using camelCase) + private int roomPowerRequest; + + // region Debug + // TODO: consider removing these properties handled in getDebugs() + @Getter(NONE) + private int debug0; + + @Getter(NONE) + private int debug1; + + @Getter(NONE) + private int debug2; + + @Getter(NONE) + private int debug3; + + @Getter(NONE) + private int debug4; + + // endregion + private List debugs; +} diff --git a/plugins-api/src/main/java/dev/cookiecode/rika2mqtt/plugins/api/v1/model/ConvectionFan.java b/plugins-api/src/main/java/dev/cookiecode/rika2mqtt/plugins/api/v1/model/ConvectionFan.java new file mode 100644 index 00000000..197e9db1 --- /dev/null +++ b/plugins-api/src/main/java/dev/cookiecode/rika2mqtt/plugins/api/v1/model/ConvectionFan.java @@ -0,0 +1,39 @@ +/* + * The MIT License + * Copyright © 2022 Sebastien Vermeille + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package dev.cookiecode.rika2mqtt.plugins.api.v1.model; + +import dev.cookiecode.rika2mqtt.plugins.api.Beta; +import lombok.*; + +/** + * @author Sebastien Vermeille + */ +@Data +@Builder +@Beta +public class ConvectionFan { + private int identifier; + private boolean active; + private int level; + private int area; +} diff --git a/plugins-api/src/main/java/dev/cookiecode/rika2mqtt/plugins/api/v1/model/ParameterDebug.java b/plugins-api/src/main/java/dev/cookiecode/rika2mqtt/plugins/api/v1/model/ParameterDebug.java new file mode 100644 index 00000000..05cc7017 --- /dev/null +++ b/plugins-api/src/main/java/dev/cookiecode/rika2mqtt/plugins/api/v1/model/ParameterDebug.java @@ -0,0 +1,39 @@ +/* + * The MIT License + * Copyright © 2022 Sebastien Vermeille + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package dev.cookiecode.rika2mqtt.plugins.api.v1.model; + +import dev.cookiecode.rika2mqtt.plugins.api.Beta; +import lombok.Builder; +import lombok.Data; + +/** + * @author Sebastien Vermeille + */ +@Data +@Builder +@Beta +public class ParameterDebug { + + private int number; + private int value; +} diff --git a/plugins-api/src/main/java/dev/cookiecode/rika2mqtt/plugins/api/v1/model/ParameterErrorCount.java b/plugins-api/src/main/java/dev/cookiecode/rika2mqtt/plugins/api/v1/model/ParameterErrorCount.java new file mode 100644 index 00000000..4d8d812d --- /dev/null +++ b/plugins-api/src/main/java/dev/cookiecode/rika2mqtt/plugins/api/v1/model/ParameterErrorCount.java @@ -0,0 +1,39 @@ +/* + * The MIT License + * Copyright © 2022 Sebastien Vermeille + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package dev.cookiecode.rika2mqtt.plugins.api.v1.model; + +import dev.cookiecode.rika2mqtt.plugins.api.Beta; +import lombok.Builder; +import lombok.Data; + +/** + * @author Sebastien Vermeille + */ +@Data +@Builder +@Beta +public class ParameterErrorCount { + + private int number; + private int value; +} diff --git a/plugins-api/src/main/java/dev/cookiecode/rika2mqtt/plugins/api/v1/model/Sensors.java b/plugins-api/src/main/java/dev/cookiecode/rika2mqtt/plugins/api/v1/model/Sensors.java new file mode 100644 index 00000000..ec1ee1d1 --- /dev/null +++ b/plugins-api/src/main/java/dev/cookiecode/rika2mqtt/plugins/api/v1/model/Sensors.java @@ -0,0 +1,221 @@ +/* + * The MIT License + * Copyright © 2022 Sebastien Vermeille + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package dev.cookiecode.rika2mqtt.plugins.api.v1.model; + +import static lombok.AccessLevel.NONE; + +import dev.cookiecode.rika2mqtt.plugins.api.Beta; +import java.util.List; +import lombok.Builder; +import lombok.Data; +import lombok.Getter; + +/** + * @author Sebastien Vermeille + */ +@Data +@Builder +@Beta +public class Sensors { + + private Double inputRoomTemperature; + private Integer inputFlameTemperature; + private Integer inputBakeTemperature; + private Integer statusError; + private Integer statusSubError; + private Integer statusWarning; + private Integer statusService; + private Integer outputDischargeMotor; + private Integer outputDischargeCurrent; + private Integer outputIdFan; + private Integer outputIdFanTarget; + private Integer outputInsertionMotor; + private Integer outputInsertionCurrent; + private Integer outputAirFlaps; + private Integer outputAirFlapsTargetPosition; + private Boolean outputBurnBackFlapMagnet; + private Boolean outputGridMotor; + private Boolean outputIgnition; + private Boolean inputUpperTemperatureLimiter; + private Boolean inputPressureSwitch; + private Integer inputPressureSensor; + private Boolean inputGridContact; + private Boolean inputDoor; + private Boolean inputCover; + private Boolean inputExternalRequest; + private Boolean inputBurnBackFlapSwitch; + private Boolean inputFlueGasFlapSwitch; + private Double inputBoardTemperature; + private Integer inputCurrentStage; + + // @SerializedName("inputTargetStagePID") // for coherence (the rest of the api is using + // camelCase) + private Integer inputTargetStagePid; + + // @SerializedName("inputCurrentStagePID") // for coherence (the rest of the api is using + // camelCase) + private Integer inputCurrentStagePid; + + private Integer statusMainState; + private Integer statusSubState; + private Integer statusWifiStrength; + private Boolean parameterEcoModePossible; + private Integer parameterFabricationNumber; + private Integer parameterStoveTypeNumber; + private Integer parameterLanguageNumber; + private Integer parameterVersionMainBoard; + + // @SerializedName("parameterVersionTFT") // for coherence (the rest of the api is using + // camelCase) + private Integer parameterVersionTft; + + // @SerializedName("parameterVersionWiFi") // for coherence (the rest of the api use Wifi not + // WiFi) + private Integer parameterVersionWifi; + + private Integer parameterVersionMainBoardBootLoader; + + // @SerializedName("parameterVersionTFTBootLoader") + // for coherence (the rest of the api is using camelCase) + private Integer parameterVersionTftBootLoader; + + // @SerializedName("parameterVersionWiFiBootLoader") + // for coherence (the rest of the api is using camelCase) + private Integer parameterVersionWifiBootLoader; + + private Integer parameterVersionMainBoardSub; + + // @SerializedName("parameterVersionTFTSub") + // for coherence (the rest of the api is using camelCase) + private Integer parameterVersionTftSub; + + // @SerializedName("parameterVersionWiFiSub") + // for coherence (the rest of the api use Wifi not WiFi) + private Integer parameterVersionWifiSub; + + private Integer parameterRuntimePellets; + private Integer parameterRuntimeLogs; + private Integer parameterFeedRateTotal; + private Integer parameterFeedRateService; + private Integer parameterServiceCountdownKg; + private Integer parameterServiceCountdownTime; + private Integer parameterIgnitionCount; + private Integer parameterOnOffCycleCount; + private Integer parameterFlameSensorOffset; + private Integer parameterPressureSensorOffset; + + // region HeatingTime + // TODO: consider removing these properties handled in getParametersErrorCount() + @Getter(NONE) + private Integer parameterErrorCount0; + + @Getter(NONE) + private Integer parameterErrorCount1; + + @Getter(NONE) + private Integer parameterErrorCount2; + + @Getter(NONE) + private Integer parameterErrorCount3; + + @Getter(NONE) + private Integer parameterErrorCount4; + + @Getter(NONE) + private Integer parameterErrorCount5; + + @Getter(NONE) + private Integer parameterErrorCount6; + + @Getter(NONE) + private Integer parameterErrorCount7; + + @Getter(NONE) + private Integer parameterErrorCount8; + + @Getter(NONE) + private Integer parameterErrorCount9; + + @Getter(NONE) + private Integer parameterErrorCount10; + + @Getter(NONE) + private Integer parameterErrorCount11; + + @Getter(NONE) + private Integer parameterErrorCount12; + + @Getter(NONE) + private Integer parameterErrorCount13; + + @Getter(NONE) + private Integer parameterErrorCount14; + + @Getter(NONE) + private Integer parameterErrorCount15; + + @Getter(NONE) + private Integer parameterErrorCount16; + + @Getter(NONE) + private Integer parameterErrorCount17; + + @Getter(NONE) + private Integer parameterErrorCount18; + + @Getter(NONE) + private Integer parameterErrorCount19; + + // endregion + private List parametersErrorCount; + + private Boolean statusHeatingTimesNotProgrammed; + private Boolean statusFrostStarted; + private Integer parameterSpiralMotorsTuning; + + // @SerializedName("parameterIDFanTuning") // for coherence (the rest of the api is using + // camelCase) + private Integer parameterIdFanTuning; + + private Integer parameterCleanIntervalBig; + private Integer parameterKgTillCleaning; + + // region ParameterDebug + @Getter(NONE) + private Integer parameterDebug0; + + @Getter(NONE) + private Integer parameterDebug1; + + @Getter(NONE) + private Integer parameterDebug2; + + @Getter(NONE) + private Integer parameterDebug3; + + @Getter(NONE) + private Integer parameterDebug4; + + // endregion + private List parametersDebug; +} diff --git a/plugins-api/src/main/java/dev/cookiecode/rika2mqtt/plugins/api/v1/model/StoveId.java b/plugins-api/src/main/java/dev/cookiecode/rika2mqtt/plugins/api/v1/model/StoveId.java new file mode 100644 index 00000000..b6f0392b --- /dev/null +++ b/plugins-api/src/main/java/dev/cookiecode/rika2mqtt/plugins/api/v1/model/StoveId.java @@ -0,0 +1,36 @@ +/* + * The MIT License + * Copyright © 2022 Sebastien Vermeille + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package dev.cookiecode.rika2mqtt.plugins.api.v1.model; + +import dev.cookiecode.rika2mqtt.plugins.api.Beta; + +/** + * @author Sebastien Vermeille + */ +@Beta +public record StoveId(Long id) { + + public static StoveId of(Long id) { + return new StoveId(id); + } +} diff --git a/plugins-api/src/main/java/dev/cookiecode/rika2mqtt/plugins/api/v1/model/StoveStatus.java b/plugins-api/src/main/java/dev/cookiecode/rika2mqtt/plugins/api/v1/model/StoveStatus.java new file mode 100644 index 00000000..ff6a6c4b --- /dev/null +++ b/plugins-api/src/main/java/dev/cookiecode/rika2mqtt/plugins/api/v1/model/StoveStatus.java @@ -0,0 +1,44 @@ +/* + * The MIT License + * Copyright © 2022 Sebastien Vermeille + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package dev.cookiecode.rika2mqtt.plugins.api.v1.model; + +import dev.cookiecode.rika2mqtt.plugins.api.Beta; +import lombok.Data; + +/** + * @author Sebastien Vermeille + */ +@Data +@Beta +public class StoveStatus { + + private String name; + private StoveId stoveId; + + private Long lastSeenMinutes; + private Long lastConfirmedRevision; + private String oem; + private String stoveType; + private Sensors sensors; + private Controls controls; +} diff --git a/plugins-api/src/main/java/dev/cookiecode/rika2mqtt/plugins/api/v1/model/TimeDefinition.java b/plugins-api/src/main/java/dev/cookiecode/rika2mqtt/plugins/api/v1/model/TimeDefinition.java new file mode 100644 index 00000000..e3b6e575 --- /dev/null +++ b/plugins-api/src/main/java/dev/cookiecode/rika2mqtt/plugins/api/v1/model/TimeDefinition.java @@ -0,0 +1,54 @@ +/* + * The MIT License + * Copyright © 2022 Sebastien Vermeille + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package dev.cookiecode.rika2mqtt.plugins.api.v1.model; + +import dev.cookiecode.rika2mqtt.plugins.api.Beta; +import lombok.Builder; +import lombok.EqualsAndHashCode; +import lombok.Getter; + +/** + * Class to represent time schedules such as : 10:00 - 12:30 + * + * @author Sebastien Vermeille + */ +@Builder +@Getter +@EqualsAndHashCode +@Beta +public class TimeDefinition { + private final int hours; + private final int minutes; + + private static final Double ONE_MINUTE_DURATION_IN_SECONDS = 60.0; + + @Override + public String toString() { + return String.format("%s:%02d", hours, minutes); + } + + /** Helper method to return 9.5 given 9h30 */ + public Double asDecimal() { + return this.getHours() + (this.getMinutes() / ONE_MINUTE_DURATION_IN_SECONDS); + } +} diff --git a/plugins-api/src/main/java/dev/cookiecode/rika2mqtt/plugins/api/v1/model/TimeRange.java b/plugins-api/src/main/java/dev/cookiecode/rika2mqtt/plugins/api/v1/model/TimeRange.java new file mode 100644 index 00000000..eb5f46b8 --- /dev/null +++ b/plugins-api/src/main/java/dev/cookiecode/rika2mqtt/plugins/api/v1/model/TimeRange.java @@ -0,0 +1,45 @@ +/* + * The MIT License + * Copyright © 2022 Sebastien Vermeille + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package dev.cookiecode.rika2mqtt.plugins.api.v1.model; + +import dev.cookiecode.rika2mqtt.plugins.api.Beta; +import lombok.Builder; +import lombok.EqualsAndHashCode; +import lombok.Getter; + +/** + * @author Sebastien Vermeille + */ +@Builder +@Getter +@EqualsAndHashCode +@Beta +public class TimeRange { + private final TimeDefinition from; + private final TimeDefinition to; + + @Override + public String toString() { + return String.format("%s - %s", from.toString(), to.toString()); + } +} diff --git a/plugins-api/src/main/java/dev/cookiecode/rika2mqtt/plugins/api/v1/model/plugins/OptionalPluginConfigurationParameter.java b/plugins-api/src/main/java/dev/cookiecode/rika2mqtt/plugins/api/v1/model/plugins/OptionalPluginConfigurationParameter.java new file mode 100644 index 00000000..90cd063d --- /dev/null +++ b/plugins-api/src/main/java/dev/cookiecode/rika2mqtt/plugins/api/v1/model/plugins/OptionalPluginConfigurationParameter.java @@ -0,0 +1,142 @@ +/* + * The MIT License + * Copyright © 2022 Sebastien Vermeille + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package dev.cookiecode.rika2mqtt.plugins.api.v1.model.plugins; + +import java.util.Optional; +import javax.annotation.processing.Generated; +import lombok.Getter; + +/** + * @author Sebastien Vermeille + */ +@Generated( + value = "Step Builder generator Idea Plugin", + date = "2023-12-31T09:40:55+0100", + comments = "parsely adapted by hands to handle optional props") +@Getter +public class OptionalPluginConfigurationParameter { + private final String parameterName; + private final String description; + private final Class valueType; + + private final String example; + private final Object defaultValue; + + private OptionalPluginConfigurationParameter(Builder builder) { + parameterName = builder.parameterName; + description = builder.description; + valueType = builder.valueType; + example = builder.example; + defaultValue = builder.defaultValue; + } + + public static IParameterName builder() { + return new Builder(); + } + + public interface IBuild { + IBuild withDefaultValue(Object val); + + IBuild withExample(String val); + + PluginConfigurationParameter build(); + } + + public interface IDefaultValue { + IBuild withDefaultValue(Object val); + } + + public interface IExample { + IBuild withExample(String val); + } + + public interface IValueType { + IBuild withValueType(Class val); + } + + public interface IDescription { + IValueType withDescription(String val); + } + + public interface IParameterName { + IDescription withParameterName(String val); + } + + public static final class Builder + implements IDefaultValue, IExample, IValueType, IDescription, IParameterName, IBuild { + private Object defaultValue; + private String example; + private Class valueType; + private String description; + private String parameterName; + + private Builder() {} + + @Override + public IBuild withDefaultValue(Object val) { + defaultValue = val; + return this; + } + + @Override + public IBuild withExample(String val) { + example = val; + return this; + } + + @Override + public IBuild withValueType(Class val) { + valueType = val; + return this; + } + + @Override + public IValueType withDescription(String val) { + description = val; + return this; + } + + @Override + public IDescription withParameterName(String val) { + parameterName = val; + return this; + } + + public PluginConfigurationParameter build() { + return asPluginConfigurationParameter(new OptionalPluginConfigurationParameter(this)); + } + } + + public Optional getDefaultValue() { + return Optional.ofNullable(defaultValue); + } + + public Optional getExample() { + return Optional.ofNullable(example); + } + + static PluginConfigurationParameter asPluginConfigurationParameter( + OptionalPluginConfigurationParameter optionalPluginConfigurationParameter) { + return new PluginConfigurationParameter(optionalPluginConfigurationParameter); + } +} diff --git a/plugins-api/src/main/java/dev/cookiecode/rika2mqtt/plugins/api/v1/model/plugins/PluginConfigurationParameter.java b/plugins-api/src/main/java/dev/cookiecode/rika2mqtt/plugins/api/v1/model/plugins/PluginConfigurationParameter.java new file mode 100644 index 00000000..8a4d3b04 --- /dev/null +++ b/plugins-api/src/main/java/dev/cookiecode/rika2mqtt/plugins/api/v1/model/plugins/PluginConfigurationParameter.java @@ -0,0 +1,103 @@ +/* + * The MIT License + * Copyright © 2022 Sebastien Vermeille + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package dev.cookiecode.rika2mqtt.plugins.api.v1.model.plugins; + +import dev.cookiecode.rika2mqtt.plugins.api.Beta; +import java.util.Optional; +import javax.annotation.processing.Generated; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NonNull; +import lombok.ToString; + +/** + * @author Sebastien Vermeille + */ +@Generated( + value = "Step Builder generator Idea Plugin", + date = "2023-12-31T09:40:55+0100", + comments = "parsely adapted by hands to handle optional props") +@Getter +@ToString +@EqualsAndHashCode +@Beta +public class PluginConfigurationParameter { + + private final String parameterName; + private final String description; + private final Class valueType; + private final String example; + private final boolean required; + private final Object defaultValue; + + protected PluginConfigurationParameter( + @NonNull OptionalPluginConfigurationParameter optionalParameter) { + required = false; + parameterName = optionalParameter.getParameterName(); + description = optionalParameter.getDescription(); + valueType = optionalParameter.getValueType(); + example = optionalParameter.getExample().orElse(null); + defaultValue = optionalParameter.getDefaultValue().orElse(null); + + checkCoherence(); + } + + public PluginConfigurationParameter( + @NonNull RequiredPluginConfigurationParameter requiredParameter) { + required = true; + parameterName = requiredParameter.getParameterName(); + description = requiredParameter.getDescription(); + valueType = requiredParameter.getValueType(); + example = requiredParameter.getExample().orElse(null); + defaultValue = null; + } + + private void checkCoherence() { + if (this.required && this.defaultValue != null) { + throw new IllegalArgumentException( + String.format( + """ + Dear developer, please take position. + Either '%s' is optional and have a default value, + either it is required and the user must provide a value for it. + """, + this.parameterName)); + } + } + + public Optional getDefaultValue() { + return Optional.ofNullable(defaultValue); + } + + public Optional getExample() { + return Optional.ofNullable(example); + } + + public boolean isOptional() { + return !isRequired(); + } + + public static RequiredPluginConfigurationParameter.Builder newRequiredParameter() { + return new RequiredPluginConfigurationParameter.Builder(); + } +} diff --git a/plugins-api/src/main/java/dev/cookiecode/rika2mqtt/plugins/api/v1/model/plugins/PluginConfigurationParameterBuilder.java b/plugins-api/src/main/java/dev/cookiecode/rika2mqtt/plugins/api/v1/model/plugins/PluginConfigurationParameterBuilder.java new file mode 100644 index 00000000..fed5b5b6 --- /dev/null +++ b/plugins-api/src/main/java/dev/cookiecode/rika2mqtt/plugins/api/v1/model/plugins/PluginConfigurationParameterBuilder.java @@ -0,0 +1,77 @@ +/* + * The MIT License + * Copyright © 2022 Sebastien Vermeille + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package dev.cookiecode.rika2mqtt.plugins.api.v1.model.plugins; + +import dev.cookiecode.rika2mqtt.plugins.api.Beta; +import javax.annotation.processing.Generated; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.ToString; + +/** + * @author Sebastien Vermeille + */ +@Generated( + value = "Step Builder generator Idea Plugin", + date = "2023-12-31T09:40:55+0100", + comments = "parsely adapted by hands to handle optional props") +@Getter +@ToString +@EqualsAndHashCode +@Beta +public class PluginConfigurationParameterBuilder { + + private final boolean required; + + PluginConfigurationParameterBuilder(Builder builder) { + required = builder.required; + } + + public static IRequired builder() { + return new Builder(); + } + + public interface IBuild { + PluginConfigurationParameterBuilder build(); + } + + public interface IRequired { + IBuild withRequired(boolean val); + } + + public static final class Builder implements IRequired, IBuild { + private boolean required; + + private Builder() {} + + @Override + public IBuild withRequired(boolean val) { + required = val; + return this; + } + + public PluginConfigurationParameterBuilder build() { + return new PluginConfigurationParameterBuilder(this); + } + } +} diff --git a/plugins-api/src/main/java/dev/cookiecode/rika2mqtt/plugins/api/v1/model/plugins/RequiredPluginConfigurationParameter.java b/plugins-api/src/main/java/dev/cookiecode/rika2mqtt/plugins/api/v1/model/plugins/RequiredPluginConfigurationParameter.java new file mode 100644 index 00000000..7af8a4b7 --- /dev/null +++ b/plugins-api/src/main/java/dev/cookiecode/rika2mqtt/plugins/api/v1/model/plugins/RequiredPluginConfigurationParameter.java @@ -0,0 +1,122 @@ +/* + * The MIT License + * Copyright © 2022 Sebastien Vermeille + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package dev.cookiecode.rika2mqtt.plugins.api.v1.model.plugins; + +import java.util.Optional; +import javax.annotation.processing.Generated; +import lombok.Getter; + +/** + * @author Sebastien Vermeille + */ +@Generated( + value = "Step Builder generator Idea Plugin", + date = "2023-12-31T09:40:55+0100", + comments = "parsely adapted by hands to handle optional props") +@Getter +public class RequiredPluginConfigurationParameter { + private final String parameterName; + private final String description; + private final Class valueType; + private String example; + + private RequiredPluginConfigurationParameter(Builder builder) { + parameterName = builder.parameterName; + description = builder.description; + valueType = builder.valueType; + example = builder.example; + } + + public static IParameterName builder() { + return new Builder(); + } + + public interface IBuild { + IBuild withExample(String val); + + PluginConfigurationParameter build(); + } + + public interface IExample { + IBuild withExample(String val); + } + + public interface IValueType { + IBuild withValueType(Class val); + } + + public interface IDescription { + IValueType withDescription(String val); + } + + public interface IParameterName { + IDescription withParameterName(String val); + } + + public static final class Builder + implements IExample, IValueType, IDescription, IParameterName, IBuild { + private String example; + private Class valueType; + private String description; + private String parameterName; + + Builder() {} + + @Override + public IBuild withExample(String val) { + example = val; + return this; + } + + @Override + public IBuild withValueType(Class val) { + valueType = val; + return this; + } + + @Override + public IValueType withDescription(String val) { + description = val; + return this; + } + + @Override + public IDescription withParameterName(String val) { + parameterName = val; + return this; + } + + public PluginConfigurationParameter build() { + return asPluginConfigurationParameter(new RequiredPluginConfigurationParameter(this)); + } + } + + public Optional getExample() { + return Optional.ofNullable(example); + } + + public static PluginConfigurationParameter asPluginConfigurationParameter( + RequiredPluginConfigurationParameter requiredPluginConfigurationParameter) { + return new PluginConfigurationParameter(requiredPluginConfigurationParameter); + } +} diff --git a/plugins-api/src/test/java/dev/cookiecode/rika2mqtt/plugins/api/v1/DummyPlugin.java b/plugins-api/src/test/java/dev/cookiecode/rika2mqtt/plugins/api/v1/DummyPlugin.java new file mode 100644 index 00000000..a9490cef --- /dev/null +++ b/plugins-api/src/test/java/dev/cookiecode/rika2mqtt/plugins/api/v1/DummyPlugin.java @@ -0,0 +1,31 @@ +/* + * The MIT License + * Copyright © 2022 Sebastien Vermeille + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package dev.cookiecode.rika2mqtt.plugins.api.v1; + +/** + * Test class + * + * @author Sebastien Vermeille + *

This is a dummy implementation of abstract Rika2MqttPlugin which ease testing + */ +public class DummyPlugin extends Rika2MqttPlugin {} diff --git a/plugins-api/src/test/java/dev/cookiecode/rika2mqtt/plugins/api/v1/PluginConfigurationTest.java b/plugins-api/src/test/java/dev/cookiecode/rika2mqtt/plugins/api/v1/PluginConfigurationTest.java new file mode 100644 index 00000000..f6f37cb9 --- /dev/null +++ b/plugins-api/src/test/java/dev/cookiecode/rika2mqtt/plugins/api/v1/PluginConfigurationTest.java @@ -0,0 +1,125 @@ +/* + * The MIT License + * Copyright © 2022 Sebastien Vermeille + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package dev.cookiecode.rika2mqtt.plugins.api.v1; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.Assert.assertThrows; +import static org.mockito.Mockito.*; + +import java.util.HashMap; +import java.util.Map; +import java.util.NoSuchElementException; +import java.util.Optional; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Spy; +import org.mockito.junit.jupiter.MockitoExtension; + +/** + * Test class + * + * @author Sebastien Vermeille + */ +@ExtendWith(MockitoExtension.class) +class PluginConfigurationTest { + + @InjectMocks @Spy private PluginConfiguration pluginConfiguration; + + @Test + void getParameterShouldInvokeGetOptionalParameterGivenThatThisIsMoreErrorSafe() { + // GIVEN + final var parameterName = "something"; + doReturn(Optional.of("some value")).when(pluginConfiguration).getOptionalParameter(anyString()); + + // WHEN + pluginConfiguration.getParameter(parameterName); + + // THEN + verify(pluginConfiguration, times(1)).getOptionalParameter(parameterName); + } + + @Test + void getParameterShouldThrowAnExceptionGivenTheOptionalIsEmpty() { + // GIVEN + final var parameterName = "something"; + doReturn(Optional.empty()).when(pluginConfiguration).getOptionalParameter(anyString()); + + // THEN + assertThrows( + NoSuchElementException.class, + () -> { + // WHEN + pluginConfiguration.getParameter(parameterName); + }); + } + + @Test + void getOptionalParameterShouldReturnAnEmptyOptionalGivenTheRequestedParameterIsNotProvided() { + // GIVEN + final var parameterName = "somethingThatDoNotExists"; + final var emptyPluginConfiguration = + PluginConfiguration.builder().parameters(new HashMap<>()).build(); + + // WHEN + final var result = emptyPluginConfiguration.getOptionalParameter(parameterName); + + // THEN + assertThat(result).isEmpty(); + } + + @Test + void getOptionalParameterShouldReturnAValueGivenTheRequestedParameterIsProvidedWithinTheConfig() { + // GIVEN + final var parameterName = "username"; + final var parameterValue = "root"; + final var filledPluginConfiguration = + PluginConfiguration.builder().parameters(Map.of(parameterName, parameterValue)).build(); + + // WHEN + final var result = filledPluginConfiguration.getOptionalParameter(parameterName); + + // THEN + assertThat(result).isPresent().isEqualTo(Optional.of(parameterValue)); + } + + @Test + void getOptionalParameterShouldThrowAnNullPointerExceptionGivenNoParameterIsPassed() { + assertThrows( + NullPointerException.class, + () -> { + // WHEN + pluginConfiguration.getOptionalParameter(null); + }); + } + + @Test + void getParameterShouldThrowAnNullPointerExceptionGivenNoParameterIsPassed() { + assertThrows( + NullPointerException.class, + () -> { + // WHEN + pluginConfiguration.getParameter(null); + }); + } +} diff --git a/plugins-api/src/test/java/dev/cookiecode/rika2mqtt/plugins/api/v1/Rika2MqttPluginTest.java b/plugins-api/src/test/java/dev/cookiecode/rika2mqtt/plugins/api/v1/Rika2MqttPluginTest.java new file mode 100644 index 00000000..92d2d9ca --- /dev/null +++ b/plugins-api/src/test/java/dev/cookiecode/rika2mqtt/plugins/api/v1/Rika2MqttPluginTest.java @@ -0,0 +1,70 @@ +/* + * The MIT License + * Copyright © 2022 Sebastien Vermeille + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package dev.cookiecode.rika2mqtt.plugins.api.v1; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.*; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; + +/** + * Test class + * + * @author Sebastien Vermeille + */ +@ExtendWith(MockitoExtension.class) +class Rika2MqttPluginTest { + + @Test + void preStartShouldInitPluginConfigurationAsItIsTheWholePurposeOfIt() { + // GIVEN + final var plugin = new DummyPlugin(); + final var pluginConfiguration = mock(PluginConfiguration.class); + + // WHEN + plugin.preStart(pluginConfiguration); + + // THEN + assertThat(plugin.getPluginConfiguration()).isEqualTo(pluginConfiguration); + } + + @Test + void + getPluginConfigurationParameterShouldInvokePluginConfigurationGetParameterMethodGivenItsAConvenienceWrapper() { + // GIVEN + final var plugin = new DummyPlugin(); + final var pluginConfiguration = mock(PluginConfiguration.class); + + plugin.preStart(pluginConfiguration); + + final var parameterName = "something"; + + // WHEN + plugin.getPluginConfigurationParameter(parameterName); + + // THEN + verify(pluginConfiguration, times(1)).getParameter(parameterName); + } +} diff --git a/plugins-api/src/test/java/dev/cookiecode/rika2mqtt/plugins/api/v1/model/StoveIdTest.java b/plugins-api/src/test/java/dev/cookiecode/rika2mqtt/plugins/api/v1/model/StoveIdTest.java new file mode 100644 index 00000000..a4656d47 --- /dev/null +++ b/plugins-api/src/test/java/dev/cookiecode/rika2mqtt/plugins/api/v1/model/StoveIdTest.java @@ -0,0 +1,47 @@ +/* + * The MIT License + * Copyright © 2022 Sebastien Vermeille + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package dev.cookiecode.rika2mqtt.plugins.api.v1.model; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.Test; + +/** + * Test class + * + * @author Sebastien Vermeille + */ +class StoveIdTest { + + @Test + void ofShouldBuildAStoveIdAsExpected() { + // GIVEN + final var stoveIdValue = 12321321L; + + // WHEN + final var stoveId = StoveId.of(stoveIdValue); + + // THEN + assertThat(stoveId.id()).isEqualTo(stoveIdValue); + } +} diff --git a/plugins-api/src/test/java/dev/cookiecode/rika2mqtt/plugins/api/v1/model/TimeDefinitionTest.java b/plugins-api/src/test/java/dev/cookiecode/rika2mqtt/plugins/api/v1/model/TimeDefinitionTest.java new file mode 100644 index 00000000..0a0b43e2 --- /dev/null +++ b/plugins-api/src/test/java/dev/cookiecode/rika2mqtt/plugins/api/v1/model/TimeDefinitionTest.java @@ -0,0 +1,63 @@ +/* + * The MIT License + * Copyright © 2022 Sebastien Vermeille + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package dev.cookiecode.rika2mqtt.plugins.api.v1.model; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.Test; + +/** + * Test class + * + * @author Sebastien Vermeille + */ +class TimeDefinitionTest { + + @Test + void asDecimalShouldReturnTheDecimalValueOfTheGivenHoursAndMinutes() { + // GIVEN + final var timeDefinition = TimeDefinition.builder().hours(12).minutes(30).build(); + + // WHEN + final var timeAsDecimal = timeDefinition.asDecimal(); + + // THEN + assertThat(timeAsDecimal).isEqualTo(12.5); + } + + @Test + void toStringShouldReturnHumanReadableTimeGivenHoursAndMinutesBeingLessThan10() { + // GIVEN + final var timeDefinition = + TimeDefinition.builder() + .hours(12) + .minutes(5) // intentionally a number < 10 so that we can check that "05" is printed + .build(); + + // WHEN + final var stringValue = timeDefinition.toString(); + + // THEN + assertThat(stringValue).isEqualTo("12:05"); + } +} diff --git a/plugins-api/src/test/java/dev/cookiecode/rika2mqtt/plugins/api/v1/model/TimeRangeTest.java b/plugins-api/src/test/java/dev/cookiecode/rika2mqtt/plugins/api/v1/model/TimeRangeTest.java new file mode 100644 index 00000000..5546e0ab --- /dev/null +++ b/plugins-api/src/test/java/dev/cookiecode/rika2mqtt/plugins/api/v1/model/TimeRangeTest.java @@ -0,0 +1,52 @@ +/* + * The MIT License + * Copyright © 2022 Sebastien Vermeille + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package dev.cookiecode.rika2mqtt.plugins.api.v1.model; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +import org.junit.jupiter.api.Test; + +/** + * Test class + * + * @author Sebastien Vermeille + */ +class TimeRangeTest { + + @Test + void toStringShouldReturnADeterminedFormat() { + + // GIVEN + final var from = TimeDefinition.builder().hours(12).minutes(5).build(); + + final var to = TimeDefinition.builder().hours(15).minutes(30).build(); + + final var timeRange = TimeRange.builder().from(from).to(to).build(); + + // WHEN + final var result = timeRange.toString(); + + // THEN + assertThat(result).isEqualTo("12:05 - 15:30"); + } +} diff --git a/plugins-internal/pom.xml b/plugins-internal/pom.xml new file mode 100644 index 00000000..05d18ad4 --- /dev/null +++ b/plugins-internal/pom.xml @@ -0,0 +1,120 @@ + + + + 4.0.0 + + dev.cookiecode + rika2mqtt-parent + 1.1.0 + + + plugins-internal + + + ${basedir}/.. + ${java.sdk.version} + ${java.sdk.version} + ${source.encoding} + + ${project.sonar.root.projectKey}-${project.groupId}-${project.artifactId} + + + + + org.pf4j + pf4j + ${pf4j.version} + + + dev.cookiecode + plugins-api + 1.1.0 + + + org.mapstruct + mapstruct + ${mapstruct.version} + + + + org.assertj + assertj-core + ${assertj.version} + test + + + dev.cookiecode + rika2mqtt-rika-firenet + 1.1.0 + compile + + + + + + + spring-boot-maven-plugin + + true + + org.springframework.boot + + + org.apache.maven.plugins + maven-compiler-plugin + + ${java.sdk.version} + ${java.sdk.version} + + + org.mapstruct + mapstruct-processor + ${mapstruct.version} + + + org.projectlombok + lombok + ${lombok.version} + + + org.projectlombok + lombok-mapstruct-binding + 0.2.0 + + + true + + + -Amapstruct.defaultComponentModel=spring + + + + + + + diff --git a/plugins-internal/src/main/java/dev/cookiecode/rika2mqtt/plugins/internal/v1/PluginDownloader.java b/plugins-internal/src/main/java/dev/cookiecode/rika2mqtt/plugins/internal/v1/PluginDownloader.java new file mode 100644 index 00000000..18dc6067 --- /dev/null +++ b/plugins-internal/src/main/java/dev/cookiecode/rika2mqtt/plugins/internal/v1/PluginDownloader.java @@ -0,0 +1,162 @@ +/* + * The MIT License + * Copyright © 2022 Sebastien Vermeille + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package dev.cookiecode.rika2mqtt.plugins.internal.v1; + +import static java.net.HttpURLConnection.HTTP_OK; +import static java.net.URI.create; + +import com.google.common.annotations.VisibleForTesting; +import dev.cookiecode.rika2mqtt.plugins.api.Beta; +import dev.cookiecode.rika2mqtt.plugins.internal.v1.exceptions.UnableToDownloadPluginException; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.net.HttpURLConnection; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.*; +import lombok.NonNull; +import lombok.RequiredArgsConstructor; +import lombok.extern.flogger.Flogger; +import org.springframework.core.env.Environment; +import org.springframework.stereotype.Service; + +/** + * Service class responsible to download rika2mqtt plugins and synchronize them at startup. + * + * @author Sebastien Vermeille + */ +@Beta +@Service +@Flogger +@RequiredArgsConstructor +public class PluginDownloader { + + static final String PLUGINS_ENV_VAR_NAME = "PLUGINS"; + static final String PLUGINS_DIR_ENV_VAR_NAME = "PLUGINS_DIR"; + static final String PLUGINS_SEPARATOR = ";"; + static final String DEFAULT_PLUGINS_DIR = "plugins"; + + private final Environment environment; + + /** + * sync plugins dir with PLUGINS environment variable. each plugin url has to be provided in + * PLUGINS=http://some.jar;http://another.jar + */ + public void synchronize() { + + // TODO: if md5 file is present (i.e maven central provide it then we should check integrity + // TODO: override files / delete before (should not delete files already present in plugins dir + // if they are not listed (useful for dev purpose) + var pluginsUrls = getDeclaredPlugins(); + var pluginsDir = getPluginsDir(); + for (var pluginUrl : pluginsUrls) { + try { + log.atInfo().log("Fetch plugin %s", pluginUrl); + downloadPlugin(pluginUrl, pluginsDir); + } catch (UnableToDownloadPluginException e) { + log.atSevere().withCause(e).log(e.getMessage()); + } + } + log.atInfo().log("Plugins synchronization: done."); + } + + @VisibleForTesting + String getPluginsDir() { + return Optional.ofNullable(environment.getProperty(PLUGINS_DIR_ENV_VAR_NAME)) + .orElse(DEFAULT_PLUGINS_DIR); + } + + @VisibleForTesting + List getDeclaredPlugins() { + final var concatenatedString = + Optional.ofNullable(environment.getProperty(PLUGINS_ENV_VAR_NAME)).orElse(""); + return Arrays.stream(concatenatedString.split(PLUGINS_SEPARATOR)) + .map(String::trim) // Trim each URL string + .filter(urlStr -> !urlStr.isEmpty()) // Filter out empty or null strings + .map( + pluginUrlStr -> { + try { + return create(pluginUrlStr).toURL(); + } catch (MalformedURLException e) { + log.atSevere().withCause(e).log("Ignore the following url: %s", pluginUrlStr); + return null; + } + }) + .filter(Objects::nonNull) // Remove the null URLs from the list + .toList(); + } + + @VisibleForTesting + void downloadPlugin(@NonNull URL jarUrl, @NonNull String pluginsDir) + throws UnableToDownloadPluginException { + try { + final var httpConn = (HttpURLConnection) jarUrl.openConnection(); + final var responseCode = httpConn.getResponseCode(); + + // Check for HTTP response code 200 (successful connection) + if (responseCode == HTTP_OK) { + // Get input stream from the connection + final var inputStream = httpConn.getInputStream(); + + // Create a FileOutputStream to write the downloaded file + var fileName = ""; + final var disposition = httpConn.getHeaderField("Content-Disposition"); + if (disposition != null) { + int index = disposition.indexOf("filename="); + if (index > 0) { + fileName = disposition.substring(index + 9); + } + } else { + fileName = jarUrl.toString().substring(jarUrl.toString().lastIndexOf("/") + 1); + } + final var saveFilePath = pluginsDir + File.separator + fileName; + + final var outputStream = new FileOutputStream(saveFilePath); + + // Read bytes from the input stream and write to the output stream + byte[] buffer = new byte[4096]; + int bytesRead; + while ((bytesRead = inputStream.read(buffer)) != -1) { + outputStream.write(buffer, 0, bytesRead); + } + + // Close streams + outputStream.close(); + inputStream.close(); + + log.atInfo().log("Plugin downloaded to: %s.", saveFilePath); + } else { + throw new UnableToDownloadPluginException( + String.format("No file to download. Server replied with HTTP code: %s", responseCode)); + } + httpConn.disconnect(); + } catch (MalformedURLException e) { + throw new UnableToDownloadPluginException( + String.format("Could not download the plugin, url is malformed: %s", jarUrl), e); + } catch (IOException e) { + throw new UnableToDownloadPluginException( + String.format("Could not download the plugin %s, io error.", jarUrl), e); + } + } +} diff --git a/plugins-internal/src/main/java/dev/cookiecode/rika2mqtt/plugins/internal/v1/Rika2MqttPluginService.java b/plugins-internal/src/main/java/dev/cookiecode/rika2mqtt/plugins/internal/v1/Rika2MqttPluginService.java new file mode 100644 index 00000000..ee1ac83d --- /dev/null +++ b/plugins-internal/src/main/java/dev/cookiecode/rika2mqtt/plugins/internal/v1/Rika2MqttPluginService.java @@ -0,0 +1,69 @@ +/* + * The MIT License + * Copyright © 2022 Sebastien Vermeille + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package dev.cookiecode.rika2mqtt.plugins.internal.v1; + +import dev.cookiecode.rika2mqtt.plugins.api.v1.StoveStatusExtension; +import dev.cookiecode.rika2mqtt.plugins.internal.v1.event.PolledStoveStatusEvent; +import lombok.RequiredArgsConstructor; +import lombok.extern.flogger.Flogger; +import org.pf4j.PluginManager; +import org.springframework.context.event.EventListener; +import org.springframework.stereotype.Service; + +/** + * Service responsible to orchestrate the whole plugins lifecycle: loading, start etc. + * + * @author Sebastien Vermeille + */ +@Service +@RequiredArgsConstructor +@Flogger +public class Rika2MqttPluginService { + + private final PluginManager pluginManager; + private final PluginDownloader pluginDownloader; + + public void start() { + log.atInfo().log("Fetch plugins ..."); + pluginDownloader.synchronize(); + + log.atInfo().log("Plugin manager starting ..."); + pluginManager.loadPlugins(); + pluginManager.startPlugins(); + } + + @EventListener + public void handlePolledStoveStatusEvent(PolledStoveStatusEvent event) { + var extensions = pluginManager.getExtensions(StoveStatusExtension.class); + + if (extensions.isEmpty()) { + log.atFinest().log( + "None of the %s plugin(s) registered a hook for extension %s, not forwarding stove status.", + pluginManager.getPlugins().size(), StoveStatusExtension.class.getSimpleName()); + } else { + extensions.forEach( + stoveStatusExtension -> + stoveStatusExtension.onPollStoveStatusSucceed(event.getStoveStatus())); + } + } +} diff --git a/plugins-internal/src/main/java/dev/cookiecode/rika2mqtt/plugins/internal/v1/event/PolledStoveStatusEvent.java b/plugins-internal/src/main/java/dev/cookiecode/rika2mqtt/plugins/internal/v1/event/PolledStoveStatusEvent.java new file mode 100644 index 00000000..97784df3 --- /dev/null +++ b/plugins-internal/src/main/java/dev/cookiecode/rika2mqtt/plugins/internal/v1/event/PolledStoveStatusEvent.java @@ -0,0 +1,40 @@ +/* + * The MIT License + * Copyright © 2022 Sebastien Vermeille + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package dev.cookiecode.rika2mqtt.plugins.internal.v1.event; + +import dev.cookiecode.rika2mqtt.plugins.api.Beta; +import dev.cookiecode.rika2mqtt.plugins.api.v1.model.StoveStatus; +import lombok.*; + +/** + * @author Sebastien Vermeille + */ +@RequiredArgsConstructor +@Getter +@ToString +@EqualsAndHashCode +@Builder +@Beta +public class PolledStoveStatusEvent implements Rika2MqttPluginEvent { + private final StoveStatus stoveStatus; +} diff --git a/plugins-internal/src/main/java/dev/cookiecode/rika2mqtt/plugins/internal/v1/event/Rika2MqttPluginEvent.java b/plugins-internal/src/main/java/dev/cookiecode/rika2mqtt/plugins/internal/v1/event/Rika2MqttPluginEvent.java new file mode 100644 index 00000000..267b23bf --- /dev/null +++ b/plugins-internal/src/main/java/dev/cookiecode/rika2mqtt/plugins/internal/v1/event/Rika2MqttPluginEvent.java @@ -0,0 +1,31 @@ +/* + * The MIT License + * Copyright © 2022 Sebastien Vermeille + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package dev.cookiecode.rika2mqtt.plugins.internal.v1.event; + +import dev.cookiecode.rika2mqtt.plugins.api.Beta; + +/** + * @author Sebastien Vermeille + */ +@Beta +public interface Rika2MqttPluginEvent {} diff --git a/plugins-internal/src/main/java/dev/cookiecode/rika2mqtt/plugins/internal/v1/exceptions/UnableToDownloadPluginException.java b/plugins-internal/src/main/java/dev/cookiecode/rika2mqtt/plugins/internal/v1/exceptions/UnableToDownloadPluginException.java new file mode 100644 index 00000000..c5b0fd21 --- /dev/null +++ b/plugins-internal/src/main/java/dev/cookiecode/rika2mqtt/plugins/internal/v1/exceptions/UnableToDownloadPluginException.java @@ -0,0 +1,34 @@ +/* + * The MIT License + * Copyright © 2022 Sebastien Vermeille + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package dev.cookiecode.rika2mqtt.plugins.internal.v1.exceptions; + +import dev.cookiecode.rika2mqtt.plugins.api.v1.exceptions.PluginException; +import lombok.experimental.StandardException; + +/** + * Exception thrown by Rika2Mqtt when a plugin can not be downloaded for some reasons. + * + * @author Sebastien Vermeille + */ +@StandardException +public class UnableToDownloadPluginException extends PluginException {} diff --git a/plugins-internal/src/main/java/dev/cookiecode/rika2mqtt/plugins/internal/v1/mapper/ControlsMapper.java b/plugins-internal/src/main/java/dev/cookiecode/rika2mqtt/plugins/internal/v1/mapper/ControlsMapper.java new file mode 100644 index 00000000..8c2289cf --- /dev/null +++ b/plugins-internal/src/main/java/dev/cookiecode/rika2mqtt/plugins/internal/v1/mapper/ControlsMapper.java @@ -0,0 +1,136 @@ +/* + * The MIT License + * Copyright © 2022 Sebastien Vermeille + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package dev.cookiecode.rika2mqtt.plugins.internal.v1.mapper; + +import static java.time.DayOfWeek.*; +import static java.util.List.of; +import static org.mapstruct.ReportingPolicy.IGNORE; + +import com.google.common.collect.ImmutableMap; +import dev.cookiecode.rika2mqtt.plugins.api.Beta; +import dev.cookiecode.rika2mqtt.plugins.api.v1.model.Controls; +import dev.cookiecode.rika2mqtt.plugins.api.v1.model.TimeRange; +import java.time.DayOfWeek; +import java.util.List; +import lombok.NonNull; +import org.mapstruct.AfterMapping; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.MappingTarget; + +/** + * @author Sebastien Vermeille + */ +@Beta +@Mapper( + unmappedTargetPolicy = IGNORE, + uses = {TimeRangeMapper.class, ConvectionFanMapper.class}) // ignore as we are using a map +public interface ControlsMapper { + @Mapping(source = "onOff", target = "on") + @Mapping(source = "ecoMode", target = "ecoModeEnabled") + Controls toApiControls(@NonNull dev.cookiecode.rika2mqtt.rika.firenet.model.Controls controls); + + @AfterMapping + default void afterMapping( + dev.cookiecode.rika2mqtt.rika.firenet.model.Controls source, + @MappingTarget Controls.ControlsBuilder target) { + + // init mappers + final var convectionFanMapper = new ConvectionFanMapperImpl(); + final var timeRangeMapper = new TimeRangeMapperImpl(); + + // Convection fan API Object Oriented way + final var fan1 = + convectionFanMapper.map( + 1, + source.getConvectionFan1Active(), + source.getConvectionFan1Level(), + source.getConvectionFan1Area()); + final var fan2 = + convectionFanMapper.map( + 2, + source.getConvectionFan2Active(), + source.getConvectionFan2Level(), + source.getConvectionFan2Area()); + + final var fans = of(fan1, fan2); + target.fans(fans); + + // Heating times API Object Oriented way + final var heatingTimes = + ImmutableMap.>builder() + .put( + MONDAY, + of( + timeRangeMapper.map(source.getHeatingTimeMon1()), + timeRangeMapper.map(source.getHeatingTimeMon2()))) + .put( + TUESDAY, + of( + timeRangeMapper.map(source.getHeatingTimeTue1()), + timeRangeMapper.map(source.getHeatingTimeTue2()))) + .put( + WEDNESDAY, + of( + timeRangeMapper.map(source.getHeatingTimeWed1()), + timeRangeMapper.map(source.getHeatingTimeWed2()))) + .put( + THURSDAY, + of( + timeRangeMapper.map(source.getHeatingTimeThu1()), + timeRangeMapper.map(source.getHeatingTimeThu2()))) + .put( + FRIDAY, + of( + timeRangeMapper.map(source.getHeatingTimeFri1()), + timeRangeMapper.map(source.getHeatingTimeFri2()))) + .put( + SATURDAY, + of( + timeRangeMapper.map(source.getHeatingTimeSat1()), + timeRangeMapper.map(source.getHeatingTimeSat2()))) + .put( + SUNDAY, + of( + timeRangeMapper.map(source.getHeatingTimeSun1()), + timeRangeMapper.map(source.getHeatingTimeSun2()))) + .build(); + + target.heatingTimes(heatingTimes); + + // debugs API Object Oriented way + var debugs = + of( + source.getDebug0(), + source.getDebug1(), + source.getDebug2(), + source.getDebug3(), + source.getDebug4()); + + target.debugs(debugs); + + // patch: for some reason mapstruct do not autodetect + // probably do to a property called "set" ... :/ + target.setBackTemperature(source.getSetBackTemperature()); + } +} diff --git a/plugins-internal/src/main/java/dev/cookiecode/rika2mqtt/plugins/internal/v1/mapper/ConvectionFanMapper.java b/plugins-internal/src/main/java/dev/cookiecode/rika2mqtt/plugins/internal/v1/mapper/ConvectionFanMapper.java new file mode 100644 index 00000000..a9c8f98c --- /dev/null +++ b/plugins-internal/src/main/java/dev/cookiecode/rika2mqtt/plugins/internal/v1/mapper/ConvectionFanMapper.java @@ -0,0 +1,51 @@ +/* + * The MIT License + * Copyright © 2022 Sebastien Vermeille + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package dev.cookiecode.rika2mqtt.plugins.internal.v1.mapper; + +import static org.mapstruct.ReportingPolicy.IGNORE; + +import dev.cookiecode.rika2mqtt.plugins.api.Beta; +import dev.cookiecode.rika2mqtt.plugins.api.v1.model.ConvectionFan; +import lombok.NonNull; +import org.mapstruct.Mapper; + +/** + * @author Sebastien Vermeille + */ +@Beta +@Mapper(unmappedTargetPolicy = IGNORE) // ignore as we are using a map +public interface ConvectionFanMapper { + + default ConvectionFan map( + @NonNull Integer identifier, + @NonNull Boolean active, + @NonNull Integer level, + @NonNull Integer area) { + return ConvectionFan.builder() + .identifier(identifier) + .active(active) + .level(level) + .area(area) + .build(); + } +} diff --git a/plugins-internal/src/main/java/dev/cookiecode/rika2mqtt/plugins/internal/v1/mapper/SensorsMapper.java b/plugins-internal/src/main/java/dev/cookiecode/rika2mqtt/plugins/internal/v1/mapper/SensorsMapper.java new file mode 100644 index 00000000..8c6371f6 --- /dev/null +++ b/plugins-internal/src/main/java/dev/cookiecode/rika2mqtt/plugins/internal/v1/mapper/SensorsMapper.java @@ -0,0 +1,105 @@ +/* + * The MIT License + * Copyright © 2022 Sebastien Vermeille + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package dev.cookiecode.rika2mqtt.plugins.internal.v1.mapper; + +import static org.mapstruct.ReportingPolicy.IGNORE; + +import dev.cookiecode.rika2mqtt.plugins.api.Beta; +import dev.cookiecode.rika2mqtt.plugins.api.v1.model.ParameterDebug; +import dev.cookiecode.rika2mqtt.plugins.api.v1.model.ParameterErrorCount; +import dev.cookiecode.rika2mqtt.plugins.api.v1.model.Sensors; +import java.util.ArrayList; +import java.util.List; +import lombok.NonNull; +import org.mapstruct.AfterMapping; +import org.mapstruct.Mapper; +import org.mapstruct.MappingTarget; + +/** + * @author Sebastien Vermeille + */ +@Beta +@Mapper(unmappedTargetPolicy = IGNORE) // ignore as we are using a map +public interface SensorsMapper { + + Sensors toApiSensors(@NonNull dev.cookiecode.rika2mqtt.rika.firenet.model.Sensors sensors); + + @AfterMapping + default void afterMapping( + dev.cookiecode.rika2mqtt.rika.firenet.model.Sensors source, + @MappingTarget Sensors.SensorsBuilder target) { + + // wrap debug properties as a collection + List debugList = new ArrayList<>(); + debugList.add(ParameterDebug.builder().number(0).value(source.getParameterDebug0()).build()); + debugList.add(ParameterDebug.builder().number(1).value(source.getParameterDebug1()).build()); + debugList.add(ParameterDebug.builder().number(2).value(source.getParameterDebug2()).build()); + debugList.add(ParameterDebug.builder().number(3).value(source.getParameterDebug3()).build()); + debugList.add(ParameterDebug.builder().number(4).value(source.getParameterDebug4()).build()); + target.parametersDebug(debugList); + + // wrap error count properties as a collection + List errorCountList = new ArrayList<>(); + errorCountList.add( + ParameterErrorCount.builder().number(0).value(source.getParameterErrorCount0()).build()); + errorCountList.add( + ParameterErrorCount.builder().number(1).value(source.getParameterErrorCount1()).build()); + errorCountList.add( + ParameterErrorCount.builder().number(2).value(source.getParameterErrorCount2()).build()); + errorCountList.add( + ParameterErrorCount.builder().number(3).value(source.getParameterErrorCount3()).build()); + errorCountList.add( + ParameterErrorCount.builder().number(4).value(source.getParameterErrorCount4()).build()); + errorCountList.add( + ParameterErrorCount.builder().number(5).value(source.getParameterErrorCount5()).build()); + errorCountList.add( + ParameterErrorCount.builder().number(6).value(source.getParameterErrorCount6()).build()); + errorCountList.add( + ParameterErrorCount.builder().number(7).value(source.getParameterErrorCount7()).build()); + errorCountList.add( + ParameterErrorCount.builder().number(8).value(source.getParameterErrorCount8()).build()); + errorCountList.add( + ParameterErrorCount.builder().number(9).value(source.getParameterErrorCount9()).build()); + errorCountList.add( + ParameterErrorCount.builder().number(10).value(source.getParameterErrorCount10()).build()); + errorCountList.add( + ParameterErrorCount.builder().number(11).value(source.getParameterErrorCount11()).build()); + errorCountList.add( + ParameterErrorCount.builder().number(12).value(source.getParameterErrorCount12()).build()); + errorCountList.add( + ParameterErrorCount.builder().number(13).value(source.getParameterErrorCount13()).build()); + errorCountList.add( + ParameterErrorCount.builder().number(14).value(source.getParameterErrorCount14()).build()); + errorCountList.add( + ParameterErrorCount.builder().number(15).value(source.getParameterErrorCount15()).build()); + errorCountList.add( + ParameterErrorCount.builder().number(16).value(source.getParameterErrorCount16()).build()); + errorCountList.add( + ParameterErrorCount.builder().number(17).value(source.getParameterErrorCount17()).build()); + errorCountList.add( + ParameterErrorCount.builder().number(18).value(source.getParameterErrorCount18()).build()); + errorCountList.add( + ParameterErrorCount.builder().number(19).value(source.getParameterErrorCount19()).build()); + target.parametersErrorCount(errorCountList); + } +} diff --git a/plugins-internal/src/main/java/dev/cookiecode/rika2mqtt/plugins/internal/v1/mapper/StoveIdMapper.java b/plugins-internal/src/main/java/dev/cookiecode/rika2mqtt/plugins/internal/v1/mapper/StoveIdMapper.java new file mode 100644 index 00000000..d0a549c2 --- /dev/null +++ b/plugins-internal/src/main/java/dev/cookiecode/rika2mqtt/plugins/internal/v1/mapper/StoveIdMapper.java @@ -0,0 +1,43 @@ +/* + * The MIT License + * Copyright © 2022 Sebastien Vermeille + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package dev.cookiecode.rika2mqtt.plugins.internal.v1.mapper; + +import static org.mapstruct.ReportingPolicy.ERROR; + +import dev.cookiecode.rika2mqtt.plugins.api.Beta; +import dev.cookiecode.rika2mqtt.plugins.api.v1.model.StoveId; +import lombok.NonNull; +import org.mapstruct.Mapper; + +/** + * Mapper for StoveId + * + * @author Sebastien Vermeille + */ +@Beta +@Mapper(unmappedTargetPolicy = ERROR) // ignore as we are using a map +public interface StoveIdMapper { + default StoveId map(@NonNull Long stoveId) { + return StoveId.of(stoveId); + } +} diff --git a/plugins-internal/src/main/java/dev/cookiecode/rika2mqtt/plugins/internal/v1/mapper/StoveStatusMapper.java b/plugins-internal/src/main/java/dev/cookiecode/rika2mqtt/plugins/internal/v1/mapper/StoveStatusMapper.java new file mode 100644 index 00000000..8a9732da --- /dev/null +++ b/plugins-internal/src/main/java/dev/cookiecode/rika2mqtt/plugins/internal/v1/mapper/StoveStatusMapper.java @@ -0,0 +1,46 @@ +/* + * The MIT License + * Copyright © 2022 Sebastien Vermeille + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package dev.cookiecode.rika2mqtt.plugins.internal.v1.mapper; + +import dev.cookiecode.rika2mqtt.plugins.api.Beta; +import dev.cookiecode.rika2mqtt.plugins.api.v1.model.StoveStatus; +import lombok.NonNull; +import org.mapstruct.Mapper; +import org.mapstruct.ReportingPolicy; + +/** + * @author Sebastien Vermeille + */ +@Beta +@Mapper( + unmappedTargetPolicy = ReportingPolicy.IGNORE, + uses = { + SensorsMapper.class, + ControlsMapper.class, + StoveIdMapper.class + }) // ignore as we are using a map +public interface StoveStatusMapper { + + StoveStatus toApiStoveStatus( + @NonNull dev.cookiecode.rika2mqtt.rika.firenet.model.StoveStatus stoveStatus); +} diff --git a/plugins-internal/src/main/java/dev/cookiecode/rika2mqtt/plugins/internal/v1/mapper/TimeRangeMapper.java b/plugins-internal/src/main/java/dev/cookiecode/rika2mqtt/plugins/internal/v1/mapper/TimeRangeMapper.java new file mode 100644 index 00000000..eb98a1e7 --- /dev/null +++ b/plugins-internal/src/main/java/dev/cookiecode/rika2mqtt/plugins/internal/v1/mapper/TimeRangeMapper.java @@ -0,0 +1,61 @@ +/* + * The MIT License + * Copyright © 2022 Sebastien Vermeille + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package dev.cookiecode.rika2mqtt.plugins.internal.v1.mapper; + +import static java.lang.Integer.parseInt; +import static org.mapstruct.ReportingPolicy.ERROR; + +import dev.cookiecode.rika2mqtt.plugins.api.Beta; +import dev.cookiecode.rika2mqtt.plugins.api.v1.model.TimeDefinition; +import dev.cookiecode.rika2mqtt.plugins.api.v1.model.TimeRange; +import lombok.NonNull; +import org.mapstruct.Mapper; + +/** + * @author Sebastien Vermeille + */ +@Beta +@Mapper(unmappedTargetPolicy = ERROR) +public interface TimeRangeMapper { + /** + * @param rikaTimeRange i.e: `13302200` + * @return a TimeRange from 13:30 to 22:00 + */ + default TimeRange map(@NonNull String rikaTimeRange) { + + var from = rikaTimeRange.substring(0, 4); + var to = rikaTimeRange.substring(4); + return TimeRange.builder() + .from( + TimeDefinition.builder() + .hours(parseInt(from.substring(0, 2))) + .minutes(parseInt(from.substring(2))) + .build()) + .to( + TimeDefinition.builder() + .hours(parseInt(to.substring(0, 2))) + .minutes(parseInt(to.substring(2))) + .build()) + .build(); + } +} diff --git a/plugins-internal/src/main/java/dev/cookiecode/rika2mqtt/plugins/internal/v1/pf4j/Pf4jPluginManagerConfig.java b/plugins-internal/src/main/java/dev/cookiecode/rika2mqtt/plugins/internal/v1/pf4j/Pf4jPluginManagerConfig.java new file mode 100644 index 00000000..49e91fb8 --- /dev/null +++ b/plugins-internal/src/main/java/dev/cookiecode/rika2mqtt/plugins/internal/v1/pf4j/Pf4jPluginManagerConfig.java @@ -0,0 +1,39 @@ +/* + * The MIT License + * Copyright © 2022 Sebastien Vermeille + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package dev.cookiecode.rika2mqtt.plugins.internal.v1.pf4j; + +import org.pf4j.PluginManager; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * @author Sebastien Vermeille + */ +@Configuration +public class Pf4jPluginManagerConfig { + + @Bean + public PluginManager pluginManager() { + return new Rika2MqttPluginManager(); + } +} diff --git a/plugins-internal/src/main/java/dev/cookiecode/rika2mqtt/plugins/internal/v1/pf4j/Rika2MqttPluginManager.java b/plugins-internal/src/main/java/dev/cookiecode/rika2mqtt/plugins/internal/v1/pf4j/Rika2MqttPluginManager.java new file mode 100644 index 00000000..79be6ee4 --- /dev/null +++ b/plugins-internal/src/main/java/dev/cookiecode/rika2mqtt/plugins/internal/v1/pf4j/Rika2MqttPluginManager.java @@ -0,0 +1,186 @@ +/* + * The MIT License + * Copyright © 2022 Sebastien Vermeille + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package dev.cookiecode.rika2mqtt.plugins.internal.v1.pf4j; + +import static org.pf4j.PluginState.*; + +import com.google.common.annotations.VisibleForTesting; +import dev.cookiecode.rika2mqtt.plugins.api.Beta; +import dev.cookiecode.rika2mqtt.plugins.api.v1.PluginConfiguration; +import dev.cookiecode.rika2mqtt.plugins.api.v1.Rika2MqttPlugin; +import dev.cookiecode.rika2mqtt.plugins.api.v1.annotations.ConfigurablePlugin; +import dev.cookiecode.rika2mqtt.plugins.api.v1.exceptions.InvalidPluginConfigurationException; +import java.util.*; +import lombok.NonNull; +import lombok.extern.flogger.Flogger; +import org.pf4j.*; + +/** + * @author Sebastien Vermeille + */ +@Beta +@Flogger +public class Rika2MqttPluginManager extends DefaultPluginManager { + + @VisibleForTesting + static final EnumSet PLUGIN_STATES_PREVENTING_START = EnumSet.of(DISABLED, STARTED); + + @VisibleForTesting static final String PLUGIN_ENV_NAME_PREFIX = "PLUGIN_"; + + @Override + public void startPlugins() { + log.atInfo().log("Start plugins"); + getResolvedPlugins().stream() + .filter(pluginWrapper -> shouldStartPlugin(pluginWrapper.getPluginState())) + .forEach(this::handlePlugin); + } + + @VisibleForTesting + boolean shouldStartPlugin(@NonNull final PluginState pluginState) { + return !PLUGIN_STATES_PREVENTING_START.contains(pluginState); + } + + @VisibleForTesting + void handlePlugin(@NonNull final PluginWrapper pluginWrapper) { + try { + final var rika2MqttPlugin = (Rika2MqttPlugin) pluginWrapper.getPlugin(); + final var pluginConfiguration = loadPluginConfiguration(rika2MqttPlugin); + + if (isPluginConfigurationValid(rika2MqttPlugin, pluginConfiguration)) { + startPlugin(pluginWrapper, pluginConfiguration); + } else { + handleInvalidPluginConfiguration(pluginWrapper); + } + } catch (Exception | LinkageError e) { + handlePluginStartFailure(pluginWrapper, e); + } finally { + firePluginStateEvent( + new PluginStateEvent(this, pluginWrapper, pluginWrapper.getPluginState())); + } + } + + @VisibleForTesting + void handleInvalidPluginConfiguration(@NonNull final PluginWrapper pluginWrapper) { + final var pluginName = getPluginLabel(pluginWrapper.getDescriptor()); + log.atSevere().log( + "Plugin '%s' configuration is invalid. Aborting load of the plugin", pluginName); + stopAndFailPlugin(pluginWrapper, pluginName); + } + + @VisibleForTesting + void handlePluginStartFailure( + @NonNull final PluginWrapper pluginWrapper, @NonNull final Throwable exception) { + pluginWrapper.setPluginState(FAILED); + pluginWrapper.setFailedException(exception); + log.atSevere().withCause(pluginWrapper.getFailedException()).log( + "Unable to start plugin '%s'", getPluginLabel(pluginWrapper.getDescriptor())); + } + + @VisibleForTesting + void stopAndFailPlugin( + @NonNull final PluginWrapper pluginWrapper, @NonNull final String pluginName) { + stopPlugin(pluginWrapper.getPluginId()); + pluginWrapper.setPluginState(FAILED); + pluginWrapper.setFailedException( + new InvalidPluginConfigurationException( + String.format( + "Plugin '%s' configuration is invalid. Aborting load of the plugin", pluginName))); + } + + @VisibleForTesting + void startPlugin( + @NonNull final PluginWrapper pluginWrapper, + @NonNull final PluginConfiguration pluginConfiguration) { + log.atInfo().log("Start plugin '%s'", getPluginLabel(pluginWrapper.getDescriptor())); + ((Rika2MqttPlugin) pluginWrapper.getPlugin()).preStart(pluginConfiguration); + pluginWrapper.getPlugin().start(); + pluginWrapper.setPluginState(STARTED); + pluginWrapper.setFailedException(null); + startedPlugins.add(pluginWrapper); + } + + @VisibleForTesting + boolean isPluginConfigurationValid( + @NonNull final Rika2MqttPlugin plugin, + @NonNull final PluginConfiguration pluginConfiguration) { + + // configurable plugins + if (plugin instanceof ConfigurablePlugin configurablePlugin) { + + final var parameters = configurablePlugin.declarePluginConfigurationParameters(); + + final List errors = new ArrayList<>(); + + for (final var param : parameters) { + // check required params are provided + if (param.isRequired() + && pluginConfiguration.getOptionalParameter(param.getParameterName()).isEmpty()) { + errors.add( + String.format( + "Parameter '%s' is required for this plugin to work properly. However unable to find any ENV named: '%s%s' declaring any value", + param.getParameterName(), PLUGIN_ENV_NAME_PREFIX, param.getParameterName())); + } + } + + if (errors.isEmpty()) { + return true; + } else { + log.atSevere().log("%s", errors); + return false; + } + + } else { + return true; + } + } + + @VisibleForTesting + PluginConfiguration loadPluginConfiguration(@NonNull final Rika2MqttPlugin plugin) { + + final Map configuration = new HashMap<>(); + + if (plugin instanceof ConfigurablePlugin configurablePlugin) { + for (final var parameter : configurablePlugin.declarePluginConfigurationParameters()) { + + var value = + getEnvironmentVariable(PLUGIN_ENV_NAME_PREFIX + parameter.getParameterName()) + .orElseGet( + () -> { + if (parameter.isOptional() && parameter.getDefaultValue().isPresent()) { + return parameter.getDefaultValue().get().toString(); + } else { + return null; + } + }); + configuration.put(parameter.getParameterName(), value); + } + } + + return PluginConfiguration.builder().parameters(configuration).build(); + } + + @VisibleForTesting + Optional getEnvironmentVariable(@NonNull String environmentVariableName) { + return Optional.ofNullable(System.getenv(environmentVariableName)); + } +} diff --git a/plugins-internal/src/test/java/dev/cookiecode/rika2mqtt/plugins/internal/v1/PluginDownloaderTest.java b/plugins-internal/src/test/java/dev/cookiecode/rika2mqtt/plugins/internal/v1/PluginDownloaderTest.java new file mode 100644 index 00000000..53dd9e27 --- /dev/null +++ b/plugins-internal/src/test/java/dev/cookiecode/rika2mqtt/plugins/internal/v1/PluginDownloaderTest.java @@ -0,0 +1,180 @@ +/* + * The MIT License + * Copyright © 2022 Sebastien Vermeille + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package dev.cookiecode.rika2mqtt.plugins.internal.v1; + +import static dev.cookiecode.rika2mqtt.plugins.internal.v1.PluginDownloader.*; +import static java.net.URI.create; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.*; + +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.io.TempDir; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Spy; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.core.env.Environment; + +/** + * Test class + * + * @author Sebastien Vermeille + */ +@ExtendWith(MockitoExtension.class) +class PluginDownloaderTest { + + @TempDir public Path pluginDir; + + @InjectMocks @Spy private PluginDownloader pluginDownloader; + + @Mock private Environment environment; + + @Test + void getDeclaredPluginsShouldReturnAnEmptyListGivenOnlyASpaceIsProvided() { + + // GIVEN + final var space = " "; + doReturn(space).when(environment).getProperty(PLUGINS_ENV_VAR_NAME); + + // WHEN + final var urls = pluginDownloader.getDeclaredPlugins(); + + // THEN + assertThat(urls).isEmpty(); + } + + @Test + void getDeclaredPluginsShouldReturnAnEmptyListGivenNothingIsProvided() { + + // GIVEN + final var empty = ""; + doReturn(empty).when(environment).getProperty(PLUGINS_ENV_VAR_NAME); + + // WHEN + final var urls = pluginDownloader.getDeclaredPlugins(); + + // THEN + assertThat(urls).isEmpty(); + } + + @Test + void getDeclaredPluginsShouldReturnAnEmptyListGivenEnvIsNotSet() { + + // GIVEN + doReturn(null).when(environment).getProperty(PLUGINS_ENV_VAR_NAME); + + // WHEN + final var urls = pluginDownloader.getDeclaredPlugins(); + + // THEN + assertThat(System.getenv(PLUGINS_ENV_VAR_NAME)).isNull(); + assertThat(urls).isEmpty(); + } + + @Test + void getDeclaredPluginsShouldReturnAUrlObjectWrapping2UrlsGivenTwoUrlsWereProvided() + throws Exception { + + // GIVEN + final var pluginAUrl = "http://plugin-a.jar"; + final var pluginBUrl = "http://plugin-b.jar"; + + final var pluginsUrls = String.format("%s%s%s", pluginAUrl, PLUGINS_SEPARATOR, pluginBUrl); + doReturn(pluginsUrls).when(environment).getProperty(PLUGINS_ENV_VAR_NAME); + + // WHEN + final var urls = pluginDownloader.getDeclaredPlugins(); + + // THEN + assertThat(urls) + .isNotEmpty() + .hasSize(2) + .containsExactly(create(pluginAUrl).toURL(), create(pluginBUrl).toURL()); + } + + @Test + void getPluginsDirShouldReturnDefaultPluginsDirGivenEnvIsNotSet() { + // GIVEN + doReturn(null).when(environment).getProperty(PLUGINS_DIR_ENV_VAR_NAME); + + // WHEN + final var pluginsDir = pluginDownloader.getPluginsDir(); + + // THEN + assertThat(pluginsDir).isEqualTo(DEFAULT_PLUGINS_DIR); + } + + @Test + void getPluginsDirShouldReturnPluginsDirProvidedValueDirGivenEnvIsSet() { + // GIVEN + final var somedir = "somedir"; + doReturn(somedir).when(environment).getProperty(PLUGINS_DIR_ENV_VAR_NAME); + + // WHEN + final var pluginsDir = pluginDownloader.getPluginsDir(); + + // THEN + assertThat(pluginsDir).isEqualTo(somedir); + } + + @Test + void synchronizeShouldInvokeDownloadPlugin2TimesGivenThereAreTwoPluginsToDownload() + throws Exception { + // GIVEN + final var pluginAUrl = "http://plugin-a.jar"; + final var pluginBUrl = "http://plugin-b.jar"; + + final var pluginsUrls = String.format("%s%s%s", pluginAUrl, PLUGINS_SEPARATOR, pluginBUrl); + doReturn(pluginsUrls).when(environment).getProperty(PLUGINS_ENV_VAR_NAME); + + final var pluginDirPath = pluginDir.toAbsolutePath().toString(); + doReturn(pluginDirPath).when(environment).getProperty(PLUGINS_DIR_ENV_VAR_NAME); + + doNothing().when(pluginDownloader).downloadPlugin(any(), anyString()); + + // WHEN + pluginDownloader.synchronize(); + + // THEN + verify(pluginDownloader, times(2)).downloadPlugin(any(URL.class), anyString()); + } + + @Test + void downloadPluginShouldDownloadTheFileGivenItExists() throws Exception { + // GIVEN + final var pluginUrl = "https://repo.maven.apache.org/maven2/dev/cookiecode/maven-metadata.xml"; + + // WHEN + pluginDownloader.downloadPlugin( + create(pluginUrl).toURL(), pluginDir.toAbsolutePath().toString()); + + // THEN + final var expectedFile = Paths.get(pluginDir.toAbsolutePath().toString(), "maven-metadata.xml"); + assertThat(Files.exists(expectedFile)).as("File has been downloaded").isTrue(); + } +} diff --git a/plugins-internal/src/test/java/dev/cookiecode/rika2mqtt/plugins/internal/v1/Rika2MqttPluginServiceTest.java b/plugins-internal/src/test/java/dev/cookiecode/rika2mqtt/plugins/internal/v1/Rika2MqttPluginServiceTest.java new file mode 100644 index 00000000..01ce7bef --- /dev/null +++ b/plugins-internal/src/test/java/dev/cookiecode/rika2mqtt/plugins/internal/v1/Rika2MqttPluginServiceTest.java @@ -0,0 +1,112 @@ +/* + * The MIT License + * Copyright © 2022 Sebastien Vermeille + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package dev.cookiecode.rika2mqtt.plugins.internal.v1; + +import static org.mockito.Mockito.*; + +import dev.cookiecode.rika2mqtt.plugins.api.v1.StoveStatusExtension; +import dev.cookiecode.rika2mqtt.plugins.api.v1.model.StoveStatus; +import dev.cookiecode.rika2mqtt.plugins.internal.v1.event.PolledStoveStatusEvent; +import java.util.List; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.pf4j.PluginManager; + +/** + * Test class + * + * @author Sebastien Vermeille + */ +@ExtendWith(MockitoExtension.class) +class Rika2MqttPluginServiceTest { + + @InjectMocks private Rika2MqttPluginService rika2MqttPluginService; + + @Mock private PluginManager pluginManager; + @Mock private PluginDownloader pluginDownloader; + + @Test + void startShouldInvokeSynchronizePlugins() { + + // GIVEN + // nothing particular + + // WHEN + rika2MqttPluginService.start(); + + // THEN + verify(pluginDownloader, times(1)).synchronize(); + } + + @Test + void startShouldInvokeLoadPlugins() { + + // GIVEN + // nothing particular + + // WHEN + rika2MqttPluginService.start(); + + // THEN + verify(pluginManager, times(1)).loadPlugins(); + } + + @Test + void startShouldInvokeStartPlugins() { + + // GIVEN + // nothing particular + + // WHEN + rika2MqttPluginService.start(); + + // THEN + verify(pluginManager, times(1)).startPlugins(); + } + + @Test + void + handlePolledStoveStatusEventShouldPropagateTheEventToAllRegisteredExtensionsGivenThereAreTwo() { + + // GIVEN + final var event = mock(PolledStoveStatusEvent.class); + final var stoveStatus = mock(StoveStatus.class); + when(event.getStoveStatus()).thenReturn(stoveStatus); + + // two plugins extensions + final var extensionAlpha = mock(StoveStatusExtension.class); + final var extensionBeta = mock(StoveStatusExtension.class); + final var extensions = List.of(extensionAlpha, extensionBeta); + when(pluginManager.getExtensions(StoveStatusExtension.class)).thenReturn(extensions); + + // WHEN + rika2MqttPluginService.handlePolledStoveStatusEvent(event); + + // THEN + verify(extensionAlpha, times(1)).onPollStoveStatusSucceed(stoveStatus); + verify(extensionBeta, times(1)).onPollStoveStatusSucceed(stoveStatus); + } +} diff --git a/plugins-internal/src/test/java/dev/cookiecode/rika2mqtt/plugins/internal/v1/mapper/ControlsMapperTest.java b/plugins-internal/src/test/java/dev/cookiecode/rika2mqtt/plugins/internal/v1/mapper/ControlsMapperTest.java new file mode 100644 index 00000000..8671922f --- /dev/null +++ b/plugins-internal/src/test/java/dev/cookiecode/rika2mqtt/plugins/internal/v1/mapper/ControlsMapperTest.java @@ -0,0 +1,120 @@ +/* + * The MIT License + * Copyright © 2022 Sebastien Vermeille + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package dev.cookiecode.rika2mqtt.plugins.internal.v1.mapper; + +import static org.assertj.core.api.Assertions.assertThat; + +import dev.cookiecode.rika2mqtt.rika.firenet.model.Controls; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +/** + * Test class + * + * @author Sebastien Vermeille + */ +@SpringBootTest( + classes = {ControlsMapperImpl.class, TimeRangeMapperImpl.class, ConvectionFanMapperImpl.class}) +class ControlsMapperTest { + + @Autowired private ControlsMapper mapper; + + @Test + void mapShouldProperlyMapGivenProperties() { + + // GIVEN + final var on = true; + final var revision = 1232132L; + final var operatingMode = 1; + final var heatingPower = 80; + final var targetTemperature = 19; + final var bakeTemperature = 20; + final var ecoMode = true; + final var fan1Active = true; + final var fan1Area = 50; + final var fan1Level = 12; + final var fan2Active = false; + final var fan2Area = 54; + final var fan2Level = 13; + final var frostProtection = true; + final var debug0 = 0; + final var debug1 = 1; + final var debug2 = 2; + final var debug3 = 3; + final var debug4 = 4; + final Controls controls = + Controls.builder() + .onOff(on) + .revision(revision) + .operatingMode(operatingMode) + .heatingPower(heatingPower) + .targetTemperature(targetTemperature) + .bakeTemperature(bakeTemperature) + .ecoMode(ecoMode) + .convectionFan1Active(fan1Active) + .convectionFan1Area(fan1Area) + .convectionFan1Level(fan1Level) + .convectionFan2Active(fan2Active) + .convectionFan2Area(fan2Area) + .convectionFan2Level(fan2Level) + .frostProtectionActive(frostProtection) + // TODO: heating time are provided here because could not mock the mapper + // at the moment. The conversion will not be checked in this class however) + .heatingTimeMon1("08001200") + .heatingTimeMon2("12041800") + .heatingTimeTue1("08001200") + .heatingTimeTue2("12041800") + .heatingTimeWed1("08001200") + .heatingTimeWed2("12041800") + .heatingTimeThu1("08001200") + .heatingTimeThu2("12041800") + .heatingTimeFri1("08001200") + .heatingTimeFri2("12041800") + .heatingTimeSat1("08001200") + .heatingTimeSat2("12041800") + .heatingTimeSun1("08001200") + .heatingTimeSun2("12041800") + .debug0(debug0) + .debug1(debug1) + .debug2(debug2) + .debug3(debug3) + .debug4(debug4) + .build(); + + // WHEN + final var result = mapper.toApiControls(controls); + + // THEN + assertThat(result.isOn()).isEqualTo(on); + assertThat(result.getRevision()).isEqualTo(revision); + assertThat(result.getOperatingMode()).isEqualTo(operatingMode); + assertThat(result.getHeatingPower()).isEqualTo(heatingPower); + assertThat(result.getTargetTemperature()).isEqualTo(targetTemperature); + assertThat(result.getBakeTemperature()).isEqualTo(bakeTemperature); + assertThat(result.isEcoModeEnabled()).isEqualTo(ecoMode); + + assertThat(result.getFans().get(0).isActive()).isEqualTo(fan1Active); + assertThat(result.getFans().get(1).isActive()).isEqualTo(fan2Active); + } +} diff --git a/plugins-internal/src/test/java/dev/cookiecode/rika2mqtt/plugins/internal/v1/mapper/ConvectionFanMapperTest.java b/plugins-internal/src/test/java/dev/cookiecode/rika2mqtt/plugins/internal/v1/mapper/ConvectionFanMapperTest.java new file mode 100644 index 00000000..50181a16 --- /dev/null +++ b/plugins-internal/src/test/java/dev/cookiecode/rika2mqtt/plugins/internal/v1/mapper/ConvectionFanMapperTest.java @@ -0,0 +1,70 @@ +/* + * The MIT License + * Copyright © 2022 Sebastien Vermeille + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package dev.cookiecode.rika2mqtt.plugins.internal.v1.mapper; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.Assert.assertThrows; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +/** + * Test class + * + * @author Sebastien Vermeille + */ +@SpringBootTest(classes = {ConvectionFanMapperImpl.class}) +class ConvectionFanMapperTest { + + @Autowired private ConvectionFanMapper mapper; + + @Test + void mapShouldThrowAnNullPointerExceptionGivenNoParameterIsPassed() { + assertThrows( + NullPointerException.class, + () -> { + // WHEN + mapper.map(null, null, null, null); + }); + } + + @Test + void mapShouldConvertSuccessfullyGivenFanProperties() { + + // GIVEN + final var identifier = 1; + final var active = true; + final var level = 12; + final var area = 50; + + // WHEN + final var result = mapper.map(identifier, active, level, area); + + // THEN + assertThat(result.getIdentifier()).isEqualTo(identifier); + assertThat(result.isActive()).isEqualTo(active); + assertThat(result.getLevel()).isEqualTo(level); + assertThat(result.getArea()).isEqualTo(area); + } +} diff --git a/plugins-internal/src/test/java/dev/cookiecode/rika2mqtt/plugins/internal/v1/mapper/SensorsMapperTest.java b/plugins-internal/src/test/java/dev/cookiecode/rika2mqtt/plugins/internal/v1/mapper/SensorsMapperTest.java new file mode 100644 index 00000000..477cb9ca --- /dev/null +++ b/plugins-internal/src/test/java/dev/cookiecode/rika2mqtt/plugins/internal/v1/mapper/SensorsMapperTest.java @@ -0,0 +1,666 @@ +/* + * The MIT License + * Copyright © 2022 Sebastien Vermeille + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package dev.cookiecode.rika2mqtt.plugins.internal.v1.mapper; + +import static org.assertj.core.api.Assertions.assertThat; + +import dev.cookiecode.rika2mqtt.rika.firenet.model.Sensors; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +/** + * Test class + * + * @author Sebastien Vermeille + */ +@SpringBootTest(classes = {SensorsMapperImpl.class}) +class SensorsMapperTest { + + @Autowired private SensorsMapper mapper; + + @Test + void toApiSensorsShouldFillAllSensorsPropertiesGivenACompleteRikaFirenetSensorInstance() { + + // GIVEN + Integer inputFlameTemperature = 200; + Integer inputBakeTemperature = 300; + Integer statusError = 1; + Integer statusSubError = 2; + Integer statusWarning = 3; + Integer statusService = 4; + Integer outputDischargeMotor = 5; + Integer outputDischargeCurrent = 6; + Integer outputIdFan = 7; + Integer outputIdFanTarget = 8; + Integer outputInsertionMotor = 9; + Integer outputInsertionCurrent = 10; + Integer outputAirFlaps = 11; + Integer outputAirFlapsTargetPosition = 12; + Boolean outputBurnBackFlapMagnet = true; + Boolean outputGridMotor = false; + Boolean outputIgnition = true; + Boolean inputUpperTemperatureLimiter = true; + Boolean inputPressureSwitch = true; + Integer inputPressureSensor = 13; + Boolean inputGridContact = true; + Boolean inputDoor = true; + Boolean inputCover = true; + Boolean inputExternalRequest = true; + Boolean inputBurnBackFlapSwitch = true; + Integer parameterDebug0 = 44; + Integer parameterDebug1 = 45; + Integer parameterDebug2 = 46; + Integer parameterDebug3 = 47; + Integer parameterDebug4 = 48; + Integer parameterErrorCount0 = 30; + Integer parameterErrorCount1 = 31; + Integer parameterErrorCount2 = 32; + Integer parameterErrorCount3 = 33; + Integer parameterErrorCount4 = 34; + Integer parameterErrorCount5 = 35; + Integer parameterErrorCount6 = 36; + Integer parameterErrorCount7 = 37; + Integer parameterErrorCount8 = 38; + Integer parameterErrorCount9 = 39; + Integer parameterErrorCount10 = 40; + Integer parameterErrorCount11 = 41; + Integer parameterErrorCount12 = 42; + Integer parameterErrorCount13 = 43; + Integer parameterErrorCount14 = 44; + Integer parameterErrorCount15 = 45; + Integer parameterErrorCount16 = 46; + Integer parameterErrorCount17 = 47; + Integer parameterErrorCount18 = 48; + Integer parameterErrorCount19 = 49; + Boolean inputFlueGasFlapSwitch = true; + Double inputBoardTemperature = 20.20; + Integer inputCurrentStage = 12; + Integer statusMainState = 42; + Integer statusSubState = 43; + Integer statusWifiStrength = 100; + Boolean parameterEcoModePossible = true; + Integer parameterFabricationNumber = 12323; + Integer parameterStoveTypeNumber = 46; + Integer parameterLanguageNumber = 2; + Integer parameterVersionMainBoard = 5; + Integer parameterVersionTft = 6; + Integer parameterVersionWifi = 7; + Integer parameterVersionMainBoardBootLoader = 8; + Integer parameterVersionTftBootLoader = 9; + Integer parameterVersionWifiBootLoader = 10; + Integer parameterVersionMainBoardSub = 11; + Integer inputTargetStagePid = 12; + Integer inputCurrentStagePid = 13; + Integer parameterVersionTftSub = 14; + Integer parameterVersionWifiSub = 15; + Integer parameterRuntimePellets = 16; + Integer parameterRuntimeLogs = 17; + Integer parameterFeedRateTotal = 18; + Integer parameterFeedRateService = 19; + Integer parameterServiceCountdownKg = 20; + Integer parameterServiceCountdownTime = 21; + Integer parameterIgnitionCount = 22; + Integer parameterOnOffCycleCount = 23; + Integer parameterFlameSensorOffset = 24; + Integer parameterPressureSensorOffset = 25; + Integer parameterKgTillCleaning = 26; + Integer parameterCleanIntervalBig = 27; + Integer parameterIdFanTuning = 28; + Integer parameterSpiralMotorsTuning = 29; + Boolean statusFrostStarted = true; + Boolean statusHeatingTimesNotProgrammed = true; + Double inputRoomTemperature = 22.4; + Sensors rikaFirenetSensors = + Sensors.builder() + .inputRoomTemperature(inputRoomTemperature) + .inputFlameTemperature(inputFlameTemperature) + .inputBakeTemperature(inputBakeTemperature) + .statusError(statusError) + .statusSubError(statusSubError) + .statusWarning(statusWarning) + .statusService(statusService) + .outputDischargeMotor(outputDischargeMotor) + .outputDischargeCurrent(outputDischargeCurrent) + .outputIdFan(outputIdFan) + .outputIdFanTarget(outputIdFanTarget) + .outputInsertionMotor(outputInsertionMotor) + .outputInsertionCurrent(outputInsertionCurrent) + .outputAirFlaps(outputAirFlaps) + .outputAirFlapsTargetPosition(outputAirFlapsTargetPosition) + .outputBurnBackFlapMagnet(outputBurnBackFlapMagnet) + .outputGridMotor(outputGridMotor) + .outputIgnition(outputIgnition) + .inputUpperTemperatureLimiter(inputUpperTemperatureLimiter) + .inputPressureSwitch(inputPressureSwitch) + .inputPressureSensor(inputPressureSensor) + .inputGridContact(inputGridContact) + .inputDoor(inputDoor) + .inputCover(inputCover) + .inputExternalRequest(inputExternalRequest) + .inputBurnBackFlapSwitch(inputBurnBackFlapSwitch) + .inputFlueGasFlapSwitch(inputFlueGasFlapSwitch) + .inputBoardTemperature(inputBoardTemperature) + .inputCurrentStage(inputCurrentStage) + .statusMainState(statusMainState) + .statusSubState(statusSubState) + .statusWifiStrength(statusWifiStrength) + .parameterEcoModePossible(parameterEcoModePossible) + .parameterFabricationNumber(parameterFabricationNumber) + .parameterStoveTypeNumber(parameterStoveTypeNumber) + .parameterLanguageNumber(parameterLanguageNumber) + .parameterVersionMainBoard(parameterVersionMainBoard) + .parameterVersionTft(parameterVersionTft) + .parameterVersionWifi(parameterVersionWifi) + .parameterVersionMainBoardBootLoader(parameterVersionMainBoardBootLoader) + .parameterVersionTftBootLoader(parameterVersionTftBootLoader) + .parameterVersionWifiBootLoader(parameterVersionWifiBootLoader) + .parameterVersionMainBoardSub(parameterVersionMainBoardSub) + .inputTargetStagePid(inputTargetStagePid) + .inputCurrentStagePid(inputCurrentStagePid) + .parameterVersionTftSub(parameterVersionTftSub) + .parameterVersionWifiSub(parameterVersionWifiSub) + .parameterRuntimePellets(parameterRuntimePellets) + .parameterRuntimeLogs(parameterRuntimeLogs) + .parameterFeedRateTotal(parameterFeedRateTotal) + .parameterFeedRateService(parameterFeedRateService) + .parameterServiceCountdownKg(parameterServiceCountdownKg) + .parameterServiceCountdownTime(parameterServiceCountdownTime) + .parameterIgnitionCount(parameterIgnitionCount) + .parameterOnOffCycleCount(parameterOnOffCycleCount) + .parameterFlameSensorOffset(parameterFlameSensorOffset) + .parameterPressureSensorOffset(parameterPressureSensorOffset) + .parameterKgTillCleaning(parameterKgTillCleaning) + .parameterCleanIntervalBig(parameterCleanIntervalBig) + .parameterIdFanTuning(parameterIdFanTuning) + .parameterSpiralMotorsTuning(parameterSpiralMotorsTuning) + .statusFrostStarted(statusFrostStarted) + .statusHeatingTimesNotProgrammed(statusHeatingTimesNotProgrammed) + .parameterErrorCount0(parameterErrorCount0) + .parameterErrorCount1(parameterErrorCount1) + .parameterErrorCount2(parameterErrorCount2) + .parameterErrorCount3(parameterErrorCount3) + .parameterErrorCount4(parameterErrorCount4) + .parameterErrorCount5(parameterErrorCount5) + .parameterErrorCount6(parameterErrorCount6) + .parameterErrorCount7(parameterErrorCount7) + .parameterErrorCount8(parameterErrorCount8) + .parameterErrorCount9(parameterErrorCount9) + .parameterErrorCount10(parameterErrorCount10) + .parameterErrorCount11(parameterErrorCount11) + .parameterErrorCount12(parameterErrorCount12) + .parameterErrorCount13(parameterErrorCount13) + .parameterErrorCount14(parameterErrorCount14) + .parameterErrorCount15(parameterErrorCount15) + .parameterErrorCount16(parameterErrorCount16) + .parameterErrorCount17(parameterErrorCount17) + .parameterErrorCount18(parameterErrorCount18) + .parameterErrorCount19(parameterErrorCount19) + .parameterDebug0(parameterDebug0) + .parameterDebug1(parameterDebug1) + .parameterDebug2(parameterDebug2) + .parameterDebug3(parameterDebug3) + .parameterDebug4(parameterDebug4) + .build(); + + // WHEN + var apiSensors = mapper.toApiSensors(rikaFirenetSensors); + + // THEN + assertThat(apiSensors).hasNoNullFieldsOrProperties(); + } + + @Test + void toApiSensorsShouldFillParametersDebugCollectionExtraFieldProperly() { + + // GIVEN + Integer inputFlameTemperature = 200; + Integer inputBakeTemperature = 300; + Integer statusError = 1; + Integer statusSubError = 2; + Integer statusWarning = 3; + Integer statusService = 4; + Integer outputDischargeMotor = 5; + Integer outputDischargeCurrent = 6; + Integer outputIdFan = 7; + Integer outputIdFanTarget = 8; + Integer outputInsertionMotor = 9; + Integer outputInsertionCurrent = 10; + Integer outputAirFlaps = 11; + Integer outputAirFlapsTargetPosition = 12; + Boolean outputBurnBackFlapMagnet = true; + Boolean outputGridMotor = false; + Boolean outputIgnition = true; + Boolean inputUpperTemperatureLimiter = true; + Boolean inputPressureSwitch = true; + Integer inputPressureSensor = 13; + Boolean inputGridContact = true; + Boolean inputDoor = true; + Boolean inputCover = true; + Boolean inputExternalRequest = true; + Boolean inputBurnBackFlapSwitch = true; + Integer parameterDebug0 = 44; + Integer parameterDebug1 = 45; + Integer parameterDebug2 = 46; + Integer parameterDebug3 = 47; + Integer parameterDebug4 = 48; + Integer parameterErrorCount0 = 30; + Integer parameterErrorCount1 = 31; + Integer parameterErrorCount2 = 32; + Integer parameterErrorCount3 = 33; + Integer parameterErrorCount4 = 34; + Integer parameterErrorCount5 = 35; + Integer parameterErrorCount6 = 36; + Integer parameterErrorCount7 = 37; + Integer parameterErrorCount8 = 38; + Integer parameterErrorCount9 = 39; + Integer parameterErrorCount10 = 40; + Integer parameterErrorCount11 = 41; + Integer parameterErrorCount12 = 42; + Integer parameterErrorCount13 = 43; + Integer parameterErrorCount14 = 44; + Integer parameterErrorCount15 = 45; + Integer parameterErrorCount16 = 46; + Integer parameterErrorCount17 = 47; + Integer parameterErrorCount18 = 48; + Integer parameterErrorCount19 = 49; + Boolean inputFlueGasFlapSwitch = true; + Double inputBoardTemperature = 20.20; + Integer inputCurrentStage = 12; + Integer statusMainState = 42; + Integer statusSubState = 43; + Integer statusWifiStrength = 100; + Boolean parameterEcoModePossible = true; + Integer parameterFabricationNumber = 12323; + Integer parameterStoveTypeNumber = 46; + Integer parameterLanguageNumber = 2; + Integer parameterVersionMainBoard = 5; + Integer parameterVersionTft = 6; + Integer parameterVersionWifi = 7; + Integer parameterVersionMainBoardBootLoader = 8; + Integer parameterVersionTftBootLoader = 9; + Integer parameterVersionWifiBootLoader = 10; + Integer parameterVersionMainBoardSub = 11; + Integer inputTargetStagePid = 12; + Integer inputCurrentStagePid = 13; + Integer parameterVersionTftSub = 14; + Integer parameterVersionWifiSub = 15; + Integer parameterRuntimePellets = 16; + Integer parameterRuntimeLogs = 17; + Integer parameterFeedRateTotal = 18; + Integer parameterFeedRateService = 19; + Integer parameterServiceCountdownKg = 20; + Integer parameterServiceCountdownTime = 21; + Integer parameterIgnitionCount = 22; + Integer parameterOnOffCycleCount = 23; + Integer parameterFlameSensorOffset = 24; + Integer parameterPressureSensorOffset = 25; + Integer parameterKgTillCleaning = 26; + Integer parameterCleanIntervalBig = 27; + Integer parameterIdFanTuning = 28; + Integer parameterSpiralMotorsTuning = 29; + Boolean statusFrostStarted = true; + Boolean statusHeatingTimesNotProgrammed = true; + Double inputRoomTemperature = 22.4; + Sensors rikaFirenetSensors = + Sensors.builder() + .inputRoomTemperature(inputRoomTemperature) + .inputFlameTemperature(inputFlameTemperature) + .inputBakeTemperature(inputBakeTemperature) + .statusError(statusError) + .statusSubError(statusSubError) + .statusWarning(statusWarning) + .statusService(statusService) + .outputDischargeMotor(outputDischargeMotor) + .outputDischargeCurrent(outputDischargeCurrent) + .outputIdFan(outputIdFan) + .outputIdFanTarget(outputIdFanTarget) + .outputInsertionMotor(outputInsertionMotor) + .outputInsertionCurrent(outputInsertionCurrent) + .outputAirFlaps(outputAirFlaps) + .outputAirFlapsTargetPosition(outputAirFlapsTargetPosition) + .outputBurnBackFlapMagnet(outputBurnBackFlapMagnet) + .outputGridMotor(outputGridMotor) + .outputIgnition(outputIgnition) + .inputUpperTemperatureLimiter(inputUpperTemperatureLimiter) + .inputPressureSwitch(inputPressureSwitch) + .inputPressureSensor(inputPressureSensor) + .inputGridContact(inputGridContact) + .inputDoor(inputDoor) + .inputCover(inputCover) + .inputExternalRequest(inputExternalRequest) + .inputBurnBackFlapSwitch(inputBurnBackFlapSwitch) + .inputFlueGasFlapSwitch(inputFlueGasFlapSwitch) + .inputBoardTemperature(inputBoardTemperature) + .inputCurrentStage(inputCurrentStage) + .statusMainState(statusMainState) + .statusSubState(statusSubState) + .statusWifiStrength(statusWifiStrength) + .parameterEcoModePossible(parameterEcoModePossible) + .parameterFabricationNumber(parameterFabricationNumber) + .parameterStoveTypeNumber(parameterStoveTypeNumber) + .parameterLanguageNumber(parameterLanguageNumber) + .parameterVersionMainBoard(parameterVersionMainBoard) + .parameterVersionTft(parameterVersionTft) + .parameterVersionWifi(parameterVersionWifi) + .parameterVersionMainBoardBootLoader(parameterVersionMainBoardBootLoader) + .parameterVersionTftBootLoader(parameterVersionTftBootLoader) + .parameterVersionWifiBootLoader(parameterVersionWifiBootLoader) + .parameterVersionMainBoardSub(parameterVersionMainBoardSub) + .inputTargetStagePid(inputTargetStagePid) + .inputCurrentStagePid(inputCurrentStagePid) + .parameterVersionTftSub(parameterVersionTftSub) + .parameterVersionWifiSub(parameterVersionWifiSub) + .parameterRuntimePellets(parameterRuntimePellets) + .parameterRuntimeLogs(parameterRuntimeLogs) + .parameterFeedRateTotal(parameterFeedRateTotal) + .parameterFeedRateService(parameterFeedRateService) + .parameterServiceCountdownKg(parameterServiceCountdownKg) + .parameterServiceCountdownTime(parameterServiceCountdownTime) + .parameterIgnitionCount(parameterIgnitionCount) + .parameterOnOffCycleCount(parameterOnOffCycleCount) + .parameterFlameSensorOffset(parameterFlameSensorOffset) + .parameterPressureSensorOffset(parameterPressureSensorOffset) + .parameterKgTillCleaning(parameterKgTillCleaning) + .parameterCleanIntervalBig(parameterCleanIntervalBig) + .parameterIdFanTuning(parameterIdFanTuning) + .parameterSpiralMotorsTuning(parameterSpiralMotorsTuning) + .statusFrostStarted(statusFrostStarted) + .statusHeatingTimesNotProgrammed(statusHeatingTimesNotProgrammed) + .parameterErrorCount0(parameterErrorCount0) + .parameterErrorCount1(parameterErrorCount1) + .parameterErrorCount2(parameterErrorCount2) + .parameterErrorCount3(parameterErrorCount3) + .parameterErrorCount4(parameterErrorCount4) + .parameterErrorCount5(parameterErrorCount5) + .parameterErrorCount6(parameterErrorCount6) + .parameterErrorCount7(parameterErrorCount7) + .parameterErrorCount8(parameterErrorCount8) + .parameterErrorCount9(parameterErrorCount9) + .parameterErrorCount10(parameterErrorCount10) + .parameterErrorCount11(parameterErrorCount11) + .parameterErrorCount12(parameterErrorCount12) + .parameterErrorCount13(parameterErrorCount13) + .parameterErrorCount14(parameterErrorCount14) + .parameterErrorCount15(parameterErrorCount15) + .parameterErrorCount16(parameterErrorCount16) + .parameterErrorCount17(parameterErrorCount17) + .parameterErrorCount18(parameterErrorCount18) + .parameterErrorCount19(parameterErrorCount19) + .parameterDebug0(parameterDebug0) + .parameterDebug1(parameterDebug1) + .parameterDebug2(parameterDebug2) + .parameterDebug3(parameterDebug3) + .parameterDebug4(parameterDebug4) + .build(); + + // WHEN + var apiSensors = mapper.toApiSensors(rikaFirenetSensors); + + // THEN + assertThat(apiSensors.getParametersDebug()).isNotEmpty(); + assertThat(apiSensors.getParametersDebug()).hasSize(5); + assertThat(apiSensors.getParametersDebug().get(0).getValue()) + .isEqualTo(rikaFirenetSensors.getParameterDebug0()); + assertThat(apiSensors.getParametersDebug().get(1).getValue()) + .isEqualTo(rikaFirenetSensors.getParameterDebug1()); + assertThat(apiSensors.getParametersDebug().get(2).getValue()) + .isEqualTo(rikaFirenetSensors.getParameterDebug2()); + assertThat(apiSensors.getParametersDebug().get(3).getValue()) + .isEqualTo(rikaFirenetSensors.getParameterDebug3()); + assertThat(apiSensors.getParametersDebug().get(4).getValue()) + .isEqualTo(rikaFirenetSensors.getParameterDebug4()); + } + + @Test + void toApiSensorsShouldFillParametersErrorCountCollectionExtraFieldProperly() { + + // GIVEN + Integer inputFlameTemperature = 200; + Integer inputBakeTemperature = 300; + Integer statusError = 1; + Integer statusSubError = 2; + Integer statusWarning = 3; + Integer statusService = 4; + Integer outputDischargeMotor = 5; + Integer outputDischargeCurrent = 6; + Integer outputIdFan = 7; + Integer outputIdFanTarget = 8; + Integer outputInsertionMotor = 9; + Integer outputInsertionCurrent = 10; + Integer outputAirFlaps = 11; + Integer outputAirFlapsTargetPosition = 12; + Boolean outputBurnBackFlapMagnet = true; + Boolean outputGridMotor = false; + Boolean outputIgnition = true; + Boolean inputUpperTemperatureLimiter = true; + Boolean inputPressureSwitch = true; + Integer inputPressureSensor = 13; + Boolean inputGridContact = true; + Boolean inputDoor = true; + Boolean inputCover = true; + Boolean inputExternalRequest = true; + Boolean inputBurnBackFlapSwitch = true; + Integer parameterDebug0 = 44; + Integer parameterDebug1 = 45; + Integer parameterDebug2 = 46; + Integer parameterDebug3 = 47; + Integer parameterDebug4 = 48; + Integer parameterErrorCount0 = 30; + Integer parameterErrorCount1 = 31; + Integer parameterErrorCount2 = 32; + Integer parameterErrorCount3 = 33; + Integer parameterErrorCount4 = 34; + Integer parameterErrorCount5 = 35; + Integer parameterErrorCount6 = 36; + Integer parameterErrorCount7 = 37; + Integer parameterErrorCount8 = 38; + Integer parameterErrorCount9 = 39; + Integer parameterErrorCount10 = 40; + Integer parameterErrorCount11 = 41; + Integer parameterErrorCount12 = 42; + Integer parameterErrorCount13 = 43; + Integer parameterErrorCount14 = 44; + Integer parameterErrorCount15 = 45; + Integer parameterErrorCount16 = 46; + Integer parameterErrorCount17 = 47; + Integer parameterErrorCount18 = 48; + Integer parameterErrorCount19 = 49; + Boolean inputFlueGasFlapSwitch = true; + Double inputBoardTemperature = 20.20; + Integer inputCurrentStage = 12; + Integer statusMainState = 42; + Integer statusSubState = 43; + Integer statusWifiStrength = 100; + Boolean parameterEcoModePossible = true; + Integer parameterFabricationNumber = 12323; + Integer parameterStoveTypeNumber = 46; + Integer parameterLanguageNumber = 2; + Integer parameterVersionMainBoard = 5; + Integer parameterVersionTft = 6; + Integer parameterVersionWifi = 7; + Integer parameterVersionMainBoardBootLoader = 8; + Integer parameterVersionTftBootLoader = 9; + Integer parameterVersionWifiBootLoader = 10; + Integer parameterVersionMainBoardSub = 11; + Integer inputTargetStagePid = 12; + Integer inputCurrentStagePid = 13; + Integer parameterVersionTftSub = 14; + Integer parameterVersionWifiSub = 15; + Integer parameterRuntimePellets = 16; + Integer parameterRuntimeLogs = 17; + Integer parameterFeedRateTotal = 18; + Integer parameterFeedRateService = 19; + Integer parameterServiceCountdownKg = 20; + Integer parameterServiceCountdownTime = 21; + Integer parameterIgnitionCount = 22; + Integer parameterOnOffCycleCount = 23; + Integer parameterFlameSensorOffset = 24; + Integer parameterPressureSensorOffset = 25; + Integer parameterKgTillCleaning = 26; + Integer parameterCleanIntervalBig = 27; + Integer parameterIdFanTuning = 28; + Integer parameterSpiralMotorsTuning = 29; + Boolean statusFrostStarted = true; + Boolean statusHeatingTimesNotProgrammed = true; + Double inputRoomTemperature = 22.4; + Sensors rikaFirenetSensors = + Sensors.builder() + .inputRoomTemperature(inputRoomTemperature) + .inputFlameTemperature(inputFlameTemperature) + .inputBakeTemperature(inputBakeTemperature) + .statusError(statusError) + .statusSubError(statusSubError) + .statusWarning(statusWarning) + .statusService(statusService) + .outputDischargeMotor(outputDischargeMotor) + .outputDischargeCurrent(outputDischargeCurrent) + .outputIdFan(outputIdFan) + .outputIdFanTarget(outputIdFanTarget) + .outputInsertionMotor(outputInsertionMotor) + .outputInsertionCurrent(outputInsertionCurrent) + .outputAirFlaps(outputAirFlaps) + .outputAirFlapsTargetPosition(outputAirFlapsTargetPosition) + .outputBurnBackFlapMagnet(outputBurnBackFlapMagnet) + .outputGridMotor(outputGridMotor) + .outputIgnition(outputIgnition) + .inputUpperTemperatureLimiter(inputUpperTemperatureLimiter) + .inputPressureSwitch(inputPressureSwitch) + .inputPressureSensor(inputPressureSensor) + .inputGridContact(inputGridContact) + .inputDoor(inputDoor) + .inputCover(inputCover) + .inputExternalRequest(inputExternalRequest) + .inputBurnBackFlapSwitch(inputBurnBackFlapSwitch) + .inputFlueGasFlapSwitch(inputFlueGasFlapSwitch) + .inputBoardTemperature(inputBoardTemperature) + .inputCurrentStage(inputCurrentStage) + .statusMainState(statusMainState) + .statusSubState(statusSubState) + .statusWifiStrength(statusWifiStrength) + .parameterEcoModePossible(parameterEcoModePossible) + .parameterFabricationNumber(parameterFabricationNumber) + .parameterStoveTypeNumber(parameterStoveTypeNumber) + .parameterLanguageNumber(parameterLanguageNumber) + .parameterVersionMainBoard(parameterVersionMainBoard) + .parameterVersionTft(parameterVersionTft) + .parameterVersionWifi(parameterVersionWifi) + .parameterVersionMainBoardBootLoader(parameterVersionMainBoardBootLoader) + .parameterVersionTftBootLoader(parameterVersionTftBootLoader) + .parameterVersionWifiBootLoader(parameterVersionWifiBootLoader) + .parameterVersionMainBoardSub(parameterVersionMainBoardSub) + .inputTargetStagePid(inputTargetStagePid) + .inputCurrentStagePid(inputCurrentStagePid) + .parameterVersionTftSub(parameterVersionTftSub) + .parameterVersionWifiSub(parameterVersionWifiSub) + .parameterRuntimePellets(parameterRuntimePellets) + .parameterRuntimeLogs(parameterRuntimeLogs) + .parameterFeedRateTotal(parameterFeedRateTotal) + .parameterFeedRateService(parameterFeedRateService) + .parameterServiceCountdownKg(parameterServiceCountdownKg) + .parameterServiceCountdownTime(parameterServiceCountdownTime) + .parameterIgnitionCount(parameterIgnitionCount) + .parameterOnOffCycleCount(parameterOnOffCycleCount) + .parameterFlameSensorOffset(parameterFlameSensorOffset) + .parameterPressureSensorOffset(parameterPressureSensorOffset) + .parameterKgTillCleaning(parameterKgTillCleaning) + .parameterCleanIntervalBig(parameterCleanIntervalBig) + .parameterIdFanTuning(parameterIdFanTuning) + .parameterSpiralMotorsTuning(parameterSpiralMotorsTuning) + .statusFrostStarted(statusFrostStarted) + .statusHeatingTimesNotProgrammed(statusHeatingTimesNotProgrammed) + .parameterErrorCount0(parameterErrorCount0) + .parameterErrorCount1(parameterErrorCount1) + .parameterErrorCount2(parameterErrorCount2) + .parameterErrorCount3(parameterErrorCount3) + .parameterErrorCount4(parameterErrorCount4) + .parameterErrorCount5(parameterErrorCount5) + .parameterErrorCount6(parameterErrorCount6) + .parameterErrorCount7(parameterErrorCount7) + .parameterErrorCount8(parameterErrorCount8) + .parameterErrorCount9(parameterErrorCount9) + .parameterErrorCount10(parameterErrorCount10) + .parameterErrorCount11(parameterErrorCount11) + .parameterErrorCount12(parameterErrorCount12) + .parameterErrorCount13(parameterErrorCount13) + .parameterErrorCount14(parameterErrorCount14) + .parameterErrorCount15(parameterErrorCount15) + .parameterErrorCount16(parameterErrorCount16) + .parameterErrorCount17(parameterErrorCount17) + .parameterErrorCount18(parameterErrorCount18) + .parameterErrorCount19(parameterErrorCount19) + .parameterDebug0(parameterDebug0) + .parameterDebug1(parameterDebug1) + .parameterDebug2(parameterDebug2) + .parameterDebug3(parameterDebug3) + .parameterDebug4(parameterDebug4) + .build(); + + // WHEN + var apiSensors = mapper.toApiSensors(rikaFirenetSensors); + + // THEN + assertThat(apiSensors.getParametersErrorCount()).isNotEmpty(); + assertThat(apiSensors.getParametersErrorCount()).hasSize(20); + assertThat(apiSensors.getParametersErrorCount().get(0).getValue()) + .isEqualTo(rikaFirenetSensors.getParameterErrorCount0()); + assertThat(apiSensors.getParametersErrorCount().get(1).getValue()) + .isEqualTo(rikaFirenetSensors.getParameterErrorCount1()); + assertThat(apiSensors.getParametersErrorCount().get(2).getValue()) + .isEqualTo(rikaFirenetSensors.getParameterErrorCount2()); + assertThat(apiSensors.getParametersErrorCount().get(3).getValue()) + .isEqualTo(rikaFirenetSensors.getParameterErrorCount3()); + assertThat(apiSensors.getParametersErrorCount().get(4).getValue()) + .isEqualTo(rikaFirenetSensors.getParameterErrorCount4()); + assertThat(apiSensors.getParametersErrorCount().get(5).getValue()) + .isEqualTo(rikaFirenetSensors.getParameterErrorCount5()); + assertThat(apiSensors.getParametersErrorCount().get(6).getValue()) + .isEqualTo(rikaFirenetSensors.getParameterErrorCount6()); + assertThat(apiSensors.getParametersErrorCount().get(7).getValue()) + .isEqualTo(rikaFirenetSensors.getParameterErrorCount7()); + assertThat(apiSensors.getParametersErrorCount().get(8).getValue()) + .isEqualTo(rikaFirenetSensors.getParameterErrorCount8()); + assertThat(apiSensors.getParametersErrorCount().get(9).getValue()) + .isEqualTo(rikaFirenetSensors.getParameterErrorCount9()); + assertThat(apiSensors.getParametersErrorCount().get(10).getValue()) + .isEqualTo(rikaFirenetSensors.getParameterErrorCount10()); + assertThat(apiSensors.getParametersErrorCount().get(11).getValue()) + .isEqualTo(rikaFirenetSensors.getParameterErrorCount11()); + assertThat(apiSensors.getParametersErrorCount().get(12).getValue()) + .isEqualTo(rikaFirenetSensors.getParameterErrorCount12()); + assertThat(apiSensors.getParametersErrorCount().get(13).getValue()) + .isEqualTo(rikaFirenetSensors.getParameterErrorCount13()); + assertThat(apiSensors.getParametersErrorCount().get(14).getValue()) + .isEqualTo(rikaFirenetSensors.getParameterErrorCount14()); + assertThat(apiSensors.getParametersErrorCount().get(15).getValue()) + .isEqualTo(rikaFirenetSensors.getParameterErrorCount15()); + assertThat(apiSensors.getParametersErrorCount().get(16).getValue()) + .isEqualTo(rikaFirenetSensors.getParameterErrorCount16()); + assertThat(apiSensors.getParametersErrorCount().get(17).getValue()) + .isEqualTo(rikaFirenetSensors.getParameterErrorCount17()); + assertThat(apiSensors.getParametersErrorCount().get(18).getValue()) + .isEqualTo(rikaFirenetSensors.getParameterErrorCount18()); + assertThat(apiSensors.getParametersErrorCount().get(19).getValue()) + .isEqualTo(rikaFirenetSensors.getParameterErrorCount19()); + } +} diff --git a/plugins-internal/src/test/java/dev/cookiecode/rika2mqtt/plugins/internal/v1/mapper/StoveIdMapperTest.java b/plugins-internal/src/test/java/dev/cookiecode/rika2mqtt/plugins/internal/v1/mapper/StoveIdMapperTest.java new file mode 100644 index 00000000..8e632eeb --- /dev/null +++ b/plugins-internal/src/test/java/dev/cookiecode/rika2mqtt/plugins/internal/v1/mapper/StoveIdMapperTest.java @@ -0,0 +1,63 @@ +/* + * The MIT License + * Copyright © 2022 Sebastien Vermeille + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package dev.cookiecode.rika2mqtt.plugins.internal.v1.mapper; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.Assert.assertThrows; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +/** + * Test class + * + * @author Sebastien Vermeille + */ +@SpringBootTest(classes = {StoveIdMapperImpl.class}) +class StoveIdMapperTest { + + @Autowired private StoveIdMapper mapper; + + @Test + void mapShouldThrowAnNullPointerExceptionGivenNoParameterIsPassed() { + assertThrows( + NullPointerException.class, + () -> { + // WHEN + mapper.map(null); + }); + } + + @Test + void mapShouldConvertSuccessfullyGivenAStoveIdLongValueIsProvided() { + // GIVEN + final var stoveIdLongValue = 12321312L; + + // WHEN + final var result = mapper.map(stoveIdLongValue); + + // THEN + assertThat(result.id()).isEqualTo(stoveIdLongValue); + } +} diff --git a/plugins-internal/src/test/java/dev/cookiecode/rika2mqtt/plugins/internal/v1/mapper/StoveStatusMapperTest.java b/plugins-internal/src/test/java/dev/cookiecode/rika2mqtt/plugins/internal/v1/mapper/StoveStatusMapperTest.java new file mode 100644 index 00000000..1a00e4c4 --- /dev/null +++ b/plugins-internal/src/test/java/dev/cookiecode/rika2mqtt/plugins/internal/v1/mapper/StoveStatusMapperTest.java @@ -0,0 +1,82 @@ +/* + * The MIT License + * Copyright © 2022 Sebastien Vermeille + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package dev.cookiecode.rika2mqtt.plugins.internal.v1.mapper; + +import static org.assertj.core.api.Assertions.assertThat; + +import dev.cookiecode.rika2mqtt.plugins.internal.v1.mapper.helper.ControlsMapperEmptyImpl; +import dev.cookiecode.rika2mqtt.plugins.internal.v1.mapper.helper.SensorsMapperEmptyImpl; +import dev.cookiecode.rika2mqtt.rika.firenet.model.Controls; +import dev.cookiecode.rika2mqtt.rika.firenet.model.Sensors; +import dev.cookiecode.rika2mqtt.rika.firenet.model.StoveStatus; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +/** + * Test class + * + * @author Sebastien Vermeille + */ +@SpringBootTest( + classes = { + StoveStatusMapperImpl.class, + SensorsMapperEmptyImpl.class, + ControlsMapperEmptyImpl.class, + StoveIdMapperImpl.class + }) +class StoveStatusMapperTest { + + @Autowired private StoveStatusMapper mapper; + + @Test + void toApiStoveStatusShouldFillAllStoveProperties() { + + // GIVEN + String name = ""; + Long stoveId = 12L; + Long lastConfirmedRevision = 12321312L; + String oem = ""; + Long lastSeenMinutes = 0L; + String stoveType = ""; + Sensors sensors = Sensors.builder().build(); + Controls controls = Controls.builder().build(); + StoveStatus rikaFirenetStatus = + StoveStatus.builder() + .name(name) + .stoveId(stoveId) + .lastConfirmedRevision(lastConfirmedRevision) + .oem(oem) + .lastSeenMinutes(lastSeenMinutes) + .stoveType(stoveType) + .sensors(sensors) + .controls(controls) + .build(); + + // WHEN + final var apiStatus = mapper.toApiStoveStatus(rikaFirenetStatus); + + // THEN + assertThat(apiStatus).hasNoNullFieldsOrProperties(); + } +} diff --git a/plugins-internal/src/test/java/dev/cookiecode/rika2mqtt/plugins/internal/v1/mapper/TimeRangeMapperTest.java b/plugins-internal/src/test/java/dev/cookiecode/rika2mqtt/plugins/internal/v1/mapper/TimeRangeMapperTest.java new file mode 100644 index 00000000..4917eba0 --- /dev/null +++ b/plugins-internal/src/test/java/dev/cookiecode/rika2mqtt/plugins/internal/v1/mapper/TimeRangeMapperTest.java @@ -0,0 +1,77 @@ +/* + * The MIT License + * Copyright © 2022 Sebastien Vermeille + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package dev.cookiecode.rika2mqtt.plugins.internal.v1.mapper; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.Assert.assertThrows; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +/** + * Test class + * + * @author Sebastien Vermeille + */ +@SpringBootTest(classes = {TimeRangeMapperImpl.class}) +class TimeRangeMapperTest { + + @Autowired private TimeRangeMapper mapper; + + @Test + void mapShouldThrowAnNullPointerExceptionGivenNoParameterIsPassed() { + assertThrows( + NullPointerException.class, + () -> { + // WHEN + mapper.map(null); + }); + } + + @Test + void mapShouldConvertSuccessfullyGivenAValidRikaTimeRangeInputGivenDoubleDigitTimes() { + + // GIVEN + final var rikaTimeRange = "13302215"; + + // WHEN + final var result = mapper.map(rikaTimeRange); + + // THEN + assertThat(result.toString()).isEqualTo("13:30 - 22:15"); + } + + @Test + void mapShouldConvertSuccessfullyGivenAValidRikaTimeRangeInputGivenSingleDigitTimes() { + + // GIVEN + final var rikaTimeRange = "13012205"; + + // WHEN + final var result = mapper.map(rikaTimeRange); + + // THEN + assertThat(result.toString()).isEqualTo("13:01 - 22:05"); + } +} diff --git a/plugins-internal/src/test/java/dev/cookiecode/rika2mqtt/plugins/internal/v1/mapper/helper/ControlsMapperEmptyImpl.java b/plugins-internal/src/test/java/dev/cookiecode/rika2mqtt/plugins/internal/v1/mapper/helper/ControlsMapperEmptyImpl.java new file mode 100644 index 00000000..1c95a701 --- /dev/null +++ b/plugins-internal/src/test/java/dev/cookiecode/rika2mqtt/plugins/internal/v1/mapper/helper/ControlsMapperEmptyImpl.java @@ -0,0 +1,44 @@ +/* + * The MIT License + * Copyright © 2022 Sebastien Vermeille + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package dev.cookiecode.rika2mqtt.plugins.internal.v1.mapper.helper; + +import dev.cookiecode.rika2mqtt.plugins.api.v1.model.Controls; +import dev.cookiecode.rika2mqtt.plugins.internal.v1.mapper.ControlsMapper; +import lombok.NonNull; +import org.springframework.stereotype.Component; + +/** + * Test class This class is intentionally very empty (It allows to keep tests valuable and not + * repeating themselves) + * + * @author Sebastien Vermeille + */ +@Component +public class ControlsMapperEmptyImpl implements ControlsMapper { + + @Override + public Controls toApiControls( + dev.cookiecode.rika2mqtt.rika.firenet.model.@NonNull Controls controls) { + return Controls.builder().build(); + } +} diff --git a/plugins-internal/src/test/java/dev/cookiecode/rika2mqtt/plugins/internal/v1/mapper/helper/SensorsMapperEmptyImpl.java b/plugins-internal/src/test/java/dev/cookiecode/rika2mqtt/plugins/internal/v1/mapper/helper/SensorsMapperEmptyImpl.java new file mode 100644 index 00000000..dc1ec26b --- /dev/null +++ b/plugins-internal/src/test/java/dev/cookiecode/rika2mqtt/plugins/internal/v1/mapper/helper/SensorsMapperEmptyImpl.java @@ -0,0 +1,44 @@ +/* + * The MIT License + * Copyright © 2022 Sebastien Vermeille + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package dev.cookiecode.rika2mqtt.plugins.internal.v1.mapper.helper; + +import dev.cookiecode.rika2mqtt.plugins.api.v1.model.Sensors; +import dev.cookiecode.rika2mqtt.plugins.internal.v1.mapper.SensorsMapper; +import lombok.NonNull; +import org.springframework.stereotype.Component; + +/** + * Test class This class is intentionally very empty (It allows to keep tests valuable and not + * repeating themselves) + * + * @author Sebastien Vermeille + */ +@Component +public class SensorsMapperEmptyImpl implements SensorsMapper { + + @Override + public Sensors toApiSensors( + dev.cookiecode.rika2mqtt.rika.firenet.model.@NonNull Sensors sensors) { + return Sensors.builder().build(); + } +} diff --git a/plugins-internal/src/test/java/dev/cookiecode/rika2mqtt/plugins/internal/v1/pf4j/DummyConfigurablePlugin.java b/plugins-internal/src/test/java/dev/cookiecode/rika2mqtt/plugins/internal/v1/pf4j/DummyConfigurablePlugin.java new file mode 100644 index 00000000..5c363475 --- /dev/null +++ b/plugins-internal/src/test/java/dev/cookiecode/rika2mqtt/plugins/internal/v1/pf4j/DummyConfigurablePlugin.java @@ -0,0 +1,54 @@ +/* + * The MIT License + * Copyright © 2022 Sebastien Vermeille + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package dev.cookiecode.rika2mqtt.plugins.internal.v1.pf4j; + +import dev.cookiecode.rika2mqtt.plugins.api.v1.Rika2MqttPlugin; +import dev.cookiecode.rika2mqtt.plugins.api.v1.annotations.ConfigurablePlugin; +import dev.cookiecode.rika2mqtt.plugins.api.v1.model.plugins.OptionalPluginConfigurationParameter; +import dev.cookiecode.rika2mqtt.plugins.api.v1.model.plugins.PluginConfigurationParameter; +import dev.cookiecode.rika2mqtt.plugins.api.v1.model.plugins.RequiredPluginConfigurationParameter; +import java.util.List; + +/** + * Test class An example of plugin not implementing ConfigurablePlugin used for testing a behaviour + * + * @author Sebastien Vermeille + */ +public class DummyConfigurablePlugin extends Rika2MqttPlugin implements ConfigurablePlugin { + @Override + public List declarePluginConfigurationParameters() { + return List.of( + OptionalPluginConfigurationParameter.builder() + .withParameterName("language") + .withDescription("define the language to use") + .withValueType(String.class) + .withDefaultValue("en") + .withExample("en,fr,de") + .build(), + RequiredPluginConfigurationParameter.builder() + .withParameterName("password") + .withDescription("some password") + .withValueType(String.class) + .build()); + } +} diff --git a/plugins-internal/src/test/java/dev/cookiecode/rika2mqtt/plugins/internal/v1/pf4j/NotConfigurablePlugin.java b/plugins-internal/src/test/java/dev/cookiecode/rika2mqtt/plugins/internal/v1/pf4j/NotConfigurablePlugin.java new file mode 100644 index 00000000..323268f1 --- /dev/null +++ b/plugins-internal/src/test/java/dev/cookiecode/rika2mqtt/plugins/internal/v1/pf4j/NotConfigurablePlugin.java @@ -0,0 +1,32 @@ +/* + * The MIT License + * Copyright © 2022 Sebastien Vermeille + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package dev.cookiecode.rika2mqtt.plugins.internal.v1.pf4j; + +import dev.cookiecode.rika2mqtt.plugins.api.v1.Rika2MqttPlugin; + +/** + * Test class An example of plugin not implementing ConfigurablePlugin used for testing a behaviour + * + * @author Sebastien Vermeille + */ +public class NotConfigurablePlugin extends Rika2MqttPlugin {} diff --git a/plugins-internal/src/test/java/dev/cookiecode/rika2mqtt/plugins/internal/v1/pf4j/Pf4jPluginManagerConfigTest.java b/plugins-internal/src/test/java/dev/cookiecode/rika2mqtt/plugins/internal/v1/pf4j/Pf4jPluginManagerConfigTest.java new file mode 100644 index 00000000..ee49f6ae --- /dev/null +++ b/plugins-internal/src/test/java/dev/cookiecode/rika2mqtt/plugins/internal/v1/pf4j/Pf4jPluginManagerConfigTest.java @@ -0,0 +1,46 @@ +/* + * The MIT License + * Copyright © 2022 Sebastien Vermeille + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package dev.cookiecode.rika2mqtt.plugins.internal.v1.pf4j; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.Test; +import org.pf4j.PluginManager; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +/** + * Test class + * + * @author Sebastien Vermeille + */ +@SpringBootTest(classes = {Pf4jPluginManagerConfig.class}) +class Pf4jPluginManagerConfigTest { + + @Autowired private PluginManager pluginManager; + + @Test + void pluginManagerHasTheCorrectInstance() { + assertThat(pluginManager).isInstanceOf(Rika2MqttPluginManager.class); + } +} diff --git a/plugins-internal/src/test/java/dev/cookiecode/rika2mqtt/plugins/internal/v1/pf4j/Rika2MqttPluginManagerTest.java b/plugins-internal/src/test/java/dev/cookiecode/rika2mqtt/plugins/internal/v1/pf4j/Rika2MqttPluginManagerTest.java new file mode 100644 index 00000000..0ba186e5 --- /dev/null +++ b/plugins-internal/src/test/java/dev/cookiecode/rika2mqtt/plugins/internal/v1/pf4j/Rika2MqttPluginManagerTest.java @@ -0,0 +1,532 @@ +/* + * The MIT License + * Copyright © 2022 Sebastien Vermeille + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package dev.cookiecode.rika2mqtt.plugins.internal.v1.pf4j; + +import static dev.cookiecode.rika2mqtt.plugins.internal.v1.pf4j.Rika2MqttPluginManager.PLUGIN_ENV_NAME_PREFIX; +import static dev.cookiecode.rika2mqtt.plugins.internal.v1.pf4j.Rika2MqttPluginManager.PLUGIN_STATES_PREVENTING_START; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.Assert.assertThrows; +import static org.mockito.Mockito.*; +import static org.pf4j.PluginState.*; + +import dev.cookiecode.rika2mqtt.plugins.api.v1.PluginConfiguration; +import dev.cookiecode.rika2mqtt.plugins.api.v1.exceptions.InvalidPluginConfigurationException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import lombok.NonNull; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Spy; +import org.mockito.junit.jupiter.MockitoExtension; +import org.pf4j.PluginDescriptor; +import org.pf4j.PluginWrapper; + +/** + * Test class + * + * @author Sebastien Vermeille + */ +@ExtendWith(MockitoExtension.class) +class Rika2MqttPluginManagerTest { + + private final Map mockedAnswers = new HashMap<>(); + + @InjectMocks @Spy private Rika2MqttPluginManager rika2MqttPluginManager; + + @BeforeEach + void beforeEach() { + mockedAnswers.clear(); + } + + @Test + void startPluginsShouldInvokeHandlePluginsMethodOnlyToOnePluginGivenOnePluginIsDisabled() { + + // GIVEN + final var createdPlugin = mock(PluginWrapper.class); + when(createdPlugin.getPluginState()).thenReturn(CREATED); + final var disabledPlugin = mock(PluginWrapper.class); + when(disabledPlugin.getPluginState()).thenReturn(DISABLED); + + doReturn(List.of(createdPlugin, disabledPlugin)) + .when(rika2MqttPluginManager) + .getResolvedPlugins(); + doNothing().when(rika2MqttPluginManager).handlePlugin(createdPlugin); + + // WHEN + rika2MqttPluginManager.startPlugins(); + + // THEN + verify(rika2MqttPluginManager, times(1)).handlePlugin(createdPlugin); + verify(rika2MqttPluginManager, never()).handlePlugin(disabledPlugin); + } + + @Test + void shouldStartPluginShouldReturnFalseGivenPluginIsInDisabledState() { + + // GIVEN + final var state = DISABLED; + + // WHEN + final var shouldStart = rika2MqttPluginManager.shouldStartPlugin(state); + + // THEN + assertThat(shouldStart).isFalse(); + } + + @Test + void shouldStartPluginShouldReturnFalseGivenPluginIsInStartedState() { + + // GIVEN + final var state = STARTED; + + // WHEN + final var shouldStart = rika2MqttPluginManager.shouldStartPlugin(state); + + // THEN + assertThat(shouldStart).isFalse(); + } + + @Test + void shouldStartPluginShouldReturnTrueGivenPluginIsInStoppedState() { + + // GIVEN + final var state = STOPPED; + + // WHEN + final var shouldStart = rika2MqttPluginManager.shouldStartPlugin(state); + + // THEN + assertThat(shouldStart).isTrue(); + } + + @Test + void shouldStartPluginShouldReturnTrueGivenPluginIsInCreatedState() { + + // GIVEN + final var state = CREATED; + + // WHEN + final var shouldStart = rika2MqttPluginManager.shouldStartPlugin(state); + + // THEN + assertThat(shouldStart).isTrue(); + } + + @Test + void shouldStartPluginShouldReturnFalseForAllEnumeratedPluginStates() { + for (final var state : PLUGIN_STATES_PREVENTING_START) { + // GIVEN + // a state which is supposed to prevent the plugin to start + + // WHEN + final var shouldStart = rika2MqttPluginManager.shouldStartPlugin(state); + + // THEN + assertThat(shouldStart).isFalse(); + } + } + + @Test + void + loadPluginConfigurationShouldReturnAPluginConfigurationWithEmptyParametersGivenThePluginIsNotAConfigurablePlugin() { + // GIVEN + final var notConfigurablePlugin = new NotConfigurablePlugin(); + + // WHEN + final var pluginConfiguration = + rika2MqttPluginManager.loadPluginConfiguration(notConfigurablePlugin); + + // THEN + assertThat(pluginConfiguration.getParameters()).isEmpty(); + } + + @Test + void + loadPluginConfigurationShouldReturnAPluginConfigurationIncludingParametersGivenThePluginIsAConfigurablePlugin() { + // GIVEN + final var configurablePlugin = new DummyConfigurablePlugin(); + + // WHEN + final var pluginConfiguration = + rika2MqttPluginManager.loadPluginConfiguration(configurablePlugin); + + // THEN + assertThat(pluginConfiguration.getParameters()).isNotEmpty(); + } + + @Test + void + loadPluginConfigurationShouldReturnAPluginConfigurationIncludingParametersGivenThePluginDeclareAnOptionalParameterWithDefaultValueAndNoEnvIsGiven() { + // GIVEN + final var configurablePlugin = new DummyConfigurablePlugin(); + + // WHEN + final var pluginConfiguration = + rika2MqttPluginManager.loadPluginConfiguration(configurablePlugin); + + // THEN + assertThat(pluginConfiguration.getOptionalParameter("language")).isEqualTo(Optional.of("en")); + } + + @Test + void + loadPluginConfigurationShouldReturnAPluginConfigurationIncludingParametersGivenThePluginDeclareAnOptionalParameterWithDefaultValueAndEnvIsGiven() { + // GIVEN + final var configurablePlugin = new DummyConfigurablePlugin(); + final var language = "fr"; + mockPluginEnv("language", language); + + // WHEN + final var pluginConfiguration = + rika2MqttPluginManager.loadPluginConfiguration(configurablePlugin); + + // THEN + assertThat(pluginConfiguration.getOptionalParameter("language")) + .isEqualTo(Optional.of(language)); + } + + @Test + void + loadPluginConfigurationShouldReturnAPluginConfigurationIncludingParametersGivenThePluginDeclareARequiredParameterAndNoEnvIsProvided() { + // GIVEN + final var configurablePlugin = new DummyConfigurablePlugin(); + + // WHEN + final var pluginConfiguration = + rika2MqttPluginManager.loadPluginConfiguration(configurablePlugin); + + // THEN + assertThat(pluginConfiguration.getOptionalParameter("password")) + .as( + "The password parameter is not defined in ENV so it is set to null and then rika2mqtt will perform a validation of the plugin configuration") + .isEmpty(); + } + + @Test + void + loadPluginConfigurationShouldReturnAPluginConfigurationIncludingParametersGivenThePluginDeclareARequiredParameterAndEnvIsProvided() { + // GIVEN + final var configurablePlugin = new DummyConfigurablePlugin(); + final var password = "p4ssw0rd"; + mockPluginEnv("password", password); + + // WHEN + final var pluginConfiguration = + rika2MqttPluginManager.loadPluginConfiguration(configurablePlugin); + + // THEN + assertThat(pluginConfiguration.getParameter("password")).isEqualTo(password); + } + + @Test + void startPluginShouldInvokePluginPreStartMethod() { + + // GIVEN + final var pluginWrapper = mock(PluginWrapper.class); + when(pluginWrapper.getDescriptor()).thenReturn(mock(PluginDescriptor.class)); + final var rikaPlugin = spy(new DummyConfigurablePlugin()); + when(pluginWrapper.getPlugin()).thenReturn(rikaPlugin); + final var pluginConfiguration = mock(PluginConfiguration.class); + + // WHEN + rika2MqttPluginManager.startPlugin(pluginWrapper, pluginConfiguration); + + // THEN + verify(rikaPlugin, times(1)).preStart(pluginConfiguration); + } + + @Test + void startPluginShouldInvokePluginStartMethod() { + + // GIVEN + final var pluginWrapper = mock(PluginWrapper.class); + when(pluginWrapper.getDescriptor()).thenReturn(mock(PluginDescriptor.class)); + final var rikaPlugin = spy(new DummyConfigurablePlugin()); + when(pluginWrapper.getPlugin()).thenReturn(rikaPlugin); + final var pluginConfiguration = mock(PluginConfiguration.class); + + // WHEN + rika2MqttPluginManager.startPlugin(pluginWrapper, pluginConfiguration); + + // THEN + verify(rikaPlugin, times(1)).start(); + } + + @Test + void startPluginShouldUpdatePluginStateWhenInvoked() { + + // GIVEN + final var pluginWrapper = mock(PluginWrapper.class); + when(pluginWrapper.getDescriptor()).thenReturn(mock(PluginDescriptor.class)); + final var rikaPlugin = spy(new DummyConfigurablePlugin()); + when(pluginWrapper.getPlugin()).thenReturn(rikaPlugin); + final var pluginConfiguration = mock(PluginConfiguration.class); + + // WHEN + rika2MqttPluginManager.startPlugin(pluginWrapper, pluginConfiguration); + + // THEN + verify(pluginWrapper, times(1)).setPluginState(STARTED); + verify(pluginWrapper, times(1)).setFailedException(null); + } + + @Test + void handleInvalidPluginConfigurationShouldInvokeStopAndFailPlugin() { + + // GIVEN + final var pluginWrapper = mock(PluginWrapper.class); + final var pluginDescriptor = mock(PluginDescriptor.class); + doReturn(pluginDescriptor).when(pluginWrapper).getDescriptor(); + doNothing().when(rika2MqttPluginManager).stopAndFailPlugin(any(), anyString()); + + // WHEN + rika2MqttPluginManager.handleInvalidPluginConfiguration(pluginWrapper); + + // THEN + verify(rika2MqttPluginManager, times(1)).stopAndFailPlugin(any(), anyString()); + } + + @Test + void handlePluginStartFailureShouldUpdatePluginState() { + + // GIVEN + final var pluginWrapper = mock(PluginWrapper.class); + final var pluginDescriptor = mock(PluginDescriptor.class); + doReturn(pluginDescriptor).when(pluginWrapper).getDescriptor(); + final var exception = mock(Exception.class); + + // WHEN + rika2MqttPluginManager.handlePluginStartFailure(pluginWrapper, exception); + + // THEN + verify(pluginWrapper, times(1)).setPluginState(FAILED); + verify(pluginWrapper, times(1)).setFailedException(exception); + } + + @Test + void stopAndFailPluginShouldUpdatePluginStateAccordingly() { + + // GIVEN + final var pluginWrapper = mock(PluginWrapper.class); + final var pluginId = "plugin-id"; + when(pluginWrapper.getPluginId()).thenReturn(pluginId); + doReturn(null).when(rika2MqttPluginManager).stopPlugin(anyString()); + final var pluginName = "plugin-a"; + + // WHEN + rika2MqttPluginManager.stopAndFailPlugin(pluginWrapper, pluginName); + + // THEN + verify(pluginWrapper, times(1)).setPluginState(FAILED); + verify(pluginWrapper, times(1)) + .setFailedException(any(InvalidPluginConfigurationException.class)); + } + + @Test + void handlePluginShouldInvokeStartPluginGivenTheConfigurationIsValid() { + + // GIVEN + final var pluginWrapper = mock(PluginWrapper.class); + final var rika2mqttPlugin = new DummyConfigurablePlugin(); + when(pluginWrapper.getPlugin()).thenReturn(rika2mqttPlugin); + final var pluginDescriptor = mock(PluginDescriptor.class); + doReturn(pluginDescriptor).when(pluginWrapper).getDescriptor(); + doReturn(true).when(rika2MqttPluginManager).isPluginConfigurationValid(any(), any()); + + // WHEN + rika2MqttPluginManager.handlePlugin(pluginWrapper); + + // THEN + verify(rika2MqttPluginManager, times(1)).startPlugin(any(), any()); + } + + @Test + void handlePluginShouldInvokeHandleInvalidPluginConfigurationGivenTheConfigurationIsInvalid() { + + // GIVEN + final var pluginWrapper = mock(PluginWrapper.class); + final var rika2mqttPlugin = new DummyConfigurablePlugin(); + when(pluginWrapper.getPlugin()).thenReturn(rika2mqttPlugin); + final var pluginDescriptor = mock(PluginDescriptor.class); + doReturn(pluginDescriptor).when(pluginWrapper).getDescriptor(); + doReturn(false).when(rika2MqttPluginManager).isPluginConfigurationValid(any(), any()); + + // WHEN + rika2MqttPluginManager.handlePlugin(pluginWrapper); + + // THEN + verify(rika2MqttPluginManager, times(1)).handleInvalidPluginConfiguration(any()); + } + + @Test + void + isPluginConfigurationValidShouldReturnTrueGivenThePluginIsNotImplementingConfigurablePlugin() { + + // GIVEN + final var plugin = new NotConfigurablePlugin(); + final var pluginConfiguration = mock(PluginConfiguration.class); + + // WHEN + final var valid = + rika2MqttPluginManager.isPluginConfigurationValid(plugin, pluginConfiguration); + + // THEN + assertThat(valid).isTrue(); + } + + @Test + void + isPluginConfigurationValidShouldReturnFalseGivenTheConfigurablePluginDeclaresARequiredParameterAndItIsNotProvided() { + + // GIVEN + // intentionally do not define a password ENV + + final var plugin = new DummyConfigurablePlugin(); + when(rika2MqttPluginManager.loadPluginConfiguration(plugin)).thenCallRealMethod(); + final var pluginConfiguration = rika2MqttPluginManager.loadPluginConfiguration(plugin); + + // WHEN + final var valid = + rika2MqttPluginManager.isPluginConfigurationValid(plugin, pluginConfiguration); + + // THEN + assertThat(valid).isFalse(); + } + + @Test + void + isPluginConfigurationValidShouldReturnTrueGivenTheConfigurablePluginDeclaresARequiredParameterAndIsProvided() { + + // GIVEN + mockPluginEnv("password", "p4ssw0rd"); + + final var plugin = new DummyConfigurablePlugin(); + when(rika2MqttPluginManager.loadPluginConfiguration(plugin)).thenCallRealMethod(); + final var pluginConfiguration = rika2MqttPluginManager.loadPluginConfiguration(plugin); + + // WHEN + final var valid = + rika2MqttPluginManager.isPluginConfigurationValid(plugin, pluginConfiguration); + + // THEN + assertThat(valid).isTrue(); + } + + /** Helper method for testing plugin configurations */ + private void mockPluginEnv( + @NonNull final String pluginParameterName, @NonNull final String value) { + mockedAnswers.put(pluginParameterName, value); + doAnswer( + invocation -> { + final var arg = (String) invocation.getArgument(0); + final var keyName = arg.replaceFirst(PLUGIN_ENV_NAME_PREFIX, ""); + return Optional.ofNullable(mockedAnswers.get(keyName)); + }) + .when(rika2MqttPluginManager) + .getEnvironmentVariable(anyString()); + } + + @Test + void shouldStartPluginShouldThrowAnNullPointerExceptionGivenNoParameterIsPassed() { + assertThrows( + NullPointerException.class, + () -> { + // WHEN + rika2MqttPluginManager.shouldStartPlugin(null); + }); + } + + @Test + void handlePluginShouldThrowAnNullPointerExceptionGivenNoParameterIsPassed() { + assertThrows( + NullPointerException.class, + () -> { + // WHEN + rika2MqttPluginManager.handlePlugin(null); + }); + } + + @Test + void handleInvalidPluginConfigurationShouldThrowAnNullPointerExceptionGivenNoParameterIsPassed() { + assertThrows( + NullPointerException.class, + () -> { + // WHEN + rika2MqttPluginManager.handleInvalidPluginConfiguration(null); + }); + } + + @Test + void handlePluginStartFailureShouldThrowAnNullPointerExceptionGivenNoParameterIsPassed() { + assertThrows( + NullPointerException.class, + () -> { + // WHEN + rika2MqttPluginManager.handlePluginStartFailure(null, null); + }); + } + + @Test + void stopAndFailPluginShouldThrowAnNullPointerExceptionGivenNoParameterIsPassed() { + assertThrows( + NullPointerException.class, + () -> { + // WHEN + rika2MqttPluginManager.stopAndFailPlugin(null, null); + }); + } + + @Test + void isPluginConfigurationValidShouldThrowAnNullPointerExceptionGivenNoParameterIsPassed() { + assertThrows( + NullPointerException.class, + () -> { + // WHEN + rika2MqttPluginManager.isPluginConfigurationValid(null, null); + }); + } + + @Test + void loadPluginConfigurationShouldThrowAnNullPointerExceptionGivenNoParameterIsPassed() { + assertThrows( + NullPointerException.class, + () -> { + // WHEN + rika2MqttPluginManager.loadPluginConfiguration(null); + }); + } + + @Test + void getEnvironmentVariableShouldThrowAnNullPointerExceptionGivenNoParameterIsPassed() { + assertThrows( + NullPointerException.class, + () -> { + // WHEN + rika2MqttPluginManager.getEnvironmentVariable(null); + }); + } +} diff --git a/plugins/disabled.txt b/plugins/disabled.txt new file mode 100644 index 00000000..14e3b6d9 --- /dev/null +++ b/plugins/disabled.txt @@ -0,0 +1 @@ +rika2mqtt-flux-metrics-plugin-1.1.0.jar diff --git a/pom.xml b/pom.xml index 432006d0..65610f41 100644 --- a/pom.xml +++ b/pom.xml @@ -54,8 +54,10 @@ ${basedir} 1.18.30 + 1.5.5.Final 0.8 8.0.1.Final + 3.10.0 1.19.1 @@ -88,6 +90,11 @@ rika-firenet bridge mqtt + plugins-api + rika2mqtt-flux-metrics-plugin + plugins-internal + rika2mqtt-example-plugin + rika2mqtt-example-plugin-using-config @@ -360,6 +367,7 @@ src/main/resources/** Makefile lombok.config + plugins/disabled.txt @@ -381,7 +389,7 @@ ${rika2mqtt.root}/.licenses/licenses.xml false true - true + false true diff --git a/rika-firenet/pom.xml b/rika-firenet/pom.xml index 6be0f05a..c6761318 100644 --- a/rika-firenet/pom.xml +++ b/rika-firenet/pom.xml @@ -45,7 +45,6 @@ ${source.encoding} - 1.5.5.Final 2.9.0 4.12.0 5.2.1 diff --git a/rika-firenet/src/main/java/dev/cookiecode/rika2mqtt/rika/firenet/exception/CouldNotAuthenticateToRikaFirenetException.java b/rika-firenet/src/main/java/dev/cookiecode/rika2mqtt/rika/firenet/exception/CouldNotAuthenticateToRikaFirenetException.java index ad6b204f..da2c7a48 100644 --- a/rika-firenet/src/main/java/dev/cookiecode/rika2mqtt/rika/firenet/exception/CouldNotAuthenticateToRikaFirenetException.java +++ b/rika-firenet/src/main/java/dev/cookiecode/rika2mqtt/rika/firenet/exception/CouldNotAuthenticateToRikaFirenetException.java @@ -22,12 +22,10 @@ */ package dev.cookiecode.rika2mqtt.rika.firenet.exception; +import lombok.experimental.StandardException; + /** * @author Sebastien Vermeille */ -public class CouldNotAuthenticateToRikaFirenetException extends RikaFirenetException { - - public CouldNotAuthenticateToRikaFirenetException(final String message) { - super(message); - } -} +@StandardException +public class CouldNotAuthenticateToRikaFirenetException extends RikaFirenetException {} diff --git a/rika-firenet/src/main/java/dev/cookiecode/rika2mqtt/rika/firenet/exception/InvalidStoveIdException.java b/rika-firenet/src/main/java/dev/cookiecode/rika2mqtt/rika/firenet/exception/InvalidStoveIdException.java index 65063570..778b4fdf 100644 --- a/rika-firenet/src/main/java/dev/cookiecode/rika2mqtt/rika/firenet/exception/InvalidStoveIdException.java +++ b/rika-firenet/src/main/java/dev/cookiecode/rika2mqtt/rika/firenet/exception/InvalidStoveIdException.java @@ -22,12 +22,10 @@ */ package dev.cookiecode.rika2mqtt.rika.firenet.exception; +import lombok.experimental.StandardException; + /** * @author Sebastien Vermeille */ -public class InvalidStoveIdException extends RikaFirenetException { - - public InvalidStoveIdException(final String message) { - super(message); - } -} +@StandardException +public class InvalidStoveIdException extends RikaFirenetException {} diff --git a/rika-firenet/src/main/java/dev/cookiecode/rika2mqtt/rika/firenet/exception/OutdatedRevisionException.java b/rika-firenet/src/main/java/dev/cookiecode/rika2mqtt/rika/firenet/exception/OutdatedRevisionException.java index 1653973b..169af9f3 100644 --- a/rika-firenet/src/main/java/dev/cookiecode/rika2mqtt/rika/firenet/exception/OutdatedRevisionException.java +++ b/rika-firenet/src/main/java/dev/cookiecode/rika2mqtt/rika/firenet/exception/OutdatedRevisionException.java @@ -22,12 +22,10 @@ */ package dev.cookiecode.rika2mqtt.rika.firenet.exception; +import lombok.experimental.StandardException; + /** * @author Sebastien Vermeille */ -public class OutdatedRevisionException extends RikaFirenetException { - - public OutdatedRevisionException(final String message) { - super(message); - } -} +@StandardException +public class OutdatedRevisionException extends RikaFirenetException {} diff --git a/rika-firenet/src/main/java/dev/cookiecode/rika2mqtt/rika/firenet/exception/UnableToControlRikaFirenetException.java b/rika-firenet/src/main/java/dev/cookiecode/rika2mqtt/rika/firenet/exception/UnableToControlRikaFirenetException.java index 7bd18226..daedfc54 100644 --- a/rika-firenet/src/main/java/dev/cookiecode/rika2mqtt/rika/firenet/exception/UnableToControlRikaFirenetException.java +++ b/rika-firenet/src/main/java/dev/cookiecode/rika2mqtt/rika/firenet/exception/UnableToControlRikaFirenetException.java @@ -22,16 +22,10 @@ */ package dev.cookiecode.rika2mqtt.rika.firenet.exception; +import lombok.experimental.StandardException; + /** * @author Sebastien Vermeille */ -public class UnableToControlRikaFirenetException extends RikaFirenetException { - - public UnableToControlRikaFirenetException(final String message) { - super(message); - } - - public UnableToControlRikaFirenetException(final String message, final Throwable cause) { - super(message, cause); - } -} +@StandardException +public class UnableToControlRikaFirenetException extends RikaFirenetException {} diff --git a/rika-firenet/src/main/java/dev/cookiecode/rika2mqtt/rika/firenet/exception/UnableToRetrieveRikaFirenetDataException.java b/rika-firenet/src/main/java/dev/cookiecode/rika2mqtt/rika/firenet/exception/UnableToRetrieveRikaFirenetDataException.java index 3b1b5d48..3c26b3ca 100644 --- a/rika-firenet/src/main/java/dev/cookiecode/rika2mqtt/rika/firenet/exception/UnableToRetrieveRikaFirenetDataException.java +++ b/rika-firenet/src/main/java/dev/cookiecode/rika2mqtt/rika/firenet/exception/UnableToRetrieveRikaFirenetDataException.java @@ -22,12 +22,10 @@ */ package dev.cookiecode.rika2mqtt.rika.firenet.exception; +import lombok.experimental.StandardException; + /** * @author Sebastien Vermeille */ -public class UnableToRetrieveRikaFirenetDataException extends RikaFirenetException { - - public UnableToRetrieveRikaFirenetDataException(final String message, final Throwable cause) { - super(message, cause); - } -} +@StandardException +public class UnableToRetrieveRikaFirenetDataException extends RikaFirenetException {} diff --git a/rika-firenet/src/main/java/dev/cookiecode/rika2mqtt/rika/firenet/model/Controls.java b/rika-firenet/src/main/java/dev/cookiecode/rika2mqtt/rika/firenet/model/Controls.java index 5bb2df4d..b936b571 100644 --- a/rika-firenet/src/main/java/dev/cookiecode/rika2mqtt/rika/firenet/model/Controls.java +++ b/rika-firenet/src/main/java/dev/cookiecode/rika2mqtt/rika/firenet/model/Controls.java @@ -40,6 +40,8 @@ public class Controls { private Integer targetTemperature; private Integer bakeTemperature; private Boolean ecoMode; + + // Heating time format: heatingTimeWed2: "13302200" (13:30 - 22:00) private String heatingTimeMon1; private String heatingTimeMon2; private String heatingTimeTue1; @@ -54,6 +56,7 @@ public class Controls { private String heatingTimeSat2; private String heatingTimeSun1; private String heatingTimeSun2; + private Boolean heatingTimesActiveForComfort; private Integer setBackTemperature; private Boolean convectionFan1Active; diff --git a/rika-firenet/src/main/java/dev/cookiecode/rika2mqtt/rika/firenet/model/Sensors.java b/rika-firenet/src/main/java/dev/cookiecode/rika2mqtt/rika/firenet/model/Sensors.java index b18a5cc6..f2490d51 100644 --- a/rika-firenet/src/main/java/dev/cookiecode/rika2mqtt/rika/firenet/model/Sensors.java +++ b/rika-firenet/src/main/java/dev/cookiecode/rika2mqtt/rika/firenet/model/Sensors.java @@ -23,12 +23,14 @@ package dev.cookiecode.rika2mqtt.rika.firenet.model; import com.google.gson.annotations.SerializedName; +import lombok.Builder; import lombok.Data; /** * @author Sebastien Vermeille */ @Data +@Builder public class Sensors { private Double inputRoomTemperature; diff --git a/rika-firenet/src/main/java/dev/cookiecode/rika2mqtt/rika/firenet/model/StoveStatus.java b/rika-firenet/src/main/java/dev/cookiecode/rika2mqtt/rika/firenet/model/StoveStatus.java index ec1b39e7..6bb8bd09 100644 --- a/rika-firenet/src/main/java/dev/cookiecode/rika2mqtt/rika/firenet/model/StoveStatus.java +++ b/rika-firenet/src/main/java/dev/cookiecode/rika2mqtt/rika/firenet/model/StoveStatus.java @@ -23,12 +23,14 @@ package dev.cookiecode.rika2mqtt.rika.firenet.model; import com.google.gson.annotations.SerializedName; +import lombok.Builder; import lombok.Data; /** * @author Sebastien Vermeille */ @Data +@Builder public class StoveStatus { private String name; diff --git a/rika-firenet/src/test/java/dev/cookiecode/rika2mqtt/rika/firenet/RikaFirenetServiceTest.java b/rika-firenet/src/test/java/dev/cookiecode/rika2mqtt/rika/firenet/RikaFirenetServiceTest.java index f1cb854e..8000061f 100644 --- a/rika-firenet/src/test/java/dev/cookiecode/rika2mqtt/rika/firenet/RikaFirenetServiceTest.java +++ b/rika-firenet/src/test/java/dev/cookiecode/rika2mqtt/rika/firenet/RikaFirenetServiceTest.java @@ -680,7 +680,7 @@ private void initStoveStatusMock(final StoveId stoveId) { "convectionFan2Area": 7, "frostProtectionActive": true, "frostProtectionTemperature": "4", - "temperatureOffset": "0", + "temperatureOffset": "-0.5", "RoomPowerRequest": 3, "debug0": 0, "debug1": 0, diff --git a/rika-firenet/src/test/java/dev/cookiecode/rika2mqtt/rika/firenet/model/StoveStatusSerdeTest.java b/rika-firenet/src/test/java/dev/cookiecode/rika2mqtt/rika/firenet/model/StoveStatusSerdeTest.java index 7af757a5..81bf5fb9 100644 --- a/rika-firenet/src/test/java/dev/cookiecode/rika2mqtt/rika/firenet/model/StoveStatusSerdeTest.java +++ b/rika-firenet/src/test/java/dev/cookiecode/rika2mqtt/rika/firenet/model/StoveStatusSerdeTest.java @@ -47,8 +47,7 @@ class StoveStatusSerdeTest { @Test void serializationOfStoveStatusToJsonShouldNotPropagateUppercaseId() { // GIVEN - var status = new StoveStatus(); - status.setStoveId(12L); + var status = StoveStatus.builder().stoveId(12L).build(); // WHEN var jsonResult = gson.toJson(status); diff --git a/rika2mqtt-example-plugin-using-config/pom.xml b/rika2mqtt-example-plugin-using-config/pom.xml new file mode 100644 index 00000000..8dfff8f5 --- /dev/null +++ b/rika2mqtt-example-plugin-using-config/pom.xml @@ -0,0 +1,122 @@ + + + + 4.0.0 + + dev.cookiecode + rika2mqtt-parent + 1.1.0 + + + rika2mqtt-example-plugin-using-config + + + ${basedir}/.. + ${java.sdk.version} + ${java.sdk.version} + ${source.encoding} + + ${project.sonar.root.projectKey}-${project.groupId}-${project.artifactId} + + + + + dev.cookiecode + plugins-api + 1.1.0 + provided + + + + + + + org.apache.maven.plugins + maven-assembly-plugin + 3.6.0 + + + jar-with-dependencies + + ${project.artifactId}-${project.version} + false + false + + + true + true + + + dev.cookiecode.rika2mqtt.plugins.example.ExamplePluginUsingConfig + example-plugin-using-config + 0.0.1 + 2.0.0 + This plugin demonstrate usage of external configuration + Sebastien Vermeille + MIT + + + + + + + + make-assembly + package + + single + + + + + + + + maven-antrun-plugin + 3.1.0 + + + + copy-to-lib + + run + + package + + + + + + + + + + + diff --git a/rika2mqtt-example-plugin-using-config/src/main/java/dev/cookiecode/rika2mqtt/plugins/example/ExamplePluginUsingConfig.java b/rika2mqtt-example-plugin-using-config/src/main/java/dev/cookiecode/rika2mqtt/plugins/example/ExamplePluginUsingConfig.java new file mode 100644 index 00000000..350b2b30 --- /dev/null +++ b/rika2mqtt-example-plugin-using-config/src/main/java/dev/cookiecode/rika2mqtt/plugins/example/ExamplePluginUsingConfig.java @@ -0,0 +1,95 @@ +package dev.cookiecode.rika2mqtt.plugins.example; + +/* + * The MIT License + * Copyright © 2022 Sebastien Vermeille + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +import dev.cookiecode.rika2mqtt.plugins.api.v1.Rika2MqttPlugin; +import dev.cookiecode.rika2mqtt.plugins.api.v1.annotations.ConfigurablePlugin; +import dev.cookiecode.rika2mqtt.plugins.api.v1.model.plugins.OptionalPluginConfigurationParameter; +import dev.cookiecode.rika2mqtt.plugins.api.v1.model.plugins.PluginConfigurationParameter; +import dev.cookiecode.rika2mqtt.plugins.api.v1.model.plugins.RequiredPluginConfigurationParameter; +import java.util.List; +import lombok.extern.flogger.Flogger; + +/** + * Example of a plugin class that declares requiring some configuration + * + * @author Sebastien Vermeille + */ +@Flogger +public class ExamplePluginUsingConfig extends Rika2MqttPlugin implements ConfigurablePlugin { + + static final String PLUGIN_NAME = "ExamplePlugin"; + + @Override + public void start() { + log.atInfo().log("%s >> STARTED", PLUGIN_NAME); + } + + @Override + public void stop() { + log.atInfo().log("%s >> STOPPED", PLUGIN_NAME); + } + + @Override + public List declarePluginConfigurationParameters() { + + // Here the plugin declares what it needs access to + + return List.of( + // Optional Parameters + + // exhaustiveDeclaration + OptionalPluginConfigurationParameter.builder() + .withParameterName("example.plugin.postgres.port") + .withDescription("Postgres server port") + .withValueType(Integer.class) + .withDefaultValue(5432) + .withExample("5432") + .build(), + + // short declaration (no default, no example) + OptionalPluginConfigurationParameter.builder() + .withParameterName("example.plugin.influxdb.auth-token") + .withDescription("Influxdb authentication token") + .withValueType(String.class) + .build(), + + // Required parameters + + // exhaustive declaration + RequiredPluginConfigurationParameter.builder() + .withParameterName("example.plugin.postgres.host") + .withDescription("Postgres server hostname or IP") + .withValueType(String.class) + .withExample("hostname, 127.0.0.1, 0.0.0.0") + .build(), + + // short declaration (no example) + RequiredPluginConfigurationParameter.builder() + .withParameterName("example.plugin.postgres.dbname") + .withDescription("Postgres database name") + .withValueType(String.class) + .build()); + } +} diff --git a/rika2mqtt-example-plugin-using-config/src/test/java/DummyTest.java b/rika2mqtt-example-plugin-using-config/src/test/java/DummyTest.java new file mode 100644 index 00000000..f4cccffc --- /dev/null +++ b/rika2mqtt-example-plugin-using-config/src/test/java/DummyTest.java @@ -0,0 +1,41 @@ +/* + * The MIT License + * Copyright © 2022 Sebastien Vermeille + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.Test; + +/** + * Test class + * + * @author Sebastien Vermeille + */ +class DummyTest { + + @Test + void dummyTest() { + assertThat(true) + .isTrue(); // TODO: add more complexity to the example plugin so that we can write real + // tests + } +} diff --git a/rika2mqtt-example-plugin/pom.xml b/rika2mqtt-example-plugin/pom.xml new file mode 100644 index 00000000..7bf51cf3 --- /dev/null +++ b/rika2mqtt-example-plugin/pom.xml @@ -0,0 +1,122 @@ + + + + 4.0.0 + + dev.cookiecode + rika2mqtt-parent + 1.1.0 + + + rika2mqtt-example-plugin + + + ${basedir}/.. + ${java.sdk.version} + ${java.sdk.version} + ${source.encoding} + + ${project.sonar.root.projectKey}-${project.groupId}-${project.artifactId} + + + + + dev.cookiecode + plugins-api + 1.1.0 + provided + + + + + + + org.apache.maven.plugins + maven-assembly-plugin + 3.6.0 + + + jar-with-dependencies + + ${project.artifactId}-${project.version} + false + false + + + true + true + + + dev.cookiecode.rika2mqtt.plugins.example.ExamplePlugin + example-plugin + 0.0.1 + 2.0.0 + Log each time an extension point is invoked. + Sebastien Vermeille + MIT + + + + + + + + make-assembly + package + + single + + + + + + + + maven-antrun-plugin + 3.1.0 + + + + copy-to-lib + + run + + package + + + + + + + + + + + diff --git a/rika2mqtt-example-plugin/src/main/java/dev/cookiecode/rika2mqtt/plugins/example/ExampleHook.java b/rika2mqtt-example-plugin/src/main/java/dev/cookiecode/rika2mqtt/plugins/example/ExampleHook.java new file mode 100644 index 00000000..1d9a79af --- /dev/null +++ b/rika2mqtt-example-plugin/src/main/java/dev/cookiecode/rika2mqtt/plugins/example/ExampleHook.java @@ -0,0 +1,49 @@ +/* + * The MIT License + * Copyright © 2022 Sebastien Vermeille + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package dev.cookiecode.rika2mqtt.plugins.example; + +import static dev.cookiecode.rika2mqtt.plugins.example.ExamplePlugin.PLUGIN_NAME; + +import dev.cookiecode.rika2mqtt.plugins.api.v1.StoveStatusExtension; +import dev.cookiecode.rika2mqtt.plugins.api.v1.model.StoveStatus; +import lombok.extern.flogger.Flogger; +import org.pf4j.Extension; + +/** + * A sample hook (or listener) class + * + * @author Sebastien Vermeille + */ +@Extension +@Flogger +public class ExampleHook implements StoveStatusExtension { + + @Override + public void onPollStoveStatusSucceed(StoveStatus stoveStatus) { + log.atInfo().log("%s >> onPollStoveStatusSucceed invoked", PLUGIN_NAME); + } + + public void stop() { + log.atInfo().log("%s >> %s stopped", PLUGIN_NAME, ExampleHook.class.getSimpleName()); + } +} diff --git a/rika2mqtt-example-plugin/src/main/java/dev/cookiecode/rika2mqtt/plugins/example/ExamplePlugin.java b/rika2mqtt-example-plugin/src/main/java/dev/cookiecode/rika2mqtt/plugins/example/ExamplePlugin.java new file mode 100644 index 00000000..091e1b1b --- /dev/null +++ b/rika2mqtt-example-plugin/src/main/java/dev/cookiecode/rika2mqtt/plugins/example/ExamplePlugin.java @@ -0,0 +1,51 @@ +/* + * The MIT License + * Copyright © 2022 Sebastien Vermeille + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package dev.cookiecode.rika2mqtt.plugins.example; + +import dev.cookiecode.rika2mqtt.plugins.api.v1.Rika2MqttPlugin; +import lombok.extern.flogger.Flogger; + +/** + * A sample plugin class + * + * @author Sebastien Vermeille + */ +@Flogger +public class ExamplePlugin extends Rika2MqttPlugin { + + static final String PLUGIN_NAME = "ExamplePlugin"; + + private ExampleHook hook; + + @Override + public void start() { + log.atInfo().log("%s >> STARTED", PLUGIN_NAME); + hook = new ExampleHook(); // instantiate/register hook (otherwise it won't be triggered) + } + + @Override + public void stop() { + log.atInfo().log("%s >> STOPPED", PLUGIN_NAME); + hook.stop(); + } +} diff --git a/rika2mqtt-example-plugin/src/test/java/dev/cookiecode/rika2mqtt/plugins/example/ExampleHookTest.java b/rika2mqtt-example-plugin/src/test/java/dev/cookiecode/rika2mqtt/plugins/example/ExampleHookTest.java new file mode 100644 index 00000000..92543368 --- /dev/null +++ b/rika2mqtt-example-plugin/src/test/java/dev/cookiecode/rika2mqtt/plugins/example/ExampleHookTest.java @@ -0,0 +1,42 @@ +/* + * The MIT License + * Copyright © 2022 Sebastien Vermeille + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package dev.cookiecode.rika2mqtt.plugins.example; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.Test; + +/** + * Test class + * + * @author Sebastien Vermeille + */ +class ExampleHookTest { + + @Test + void dummyTest() { + assertThat(true) + .isTrue(); // TODO: add more complexity to the example plugin so that we can write real + // tests + } +} diff --git a/rika2mqtt-flux-metrics-plugin/pom.xml b/rika2mqtt-flux-metrics-plugin/pom.xml new file mode 100644 index 00000000..816a0109 --- /dev/null +++ b/rika2mqtt-flux-metrics-plugin/pom.xml @@ -0,0 +1,126 @@ + + + + 4.0.0 + + dev.cookiecode + rika2mqtt-parent + 1.1.0 + + + rika2mqtt-flux-metrics-plugin + + + ${basedir}/.. + ${java.sdk.version} + ${java.sdk.version} + ${source.encoding} + 2.5.9 + + ${project.sonar.root.projectKey}-${project.groupId}-${project.artifactId} + + + + dev.cookiecode + plugins-api + 1.1.0 + provided + + + io.kamon + kamon-bundle_2.13 + ${kamon.version} + compile + + + io.kamon + kamon-influxdb_2.13 + ${kamon.version} + compile + + + + + + + org.apache.maven.plugins + maven-shade-plugin + 3.5.0 + + + shade-jar-with-dependencies + package + + shade + + + + + + dev.cookiecode.rika2mqtt.plugins.influxdb.metrics.Rika2MqttInfluxMetricsPlugin + ${project.artifactId} + ${project.version} + ${project.version} + Export RIKA stoves values to InfluxDB. + Sebastien Vermeille + MIT + + + + + reference.conf + + + + + + + + maven-antrun-plugin + 3.1.0 + + + + copy-to-lib + + run + + package + + + + + + + + + + + diff --git a/rika2mqtt-flux-metrics-plugin/src/main/java/dev/cookiecode/rika2mqtt/plugins/influxdb/metrics/Rika2MqttInfluxMetricsPlugin.java b/rika2mqtt-flux-metrics-plugin/src/main/java/dev/cookiecode/rika2mqtt/plugins/influxdb/metrics/Rika2MqttInfluxMetricsPlugin.java new file mode 100644 index 00000000..c7f2a365 --- /dev/null +++ b/rika2mqtt-flux-metrics-plugin/src/main/java/dev/cookiecode/rika2mqtt/plugins/influxdb/metrics/Rika2MqttInfluxMetricsPlugin.java @@ -0,0 +1,110 @@ +/* + * The MIT License + * Copyright © 2022 Sebastien Vermeille + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package dev.cookiecode.rika2mqtt.plugins.influxdb.metrics; + +import com.typesafe.config.Config; +import com.typesafe.config.ConfigFactory; +import dev.cookiecode.rika2mqtt.plugins.api.v1.Rika2MqttPlugin; +import dev.cookiecode.rika2mqtt.plugins.api.v1.annotations.ConfigurablePlugin; +import dev.cookiecode.rika2mqtt.plugins.api.v1.model.plugins.OptionalPluginConfigurationParameter; +import dev.cookiecode.rika2mqtt.plugins.api.v1.model.plugins.PluginConfigurationParameter; +import dev.cookiecode.rika2mqtt.plugins.api.v1.model.plugins.RequiredPluginConfigurationParameter; +import java.util.HashMap; +import java.util.List; +import kamon.Kamon; + +/** A plugin to export rika2mqtt metrics to InfluxDB */ +public class Rika2MqttInfluxMetricsPlugin extends Rika2MqttPlugin implements ConfigurablePlugin { + private static final String INFLUXDB_HOSTNAME = "INFLUXDB_HOSTNAME"; + private static final String INFLUXDB_PORT = "INFLUXDB_PORT"; + private static final String INFLUXDB_DATABASE = "INFLUXDB_DATABASE"; + private static final String INFLUXDB_PROTOCOL = "INFLUXDB_PROTOCOL"; + private static final String INFLUXDB_AUTHENTICATION_TOKEN = "INFLUXDB_AUTHENTICATION_TOKEN"; + + @Override + public void start() { + Kamon.init(); + Kamon.reconfigure(loadConfig()); + Kamon.loadModules(); + + new StoveStatusHook(); + } + + @Override + public void stop() { + log.atInfo().log("Stopping Kamon..."); + Kamon.stop(); + } + + Config loadConfig() { + var defaultConfig = Kamon.config(); + + var props = new HashMap(); + props.put("kamon.influxdb.port", getPluginConfigurationParameter(INFLUXDB_PORT)); + props.put("kamon.influxdb.hostname", getPluginConfigurationParameter(INFLUXDB_HOSTNAME)); + props.put("kamon.influxdb.database", getPluginConfigurationParameter(INFLUXDB_DATABASE)); + props.put("kamon.influxdb.protocol", getPluginConfigurationParameter(INFLUXDB_PROTOCOL)); + props.put( + "kamon.influxdb.authentication.token", + getPluginConfigurationParameter(INFLUXDB_AUTHENTICATION_TOKEN)); + + var codeConfig = ConfigFactory.parseMap(props); + return codeConfig.withFallback(defaultConfig); + } + + @Override + public List declarePluginConfigurationParameters() { + return List.of( + RequiredPluginConfigurationParameter.builder() + .withParameterName(INFLUXDB_HOSTNAME) + .withDescription("Hostname or IP of the influxdb server") + .withValueType(String.class) + .withExample("localhost, 127.0.0.1, 0.0.0.0") + .build(), + OptionalPluginConfigurationParameter.builder() + .withParameterName(INFLUXDB_PORT) + .withDescription("Port of influxdb server") + .withValueType(Integer.class) + .withExample("8086") + .withDefaultValue(8086) + .build(), + RequiredPluginConfigurationParameter.builder() + .withParameterName(INFLUXDB_DATABASE) + .withDescription("Name of the influxdb database") + .withValueType(String.class) + .withExample("rika2mqtt") + .build(), + OptionalPluginConfigurationParameter.builder() + .withParameterName(INFLUXDB_PROTOCOL) + .withDescription("Protocol used to communicate with influxdb") + .withValueType(String.class) + .withExample("http") + .withDefaultValue("http") + .build(), + RequiredPluginConfigurationParameter.builder() + .withParameterName(INFLUXDB_AUTHENTICATION_TOKEN) + .withDescription("Auth token for influxdb database") + .withValueType(String.class) + .build()); + } +} diff --git a/rika2mqtt-flux-metrics-plugin/src/main/java/dev/cookiecode/rika2mqtt/plugins/influxdb/metrics/StoveStatusHook.java b/rika2mqtt-flux-metrics-plugin/src/main/java/dev/cookiecode/rika2mqtt/plugins/influxdb/metrics/StoveStatusHook.java new file mode 100644 index 00000000..cfd7a64c --- /dev/null +++ b/rika2mqtt-flux-metrics-plugin/src/main/java/dev/cookiecode/rika2mqtt/plugins/influxdb/metrics/StoveStatusHook.java @@ -0,0 +1,374 @@ +/* + * The MIT License + * Copyright © 2022 Sebastien Vermeille + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package dev.cookiecode.rika2mqtt.plugins.influxdb.metrics; + +import static dev.cookiecode.rika2mqtt.plugins.influxdb.metrics.reflection.ReflectionUtils.*; +import static java.lang.Boolean.TRUE; +import static java.util.Optional.ofNullable; +import static java.util.concurrent.TimeUnit.HOURS; + +import dev.cookiecode.rika2mqtt.plugins.api.v1.StoveStatusExtension; +import dev.cookiecode.rika2mqtt.plugins.api.v1.model.Controls; +import dev.cookiecode.rika2mqtt.plugins.api.v1.model.Sensors; +import dev.cookiecode.rika2mqtt.plugins.api.v1.model.StoveStatus; +import java.util.Optional; +import java.util.stream.IntStream; +import kamon.Kamon; +import lombok.NonNull; +import lombok.extern.flogger.Flogger; +import org.pf4j.Extension; + +@Extension +@Flogger +public class StoveStatusHook implements StoveStatusExtension { + + // Tags used for influx storage (ease filtering) + private static final String STOVE_ID = "STOVE_ID"; + private static final String STOVE_NAME = "STOVE_NAME"; + private static final String FAN_ID = "FAN_ID"; + private static final String DAY_OF_WEEK = "DAY_OF_WEEK"; + private static final String TIME_RANGE_INDEX = "TIME_RANGE_INDEX"; + private static final String ERROR_NUMBER = "ERROR_NUMBER"; + private static final String DEBUG_NUMBER = "DEBUG_NUMBER"; + private static final String COULD_NOT_EXPORT_PROPERTY_S_IT_COULD_NOT_BE_RETRIEVED = + "Could not export property %s, it could not be retrieved."; + private static final String COULD_NOT_GET_PROPERTY_S = "Could not get property %s"; + + @Override + public void onPollStoveStatusSucceed(StoveStatus stoveStatus) { + log.atInfo().atMostEvery(1, HOURS).log( + "Stove status is being continuously forwarded to Influx"); + + exportSensorsMetrics(stoveStatus); + exportControlsMetrics(stoveStatus); + + exportProperty(stoveStatus, "lastSeenMinutes", Long.class); + exportProperty(stoveStatus, "lastConfirmedRevision", Long.class); + } + + private void exportSensorsMetrics(@NonNull final StoveStatus stoveStatus) { + + exportProperty(stoveStatus, "sensors.inputRoomTemperature", Double.class); + exportProperty(stoveStatus, "sensors.inputFlameTemperature", Integer.class); + exportProperty(stoveStatus, "sensors.inputBakeTemperature", Integer.class); + exportProperty(stoveStatus, "sensors.statusError", Integer.class); + exportProperty(stoveStatus, "sensors.statusSubError", Integer.class); + exportProperty(stoveStatus, "sensors.statusWarning", Integer.class); + exportProperty(stoveStatus, "sensors.statusService", Integer.class); + exportProperty(stoveStatus, "sensors.outputDischargeMotor", Integer.class); + exportProperty(stoveStatus, "sensors.outputDischargeCurrent", Integer.class); + exportProperty(stoveStatus, "sensors.outputIdFan", Integer.class); + exportProperty(stoveStatus, "sensors.outputIdFanTarget", Integer.class); + exportProperty(stoveStatus, "sensors.outputInsertionMotor", Integer.class); + exportProperty(stoveStatus, "sensors.outputInsertionCurrent", Integer.class); + exportProperty(stoveStatus, "sensors.outputAirFlaps", Integer.class); + exportProperty(stoveStatus, "sensors.outputAirFlapsTargetPosition", Integer.class); + exportProperty(stoveStatus, "sensors.outputBurnBackFlapMagnet", Boolean.class); + exportProperty(stoveStatus, "sensors.outputGridMotor", Boolean.class); + exportProperty(stoveStatus, "sensors.outputIgnition", Boolean.class); + exportProperty(stoveStatus, "sensors.inputUpperTemperatureLimiter", Boolean.class); + exportProperty(stoveStatus, "sensors.inputPressureSwitch", Boolean.class); + exportProperty(stoveStatus, "sensors.inputPressureSensor", Integer.class); + exportProperty(stoveStatus, "sensors.inputGridContact", Boolean.class); + exportProperty(stoveStatus, "sensors.inputDoor", Boolean.class); + exportProperty(stoveStatus, "sensors.inputCover", Boolean.class); + exportProperty(stoveStatus, "sensors.inputExternalRequest", Boolean.class); + exportProperty(stoveStatus, "sensors.inputBurnBackFlapSwitch", Boolean.class); + exportProperty(stoveStatus, "sensors.inputFlueGasFlapSwitch", Boolean.class); + exportProperty(stoveStatus, "sensors.inputBoardTemperature", Double.class); + exportProperty(stoveStatus, "sensors.inputCurrentStage", Integer.class); + exportProperty(stoveStatus, "sensors.inputTargetStagePid", Integer.class); + exportProperty(stoveStatus, "sensors.inputCurrentStagePid", Integer.class); + + exportProperty(stoveStatus, "sensors.statusMainState", Integer.class); + exportProperty(stoveStatus, "sensors.statusSubState", Integer.class); + exportProperty(stoveStatus, "sensors.statusWifiStrength", Integer.class); + exportProperty(stoveStatus, "sensors.parameterEcoModePossible", Boolean.class); + exportProperty(stoveStatus, "sensors.parameterFabricationNumber", Integer.class); + exportProperty(stoveStatus, "sensors.parameterStoveTypeNumber", Integer.class); + exportProperty(stoveStatus, "sensors.parameterLanguageNumber", Integer.class); + exportProperty(stoveStatus, "sensors.parameterVersionMainBoard", Integer.class); + + exportProperty(stoveStatus, "sensors.parameterVersionTft", Integer.class); + exportProperty(stoveStatus, "sensors.parameterVersionWifi", Integer.class); + exportProperty(stoveStatus, "sensors.parameterVersionMainBoardBootLoader", Integer.class); + exportProperty(stoveStatus, "sensors.parameterVersionTftBootLoader", Integer.class); + exportProperty(stoveStatus, "sensors.parameterVersionWifiBootLoader", Integer.class); + exportProperty(stoveStatus, "sensors.parameterVersionMainBoardSub", Integer.class); + exportProperty(stoveStatus, "sensors.parameterVersionTftSub", Integer.class); + exportProperty(stoveStatus, "sensors.parameterVersionWifiSub", Integer.class); + + exportProperty(stoveStatus, "sensors.parameterRuntimePellets", Integer.class); + exportProperty(stoveStatus, "sensors.parameterRuntimeLogs", Integer.class); + exportProperty(stoveStatus, "sensors.parameterFeedRateTotal", Integer.class); + exportProperty(stoveStatus, "sensors.parameterFeedRateService", Integer.class); + exportProperty(stoveStatus, "sensors.parameterServiceCountdownKg", Integer.class); + exportProperty(stoveStatus, "sensors.parameterServiceCountdownTime", Integer.class); + exportProperty(stoveStatus, "sensors.parameterIgnitionCount", Integer.class); + exportProperty(stoveStatus, "sensors.parameterOnOffCycleCount", Integer.class); + exportProperty(stoveStatus, "sensors.parameterFlameSensorOffset", Integer.class); + exportProperty(stoveStatus, "sensors.parameterPressureSensorOffset", Integer.class); + + exportProperty(stoveStatus, "sensors.statusHeatingTimesNotProgrammed", Boolean.class); + exportProperty(stoveStatus, "sensors.statusFrostStarted", Boolean.class); + exportProperty(stoveStatus, "sensors.parameterSpiralMotorsTuning", Integer.class); + exportProperty(stoveStatus, "sensors.parameterIdFanTuning", Integer.class); + exportProperty(stoveStatus, "sensors.parameterCleanIntervalBig", Integer.class); + exportProperty(stoveStatus, "sensors.parameterKgTillCleaning", Integer.class); + + // following metrics are requiring special treatment + stoveStatus + .getSensors() + .getParametersErrorCount() + .forEach( + parameterErrorCount -> + Kamon.gauge("sensors.parameterErrorCount") + .withTag(STOVE_ID, stoveStatus.getStoveId().id()) + .withTag(STOVE_NAME, stoveStatus.getName()) + .withTag(ERROR_NUMBER, parameterErrorCount.getNumber()) // extra tag + .update(parameterErrorCount.getValue())); + + stoveStatus + .getSensors() + .getParametersDebug() + .forEach( + parameterDebug -> + Kamon.gauge("sensors.parameterDebug") + .withTag(STOVE_ID, stoveStatus.getStoveId().id()) + .withTag(STOVE_NAME, stoveStatus.getName()) + .withTag(DEBUG_NUMBER, parameterDebug.getNumber()) // extra tag + .update(parameterDebug.getValue())); + } + + private void exportControlsMetrics(@NonNull StoveStatus stoveStatus) { + exportProperty(stoveStatus, "controls.revision", Long.class); + exportProperty(stoveStatus, "controls.on", Boolean.class); + exportProperty(stoveStatus, "controls.operatingMode", Integer.class); + exportProperty(stoveStatus, "controls.heatingPower", Integer.class); + exportProperty(stoveStatus, "controls.targetTemperature", Integer.class); + exportProperty(stoveStatus, "controls.bakeTemperature", Integer.class); + exportProperty(stoveStatus, "controls.ecoModeEnabled", Boolean.class); + exportProperty(stoveStatus, "controls.heatingTimesActiveForComfort", Boolean.class); + exportProperty(stoveStatus, "controls.setBackTemperature", Integer.class); + exportProperty(stoveStatus, "controls.frostProtectionActive", Boolean.class); + exportProperty(stoveStatus, "controls.frostProtectionTemperature", Integer.class); + exportProperty(stoveStatus, "controls.temperatureOffset", Double.class); + exportProperty(stoveStatus, "controls.roomPowerRequest", Integer.class); + + // following metrics are requiring special treatment + stoveStatus + .getControls() + .getFans() + .forEach( + convectionFan -> { + Kamon.gauge("controls.convectionFan.area") + .withTag(STOVE_ID, stoveStatus.getStoveId().id()) + .withTag(STOVE_NAME, stoveStatus.getName()) + .withTag(FAN_ID, convectionFan.getIdentifier()) // extra tag + .update(convectionFan.getArea()); + + Kamon.gauge("controls.convectionFan.level") + .withTag(STOVE_ID, stoveStatus.getStoveId().id()) + .withTag(STOVE_NAME, stoveStatus.getName()) + .withTag(FAN_ID, convectionFan.getIdentifier()) // extra tag + .update(convectionFan.getLevel()); + + Kamon.gauge("controls.convectionFan.active") + .withTag(STOVE_ID, stoveStatus.getStoveId().id()) + .withTag(STOVE_NAME, stoveStatus.getName()) + .withTag(FAN_ID, convectionFan.getIdentifier()) // extra tag + .update(convectionFan.isActive() ? 1 : 0); + }); + + stoveStatus + .getControls() + .getHeatingTimes() + .forEach( + (dayOfWeek, timeRanges) -> + IntStream.range(0, timeRanges.size()) + .forEach( + index -> { + final var timeRange = timeRanges.get(index); + // from + Kamon.gauge("controls.heatingTime.from.hours") + .withTag(STOVE_ID, stoveStatus.getStoveId().id()) + .withTag(STOVE_NAME, stoveStatus.getName()) + .withTag(DAY_OF_WEEK, dayOfWeek.name()) // extra tag + .withTag(TIME_RANGE_INDEX, index) + .update(timeRange.getFrom().getHours()); + Kamon.gauge("controls.heatingTime.from.minutes") + .withTag(STOVE_ID, stoveStatus.getStoveId().id()) + .withTag(STOVE_NAME, stoveStatus.getName()) + .withTag(DAY_OF_WEEK, dayOfWeek.name()) // extra tag + .withTag(TIME_RANGE_INDEX, index) + .update(timeRange.getFrom().getMinutes()); + Kamon.gauge("controls.heatingTime.from.decimal") + .withTag(STOVE_ID, stoveStatus.getStoveId().id()) + .withTag(STOVE_NAME, stoveStatus.getName()) + .withTag(DAY_OF_WEEK, dayOfWeek.name()) // extra tag + .withTag(TIME_RANGE_INDEX, index) + .update(timeRange.getFrom().asDecimal()); + + // to + Kamon.gauge("controls.heatingTime.to.hours") + .withTag(STOVE_ID, stoveStatus.getStoveId().id()) + .withTag(STOVE_NAME, stoveStatus.getName()) + .withTag(DAY_OF_WEEK, dayOfWeek.name()) // extra tag + .withTag(TIME_RANGE_INDEX, index) + .update(timeRange.getTo().getHours()); + Kamon.gauge("controls.heatingTime.to.minutes") + .withTag(STOVE_ID, stoveStatus.getStoveId().id()) + .withTag(STOVE_NAME, stoveStatus.getName()) + .withTag(DAY_OF_WEEK, dayOfWeek.name()) // extra tag + .withTag(TIME_RANGE_INDEX, index) + .update(timeRange.getTo().getMinutes()); + + Kamon.gauge("controls.heatingTime.to.decimal") + .withTag(STOVE_ID, stoveStatus.getStoveId().id()) + .withTag(STOVE_NAME, stoveStatus.getName()) + .withTag(DAY_OF_WEEK, dayOfWeek.name()) // extra tag + .withTag(TIME_RANGE_INDEX, index) + .update(timeRange.getTo().asDecimal()); + })); + + final var controlsDebugs = stoveStatus.getControls().getDebugs(); + IntStream.range(0, controlsDebugs.size()) + .forEach( + index -> { + final var debugValue = controlsDebugs.get(index); + Kamon.gauge("controls.debug") + .withTag(STOVE_ID, stoveStatus.getStoveId().id()) + .withTag(STOVE_NAME, stoveStatus.getName()) + .withTag(DEBUG_NUMBER, index) // extra tag + .update(debugValue); + }); + } + + private void exportProperty( + @NonNull StoveStatus stoveStatus, @NonNull final String propertyName, Class returnType) { + if (returnType == Double.class) { + getDoublePropertyValue(stoveStatus, propertyName) + .ifPresentOrElse( + value -> + Kamon.gauge(propertyName) + .withTag(STOVE_ID, stoveStatus.getStoveId().id()) + .withTag(STOVE_NAME, stoveStatus.getName()) + .update(value), + () -> + log.atWarning().log( + COULD_NOT_EXPORT_PROPERTY_S_IT_COULD_NOT_BE_RETRIEVED, propertyName)); + } else if (returnType == Integer.class) { + getIntegerPropertyValue(stoveStatus, propertyName) + .ifPresentOrElse( + value -> + Kamon.gauge(propertyName) + .withTag(STOVE_ID, stoveStatus.getStoveId().id()) + .withTag(STOVE_NAME, stoveStatus.getName()) + .update(value), + () -> + log.atWarning().log( + COULD_NOT_EXPORT_PROPERTY_S_IT_COULD_NOT_BE_RETRIEVED, propertyName)); + } else if (returnType == Long.class) { + getLongPropertyValue(stoveStatus, propertyName) + .ifPresentOrElse( + value -> + Kamon.gauge(propertyName) + .withTag(STOVE_ID, stoveStatus.getStoveId().id()) + .withTag(STOVE_NAME, stoveStatus.getName()) + .update(value), + () -> + log.atWarning().log( + COULD_NOT_EXPORT_PROPERTY_S_IT_COULD_NOT_BE_RETRIEVED, propertyName)); + } else if (returnType == Boolean.class) { + getBooleanPropertyValue(stoveStatus, propertyName) + .map(value -> value == TRUE ? 1 : 0) + .map( + value -> + Kamon.gauge(propertyName) + .withTag(STOVE_ID, stoveStatus.getStoveId().id()) + .withTag(STOVE_NAME, stoveStatus.getName()) + .update(value)); + } else { + log.atSevere().log( + "Could not extract property %s because it's return type %s is unsupported.", + propertyName, returnType); + } + } + + private Optional getPropertyValue( + @NonNull StoveStatus stoveStatus, @NonNull final String propertyName) { + if (propertyName.startsWith("sensors.")) { + try { + final var shortName = propertyName.replace("sensors.", ""); + final var getterMethod = getPropertyGetterMethod(Sensors.class, shortName); + + var result = getterMethod.invoke(stoveStatus.getSensors()); + return ofNullable(result.toString()); + } catch (Exception ex) { + log.atSevere().withCause(ex).log(COULD_NOT_GET_PROPERTY_S, propertyName); + return Optional.empty(); + } + } else if (propertyName.startsWith("controls.")) { + try { + final var shortName = propertyName.replace("controls.", ""); + var getterMethod = getPropertyGetterMethod(Controls.class, shortName); + return ofNullable(getterMethod.invoke(stoveStatus.getControls())).map(Object::toString); + } catch (Exception ex) { + log.atSevere().withCause(ex).log(COULD_NOT_GET_PROPERTY_S, propertyName); + return Optional.empty(); + } + } else { + try { + final var shortName = + propertyName.substring(0, 1).toUpperCase() + propertyName.substring(1); + final var getterMethod = getPropertyGetterMethod(StoveStatus.class, shortName); + + var result = getterMethod.invoke(stoveStatus); + return ofNullable(result.toString()); + } catch (Exception ex) { + log.atSevere().withCause(ex).log(COULD_NOT_GET_PROPERTY_S, propertyName); + return Optional.empty(); + } + } + } + + private Optional getDoublePropertyValue( + @NonNull StoveStatus stoveStatus, @NonNull final String propertyName) { + return getPropertyValue(stoveStatus, propertyName).map(Double::valueOf); + } + + private Optional getIntegerPropertyValue( + @NonNull StoveStatus stoveStatus, @NonNull final String propertyName) { + return getPropertyValue(stoveStatus, propertyName).map(Integer::valueOf); + } + + private Optional getLongPropertyValue( + @NonNull StoveStatus stoveStatus, @NonNull final String propertyName) { + return getPropertyValue(stoveStatus, propertyName).map(Long::valueOf); + } + + private Optional getBooleanPropertyValue( + @NonNull StoveStatus stoveStatus, @NonNull final String propertyName) { + return getPropertyValue(stoveStatus, propertyName).map(Boolean::valueOf); + } +} diff --git a/rika2mqtt-flux-metrics-plugin/src/main/java/dev/cookiecode/rika2mqtt/plugins/influxdb/metrics/reflection/ReflectionUtils.java b/rika2mqtt-flux-metrics-plugin/src/main/java/dev/cookiecode/rika2mqtt/plugins/influxdb/metrics/reflection/ReflectionUtils.java new file mode 100644 index 00000000..ed726bc4 --- /dev/null +++ b/rika2mqtt-flux-metrics-plugin/src/main/java/dev/cookiecode/rika2mqtt/plugins/influxdb/metrics/reflection/ReflectionUtils.java @@ -0,0 +1,75 @@ +/* + * The MIT License + * Copyright © 2022 Sebastien Vermeille + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package dev.cookiecode.rika2mqtt.plugins.influxdb.metrics.reflection; + +import java.lang.reflect.Method; +import lombok.NonNull; +import lombok.experimental.UtilityClass; + +@UtilityClass +public class ReflectionUtils { + + /** + * @param clazz the class owning the property + * @param propertyName the property name + * @return true when the property of the class is a primitive boolean type + */ + public static boolean isBooleanPrimitiveProperty( + @NonNull Class clazz, @NonNull String propertyName) { + final var booleanGetterMethodName = + "is" + propertyName.substring(0, 1).toUpperCase() + propertyName.substring(1); + try { + // TODO: exception driven code... code smell (more elegant solution should be privileged) + clazz.getMethod(booleanGetterMethodName); + return true; + } catch (NoSuchMethodException e) { + return false; + } + } + + public static String getPropertyGetterMethodName( + @NonNull Class clazz, @NonNull String propertyName) { + if (propertyName.isEmpty()) { + throw new IllegalArgumentException( + "propertyName is empty. Please provide a valid propertyName."); + } + String getterMethodName; + + if (isBooleanPrimitiveProperty(clazz, propertyName)) { + getterMethodName = + "is" + propertyName.substring(0, 1).toUpperCase() + propertyName.substring(1); + } else { + getterMethodName = + "get" + propertyName.substring(0, 1).toUpperCase() + propertyName.substring(1); + } + + return getterMethodName; + } + + public static Method getPropertyGetterMethod( + @NonNull Class clazz, @NonNull String propertyName) throws NoSuchMethodException { + final var propertyGetterMethodName = getPropertyGetterMethodName(clazz, propertyName); + + return clazz.getMethod(propertyGetterMethodName); + } +} diff --git a/rika2mqtt-flux-metrics-plugin/src/main/resources/application.conf b/rika2mqtt-flux-metrics-plugin/src/main/resources/application.conf new file mode 100644 index 00000000..c02f0238 --- /dev/null +++ b/rika2mqtt-flux-metrics-plugin/src/main/resources/application.conf @@ -0,0 +1,69 @@ +# ====================================== # +# kamon-influxdb reference configuration # +# ====================================== # + +kamon { + influxdb { + + # Hostname and port in which your InfluxDB is running + hostname = "127.0.0.1" + port = 8086 + + # The database where to write in InfluxDB. + database = "mydb" + + # For histograms, which percentiles to count + percentiles = [50.0, 70.0, 90.0, 95.0, 99.0, 99.9] + + # The protocol to use when used to connect to your InfluxDB: HTTP/HTTPS + protocol = "http" + # Whether or not to submit distributions with count = 0 to influxdb (with 0 values) + post-empty-distributions = false + + # The precision to report the period timestamp in. Corresponds with what influx will accept, minus hours and minutes + # [ns,u,µ,ms,s] + precision = "s" + + # Client authentication credentials for connection to the InfluxDB server. There is no authentication by default. + # You can enable authentication by adding an authentication section to your configuration file with either token or + # user/password settings in it. If you specify both, token authentication will be used. + # + # authentication { + # token = "your_api_token" + # + # // OR + # + # user = "user" + # password = "password" + # + # } + + # Allow including environment information as tags on all reported metrics. + environment-tags { + + # Define whether specific environment settings will be included as tags in all exposed metrics. When enabled, + # the service, host and instance tags will be added using the values from Kamon.environment(). + include-service = no + include-host = no + include-instance = no + + # Specifies which Kamon environment tags should be ignored. All unmatched tags will be always added to al metrics. + exclude = [] + } + + tag-filter { + includes = ["**"] + excludes = [] + } + + } + + modules { + influxdb { + enabled = true + name = "InfluxReporter" + description = "Influxdb http reporter" + factory = "kamon.influxdb.InfluxDBReporterFactory" + } + } +} diff --git a/rika2mqtt-flux-metrics-plugin/src/test/java/dev/cookiecode/rika2mqtt/plugins/influxdb/metrics/Rika2MqttInfluxMetricsPluginTest.java b/rika2mqtt-flux-metrics-plugin/src/test/java/dev/cookiecode/rika2mqtt/plugins/influxdb/metrics/Rika2MqttInfluxMetricsPluginTest.java new file mode 100644 index 00000000..ec60a102 --- /dev/null +++ b/rika2mqtt-flux-metrics-plugin/src/test/java/dev/cookiecode/rika2mqtt/plugins/influxdb/metrics/Rika2MqttInfluxMetricsPluginTest.java @@ -0,0 +1,87 @@ +/* + * The MIT License + * Copyright © 2022 Sebastien Vermeille + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package dev.cookiecode.rika2mqtt.plugins.influxdb.metrics; + +import static org.mockito.Mockito.*; + +import com.typesafe.config.Config; +import kamon.Kamon; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +/** Test class */ +@ExtendWith(MockitoExtension.class) +class Rika2MqttInfluxMetricsPluginTest { + + private Rika2MqttInfluxMetricsPlugin plugin; + + @BeforeEach + public void beforeEach() { + plugin = spy(new Rika2MqttInfluxMetricsPlugin()); + } + + @Mock private Config config; + + @Test + void startShouldInitKamon() { + // GIVEN + doReturn(config).when(plugin).loadConfig(); + + try (var mockedKamon = mockStatic(Kamon.class)) { + // WHEN + plugin.start(); + + // THEN + mockedKamon.verify(Kamon::init, times(1)); + } + } + + @Test + void startShouldLoadKamonModules() { + // GIVEN + doReturn(config).when(plugin).loadConfig(); + + try (var mockedKamon = mockStatic(Kamon.class)) { + // WHEN + plugin.start(); + + // THEN + mockedKamon.verify(Kamon::loadModules, times(1)); + } + } + + @Test + void stopShouldStopKamon() { + // GIVEN + try (var mockedKamon = mockStatic(Kamon.class)) { + // WHEN + plugin.stop(); + + // THEN + mockedKamon.verify(Kamon::stop, times(1)); + } + } +} diff --git a/rika2mqtt-flux-metrics-plugin/src/test/java/dev/cookiecode/rika2mqtt/plugins/influxdb/metrics/reflection/ReflectionUtilsTest.java b/rika2mqtt-flux-metrics-plugin/src/test/java/dev/cookiecode/rika2mqtt/plugins/influxdb/metrics/reflection/ReflectionUtilsTest.java new file mode 100644 index 00000000..339f9ceb --- /dev/null +++ b/rika2mqtt-flux-metrics-plugin/src/test/java/dev/cookiecode/rika2mqtt/plugins/influxdb/metrics/reflection/ReflectionUtilsTest.java @@ -0,0 +1,116 @@ +/* + * The MIT License + * Copyright © 2022 Sebastien Vermeille + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package dev.cookiecode.rika2mqtt.plugins.influxdb.metrics.reflection; + +import static dev.cookiecode.rika2mqtt.plugins.influxdb.metrics.reflection.ReflectionUtils.getPropertyGetterMethodName; +import static dev.cookiecode.rika2mqtt.plugins.influxdb.metrics.reflection.ReflectionUtils.isBooleanPrimitiveProperty; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import org.junit.jupiter.api.Test; + +/** Test class */ +class ReflectionUtilsTest { + + @Test + void isBooleanPropertyShouldReturnFalseGivenThePropertyIsABooleanObjectNotPrimitive() { + + // GIVEN + final var clazz = TestedClass.class; + final var booleanObjectPropertyName = "enabled"; + + // WHEN + final var result = isBooleanPrimitiveProperty(clazz, booleanObjectPropertyName); + + // THEN + assertThat(result).isFalse(); + } + + @Test + void isBooleanPropertyShouldReturnTrueGivenThePropertyIsABooleanPrimitive() { + + // GIVEN + final var clazz = TestedClass.class; + final var booleanPrimitivePropertyName = "disabled"; + + // WHEN + final var result = isBooleanPrimitiveProperty(clazz, booleanPrimitivePropertyName); + + // THEN + assertThat(result).isTrue(); + } + + @Test + void isBooleanPropertyShouldReturnFalseGivenThePropertyIsAString() { + + // GIVEN + final var clazz = TestedClass.class; + final var stringPropertyName = "name"; + + // WHEN + final var result = isBooleanPrimitiveProperty(clazz, stringPropertyName); + + // THEN + assertThat(result).isFalse(); + } + + @Test + void + getPropertyGetterMethodNameShouldReturnAGetterStartingWithIsGivenAPrimitiveBooleanProperty() { + + // GIVEN + final var clazz = TestedClass.class; + final var primitiveBooleanPropertyName = "disabled"; + + // WHEN + final var result = getPropertyGetterMethodName(clazz, primitiveBooleanPropertyName); + + // THEN + assertThat(result).startsWith("is").isEqualTo("isDisabled"); + } + + @Test + void getPropertyGetterMethodNameShouldReturnAGetterStartingWithIsGivenAnObjectBooleanProperty() { + + // GIVEN + final var clazz = TestedClass.class; + final var objectBooleanPropertyName = "enabled"; + + // WHEN + final var result = getPropertyGetterMethodName(clazz, objectBooleanPropertyName); + + // THEN + assertThat(result).startsWith("get").isEqualTo("getEnabled"); + } + + @Test + void getPropertyGetterMethodNameShouldThrowAnExceptionGivenEmptyPropertyName() { + // GIVEN + final var clazz = TestedClass.class; + final var emptyPropertyName = ""; + + // WHEN/THEN + assertThatThrownBy(() -> getPropertyGetterMethodName(clazz, emptyPropertyName)) + .isInstanceOf(IllegalArgumentException.class); + } +} diff --git a/rika2mqtt-flux-metrics-plugin/src/test/java/dev/cookiecode/rika2mqtt/plugins/influxdb/metrics/reflection/TestedClass.java b/rika2mqtt-flux-metrics-plugin/src/test/java/dev/cookiecode/rika2mqtt/plugins/influxdb/metrics/reflection/TestedClass.java new file mode 100644 index 00000000..0a460a59 --- /dev/null +++ b/rika2mqtt-flux-metrics-plugin/src/test/java/dev/cookiecode/rika2mqtt/plugins/influxdb/metrics/reflection/TestedClass.java @@ -0,0 +1,32 @@ +/* + * The MIT License + * Copyright © 2022 Sebastien Vermeille + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package dev.cookiecode.rika2mqtt.plugins.influxdb.metrics.reflection; + +import lombok.Getter; + +@Getter +public class TestedClass { + private Boolean enabled; + private boolean disabled; + private String name; +} diff --git a/scripts/codestyle.sh b/scripts/codestyle.sh new file mode 100755 index 00000000..fa17eb63 --- /dev/null +++ b/scripts/codestyle.sh @@ -0,0 +1,33 @@ +#!/usr/bin/env bash +# +# The MIT License +# Copyright © 2022 Sebastien Vermeille +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# + + +set -e + +echo "Check missing headers" +./mvnw license:format + +echo "Check code style and auto format" +./mvnw com.spotify.fmt:fmt-maven-plugin:format +