]> git.basschouten.com Git - openhab-addons.git/commitdiff
[fenecon] Initial contribution (#17174)
authorPhilipp S. <16479847+nixoso@users.noreply.github.com>
Sun, 8 Sep 2024 12:48:26 +0000 (14:48 +0200)
committerGitHub <noreply@github.com>
Sun, 8 Sep 2024 12:48:26 +0000 (14:48 +0200)
* Initial implementation of the FENECON Binding

Signed-off-by: Philipp Schneider <philipp.schneider@nixo-soft.de>
26 files changed:
CODEOWNERS
bom/openhab-addons/pom.xml
bundles/org.openhab.binding.fenecon/NOTICE [new file with mode: 0644]
bundles/org.openhab.binding.fenecon/README.md [new file with mode: 0644]
bundles/org.openhab.binding.fenecon/pom.xml [new file with mode: 0644]
bundles/org.openhab.binding.fenecon/src/main/feature/feature.xml [new file with mode: 0644]
bundles/org.openhab.binding.fenecon/src/main/java/org/openhab/binding/fenecon/internal/FeneconBindingConstants.java [new file with mode: 0644]
bundles/org.openhab.binding.fenecon/src/main/java/org/openhab/binding/fenecon/internal/FeneconConfiguration.java [new file with mode: 0644]
bundles/org.openhab.binding.fenecon/src/main/java/org/openhab/binding/fenecon/internal/FeneconHandler.java [new file with mode: 0644]
bundles/org.openhab.binding.fenecon/src/main/java/org/openhab/binding/fenecon/internal/FeneconHandlerFactory.java [new file with mode: 0644]
bundles/org.openhab.binding.fenecon/src/main/java/org/openhab/binding/fenecon/internal/api/BatteryPower.java [new file with mode: 0644]
bundles/org.openhab.binding.fenecon/src/main/java/org/openhab/binding/fenecon/internal/api/FeneconController.java [new file with mode: 0644]
bundles/org.openhab.binding.fenecon/src/main/java/org/openhab/binding/fenecon/internal/api/FeneconResponse.java [new file with mode: 0644]
bundles/org.openhab.binding.fenecon/src/main/java/org/openhab/binding/fenecon/internal/api/GridPower.java [new file with mode: 0644]
bundles/org.openhab.binding.fenecon/src/main/java/org/openhab/binding/fenecon/internal/api/State.java [new file with mode: 0644]
bundles/org.openhab.binding.fenecon/src/main/java/org/openhab/binding/fenecon/internal/exception/FeneconAuthenticationException.java [new file with mode: 0644]
bundles/org.openhab.binding.fenecon/src/main/java/org/openhab/binding/fenecon/internal/exception/FeneconCommunicationException.java [new file with mode: 0644]
bundles/org.openhab.binding.fenecon/src/main/java/org/openhab/binding/fenecon/internal/exception/FeneconException.java [new file with mode: 0644]
bundles/org.openhab.binding.fenecon/src/main/resources/OH-INF/addon/addon.xml [new file with mode: 0644]
bundles/org.openhab.binding.fenecon/src/main/resources/OH-INF/i18n/fenecon.properties [new file with mode: 0644]
bundles/org.openhab.binding.fenecon/src/main/resources/OH-INF/thing/thing-types.xml [new file with mode: 0644]
bundles/org.openhab.binding.fenecon/src/test/java/org/openhab/binding/fenecon/internal/FeneconBindingConstantsTest.java [new file with mode: 0644]
bundles/org.openhab.binding.fenecon/src/test/java/org/openhab/binding/fenecon/internal/api/BatteryPowerTest.java [new file with mode: 0644]
bundles/org.openhab.binding.fenecon/src/test/java/org/openhab/binding/fenecon/internal/api/GridPowerTest.java [new file with mode: 0644]
bundles/org.openhab.binding.fenecon/src/test/java/org/openhab/binding/fenecon/internal/api/StateTest.java [new file with mode: 0644]
bundles/pom.xml

index 8395b0fea8c8b0478d53468d9cb6c8c9cd2cc526..2c497dafe73baaba85574ea8c000507082b9a9e7 100755 (executable)
 /bundles/org.openhab.binding.exec/ @kgoderis
 /bundles/org.openhab.binding.feed/ @openhab/add-ons-maintainers
 /bundles/org.openhab.binding.feican/ @Hilbrand
+/bundles/org.openhab.binding.fenecon/ @nixoso
 /bundles/org.openhab.binding.fineoffsetweatherstation/ @Andy2003
 /bundles/org.openhab.binding.flicbutton/ @pfink
 /bundles/org.openhab.binding.fmiweather/ @ssalonen
index ead89e39d037b1223890410c7c95753d319a9a46..d9d76b04fe2f9c465f74c2d52832657a2b45b2ab 100644 (file)
       <artifactId>org.openhab.binding.feican</artifactId>
       <version>${project.version}</version>
     </dependency>
+    <dependency>
+      <groupId>org.openhab.addons.bundles</groupId>
+      <artifactId>org.openhab.binding.fenecon</artifactId>
+      <version>${project.version}</version>
+    </dependency>
     <dependency>
       <groupId>org.openhab.addons.bundles</groupId>
       <artifactId>org.openhab.binding.fineoffsetweatherstation</artifactId>
diff --git a/bundles/org.openhab.binding.fenecon/NOTICE b/bundles/org.openhab.binding.fenecon/NOTICE
new file mode 100644 (file)
index 0000000..38d625e
--- /dev/null
@@ -0,0 +1,13 @@
+This content is produced and maintained by the openHAB project.
+
+* Project home: https://www.openhab.org
+
+== Declared Project Licenses
+
+This program and the accompanying materials are made available under the terms
+of the Eclipse Public License 2.0 which is available at
+https://www.eclipse.org/legal/epl-2.0/.
+
+== Source Code
+
+https://github.com/openhab/openhab-addons
diff --git a/bundles/org.openhab.binding.fenecon/README.md b/bundles/org.openhab.binding.fenecon/README.md
new file mode 100644 (file)
index 0000000..a2369af
--- /dev/null
@@ -0,0 +1,160 @@
+# FENECON Binding
+
+The FENECON Binding integrates the [FENECON energy storage system](https://fenecon.de/) device into the openHAB system via [REST-API](https://docs.fenecon.de/_/de/fems/fems-app/OEM_App_REST_JSON.html).
+
+With the binding, it is possible to request status information from FENECON Home to allow you home automation decisions based on the current energy management.
+
+This makes it possible, for example, to switch on other consumers such as the dishwasher or washing machine in the case of power overproduction.
+
+## Supported Things
+
+Currently only one Thing is supported: The `home-device` connection to the FENECON energy storage system.
+
+This Binding was tested with an [FENECON HOME 10](https://fenecon.de/fenecon-home-10/) device.
+
+## Discovery
+
+Auto-discovery is not supported.
+
+## Thing Configuration
+
+The FENECON Thing only needs to be configured with the `hostname`, all other parameters are optional and prefilled with the suitable default values:
+
+| Parameter       | Description                                                                      |
+|-----------------|----------------------------------------------------------------------------------|
+| hostname        | Hostname or IP address of the FENECON device, e.g. 192.168.1.11                  |
+| password        | Password of the FENECON device. The password for guest access is set by default. |
+| port            | Port of the FENECON device. Default: 8084                                        |
+| refreshInterval | Interval the device is polled in sec. Default 30 seconds                         |
+
+## Channels
+
+The FENECON binding currently only provides access to read out the values from the energy storage system. 
+
+| Channel                       | Type                 | Read/Write | Description                                                                 |
+|-------------------------------|----------------------|------------|-----------------------------------------------------------------------------|
+| state                         | String               | R          | FENECON system state: Ok, Info, Warning or Fault                            |
+| last-update                   | DateTime             | R          | Last successful update via REST-API from the FENECON system                 |
+| ess-soc                       | Number:Dimensionless | R          | Battery state of charge in percent                                          |
+| charger-power                 | Number:Power         | R          | Current charger power of energy storage system in watt.                     |
+| discharger-power              | Number:Power         | R          | Current discharger power of energy storage system in watt.                  |
+| emergency-power-mode          | Switch               | R          | Indicates if there is grid power is off and the emergency power mode is on. |
+| production-active-power       | Number:Power         | R          | Current active power producer load in watt.                                 |
+| production-max-active-power   | Number:Power         | R          | Maximum active production power in watt that was measured.                  |
+| export-to-grid-power          | Number:Power         | R          | Current export power to grid in watt.                                       |
+| exported-to-grid-energy       | Number:Energy        | R          | Total energy exported to the grid in watt per hour.                         |
+| consumption-active-power      | Number:Power         | R          | Current active power consumer load in watt.                                 |
+| consumption-max-active-power  | Number:Power         | R          | Maximum active consumption power in watt that was measured.                 |
+| consumption-active-power-l1   | Number:Power         | R          | Current active power consumer load in watt on phase 1.                      |
+| consumption-active-power-l2   | Number:Power         | R          | Current active power consumer load in watt on phase 2.                      |
+| consumption-active-power-l3   | Number:Power         | R          | Current active power consumer load in watt on phase 3.                      |
+| import-from-grid-power        | Number:Power         | R          | Current import power from grid in watt.                                     |
+| imported-from-grid-energy     | Number:Energy        | R          | Total energy imported from the grid in watt per hour.                       |
+
+## Full Example
+
+### fenecon.things
+
+```java
+Thing fenecon:home-device:local "FENECON Home" [hostname="192.168.1.11", refreshInterval=5]
+```
+
+### demo.items
+
+```java
+// Sitemap Items
+Group   Home                    "MyHome"              <house>                                 ["Indoor"]
+Group   GF                      "GroundFloor"         <groundfloor>          (Home)           ["GroundFloor"]
+// Utility room
+Group   GF_UtilityRoom          "Utility room"        <energy>               (Home, GF)       ["Room"]
+Group   GF_UtilityRoomSolar     "Utility room solar"  <solarplant>           (GF_UtilityRoom) ["Inverter"]
+
+// FENECON items
+String               EssState                       <text>         (GF_UtilityRoomSolar) ["Status"]                {channel="fenecon:home-device:local:state"}
+DateTime             LastFeneconUpdate              <time>         (GF_UtilityRoomSolar) ["Status"]                {channel="fenecon:home-device:local:last-update"}
+Number:Dimensionless EssSoc                         <batterylevel> (GF_UtilityRoomSolar) ["Measurement"]           {unit="%", channel="fenecon:home-device:local:ess-soc"}
+Number:Power         ChargerPower                   <energy>       (GF_UtilityRoomSolar) ["Measurement", "Power"]  {channel="fenecon:home-device:local:charger-power"}
+Number:Power         DischargerPower                <energy>       (GF_UtilityRoomSolar) ["Measurement", "Power"]  {channel="fenecon:home-device:local:discharger-power"}
+Switch               EmergencyPowerMode             <switch>       (GF_UtilityRoomSolar) ["Switch"]                {channel="fenecon:home-device:local:emergency-power-mode"}
+
+Number:Power         ProductionActivePower          <energy>       (GF_UtilityRoomSolar) ["Measurement", "Power"]  {channel="fenecon:home-device:local:production-active-power"}
+Number:Power         ProductionMaxActivePower       <energy>       (GF_UtilityRoomSolar) ["Measurement", "Power"]  {channel="fenecon:home-device:local:production-max-active-power"}
+Number:Power         SellToGridPower                <energy>       (GF_UtilityRoomSolar) ["Measurement", "Power"]  {channel="fenecon:home-device:local:export-to-grid-power"}
+Number:Energy        TotalSellEnergy                <energy>       (GF_UtilityRoomSolar) ["Measurement", "Energy"] {channel="fenecon:home-device:local:exported-to-grid-energy"}
+
+Number:Power         ConsumptionActivePower         <energy>       (GF_UtilityRoomSolar) ["Measurement", "Power"]  {channel="fenecon:home-device:local:consumption-active-power"}
+Number:Power         ConsumptionMaxActivePower      <energy>       (GF_UtilityRoomSolar) ["Measurement", "Power"]  {channel="fenecon:home-device:local:consumption-max-active-power"}
+Number:Power         ConsumptionActivePowerPhase1   <energy>       (GF_UtilityRoomSolar) ["Measurement", "Power"]  {channel="fenecon:home-device:local:consumption-active-power-l1"}
+Number:Power         ConsumptionActivePowerPhase2   <energy>       (GF_UtilityRoomSolar) ["Measurement", "Power"]  {channel="fenecon:home-device:local:consumption-active-power-l2"}
+Number:Power         ConsumptionActivePowerPhase3   <energy>       (GF_UtilityRoomSolar) ["Measurement", "Power"]  {channel="fenecon:home-device:local:consumption-active-power-l3"}
+Number:Power         BuyFromGridPower               <energy>       (GF_UtilityRoomSolar) ["Measurement", "Power"]  {channel="fenecon:home-device:local:import-from-grid-power"}
+Number:Energy        TotalBuyEnergy                 <energy>       (GF_UtilityRoomSolar) ["Measurement", "Energy"] {channel="fenecon:home-device:local:imported-from-grid-energy"}
+
+// Examples of items for calculating the energy purchased and sold. Look at the demo.rules section.
+Number:Currency      SoldEnergy "Total sold energy [%.2f €]"           <price> (GF_UtilityRoomSolar)
+Number:Currency      PurchasedEnergy "Total purchased energy [%.2f €]" <price> (GF_UtilityRoomSolar)
+
+```
+
+### demo.sitemap
+
+```perl
+sitemap demo label="FENECON Example Sitemap" {
+    Frame label="Groundfloor" icon="groundfloor" {
+        Group item=GF_UtilityRoom
+    }
+}
+```
+
+### demo.rules
+
+:::: tabs
+
+::: tab DSL
+
+```java
+rule "Blackout detection"
+when
+  Item EmergencyPowerMode changed to ON
+then
+    val msg = "🚨 Power blackout detected, emergency power mode running." 
+    logInfo("PowerBlackout", msg)
+    sendBroadcastNotification(msg)
+end
+
+rule "Battery 100 percent"
+when
+  Item EssSoc changed
+then
+    var batteryState = (EssSoc.getState() as Number).intValue()
+    if(batteryState == 100){
+        val msg = "🔋 Full battery, consumers can be activated."
+        logInfo("FullBattery", msg)
+        sendBroadcastNotification(msg)
+    }
+end
+
+rule "Calculation sold energy"
+when
+  Item TotalSellEnergy changed
+then
+    val sellingPricePerKiloWattHour = 0.07 // €
+    var current = (TotalSellEnergy.getState() as Number).intValue()
+    var result = current * sellingPricePerKiloWattHour;
+    SoldEnergy.postUpdate(result)
+end
+
+rule "Calculation purchased energy"
+when
+  Item TotalBuyEnergy changed
+then
+    val purchasedPricePerKiloWattHour = 0.32 // €
+    var current = (TotalBuyEnergy.getState() as Number).intValue()
+    var result = current * purchasedPricePerKiloWattHour;
+    PurchasedEnergy.postUpdate(result)
+end
+```
+
+:::
+
+::::
diff --git a/bundles/org.openhab.binding.fenecon/pom.xml b/bundles/org.openhab.binding.fenecon/pom.xml
new file mode 100644 (file)
index 0000000..fd76dac
--- /dev/null
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
+  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
+
+  <modelVersion>4.0.0</modelVersion>
+
+  <parent>
+    <groupId>org.openhab.addons.bundles</groupId>
+    <artifactId>org.openhab.addons.reactor.bundles</artifactId>
+    <version>4.3.0-SNAPSHOT</version>
+  </parent>
+
+  <artifactId>org.openhab.binding.fenecon</artifactId>
+
+  <name>openHAB Add-ons :: Bundles :: FENECON Binding</name>
+
+</project>
diff --git a/bundles/org.openhab.binding.fenecon/src/main/feature/feature.xml b/bundles/org.openhab.binding.fenecon/src/main/feature/feature.xml
new file mode 100644 (file)
index 0000000..fc124c7
--- /dev/null
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<features name="org.openhab.binding.fenecon-${project.version}" xmlns="http://karaf.apache.org/xmlns/features/v1.4.0">
+       <repository>mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features</repository>
+
+       <feature name="openhab-binding-fenecon" description="FENECON Binding" version="${project.version}">
+               <feature>openhab-runtime-base</feature>
+               <bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.fenecon/${project.version}</bundle>
+       </feature>
+</features>
diff --git a/bundles/org.openhab.binding.fenecon/src/main/java/org/openhab/binding/fenecon/internal/FeneconBindingConstants.java b/bundles/org.openhab.binding.fenecon/src/main/java/org/openhab/binding/fenecon/internal/FeneconBindingConstants.java
new file mode 100644 (file)
index 0000000..d8218ab
--- /dev/null
@@ -0,0 +1,75 @@
+/**
+ * Copyright (c) 2010-2024 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.fenecon.internal;
+
+import java.util.List;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.core.thing.ThingTypeUID;
+
+/**
+ * The {@link FeneconBindingConstants} class defines common constants, which are
+ * used across the whole binding.
+ *
+ * @author Philipp Schneider - Initial contribution
+ */
+@NonNullByDefault
+public class FeneconBindingConstants {
+
+    private static final String BINDING_ID = "fenecon";
+
+    // List of all Thing Type UIDs
+    public static final ThingTypeUID THING_TYPE_HOME_DEVICE = new ThingTypeUID(BINDING_ID, "home-device");
+
+    // List of all FENECON Addresses
+    public static final String STATE_ADDRESS = "_sum/State";
+    public static final String ESS_SOC_ADDRESS = "_sum/EssSoc";
+    public static final String CONSUMPTION_ACTIVE_POWER_ADDRESS = "_sum/ConsumptionActivePower";
+    public static final String CONSUMPTION_ACTIVE_POWER_PHASE1_ADDRESS = "_sum/ConsumptionActivePowerL1";
+    public static final String CONSUMPTION_ACTIVE_POWER_PHASE2_ADDRESS = "_sum/ConsumptionActivePowerL2";
+    public static final String CONSUMPTION_ACTIVE_POWER_PHASE3_ADDRESS = "_sum/ConsumptionActivePowerL3";
+    public static final String CONSUMPTION_MAX_ACTIVE_POWER_ADDRESS = "_sum/ConsumptionMaxActivePower";
+    public static final String PRODUCTION_MAX_ACTIVE_POWER_ADDRESS = "_sum/ProductionMaxActivePower";
+    public static final String PRODUCTION_ACTIVE_POWER_ADDRESS = "_sum/ProductionActivePower";
+    public static final String GRID_ACTIVE_POWER_ADDRESS = "_sum/GridActivePower";
+    public static final String ESS_DISCHARGE_POWER_ADDRESS = "_sum/EssDischargePower";
+    public static final String GRID_MODE_ADDRESS = "_sum/GridMode";
+    public static final String GRID_SELL_ACTIVE_ENERGY_ADDRESS = "_sum/GridSellActiveEnergy";
+    public static final String GRID_BUY_ACTIVE_ENERGY_ADDRESS = "_sum/GridBuyActiveEnergy";
+    // Group of all FENECON Addresses
+    public static final List<String> ADDRESSES = List.of(STATE_ADDRESS, GRID_MODE_ADDRESS,
+            CONSUMPTION_ACTIVE_POWER_ADDRESS, CONSUMPTION_ACTIVE_POWER_PHASE1_ADDRESS,
+            CONSUMPTION_ACTIVE_POWER_PHASE2_ADDRESS, CONSUMPTION_ACTIVE_POWER_PHASE3_ADDRESS,
+            CONSUMPTION_MAX_ACTIVE_POWER_ADDRESS, PRODUCTION_MAX_ACTIVE_POWER_ADDRESS, PRODUCTION_ACTIVE_POWER_ADDRESS,
+            GRID_ACTIVE_POWER_ADDRESS, GRID_BUY_ACTIVE_ENERGY_ADDRESS, GRID_SELL_ACTIVE_ENERGY_ADDRESS, ESS_SOC_ADDRESS,
+            ESS_DISCHARGE_POWER_ADDRESS);
+
+    // List of all Channel IDs
+    public static final String STATE_CHANNEL = "state";
+    public static final String ESS_SOC_CHANNEL = "ess-soc";
+    public static final String CONSUMPTION_ACTIVE_POWER_CHANNEL = "consumption-active-power";
+    public static final String CONSUMPTION_ACTIVE_POWER_PHASE1_CHANNEL = "consumption-active-power-l1";
+    public static final String CONSUMPTION_ACTIVE_POWER_PHASE2_CHANNEL = "consumption-active-power-l2";
+    public static final String CONSUMPTION_ACTIVE_POWER_PHASE3_CHANNEL = "consumption-active-power-l3";
+    public static final String CONSUMPTION_MAX_ACTIVE_POWER_CHANNEL = "consumption-max-active-power";
+    public static final String PRODUCTION_MAX_ACTIVE_POWER_CHANNEL = "production-max-active-power";
+    public static final String PRODUCTION_ACTIVE_POWER_CHANNEL = "production-active-power";
+    public static final String EXPORT_TO_GRID_POWER_CHANNEL = "export-to-grid-power";
+    public static final String IMPORT_FROM_GRID_POWER_CHANNEL = "import-from-grid-power";
+    public static final String ESS_CHARGER_POWER_CHANNEL = "charger-power";
+    public static final String ESS_DISCHARGER_POWER_CHANNEL = "discharger-power";
+    public static final String EMERGENCY_POWER_MODE_CHANNEL = "emergency-power-mode";
+    public static final String EXPORTED_TO_GRID_ENERGY_CHANNEL = "exported-to-grid-energy";
+    public static final String IMPORTED_FROM_GRID_ENERGY_CHANNEL = "imported-from-grid-energy";
+    public static final String LAST_UPDATE_CHANNEL = "last-update";
+}
diff --git a/bundles/org.openhab.binding.fenecon/src/main/java/org/openhab/binding/fenecon/internal/FeneconConfiguration.java b/bundles/org.openhab.binding.fenecon/src/main/java/org/openhab/binding/fenecon/internal/FeneconConfiguration.java
new file mode 100644 (file)
index 0000000..8a5127f
--- /dev/null
@@ -0,0 +1,29 @@
+/**
+ * Copyright (c) 2010-2024 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.fenecon.internal;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * The {@link FeneconConfiguration} class contains fields mapping thing configuration parameters.
+ *
+ * @author Philipp Schneider - Initial contribution
+ */
+@NonNullByDefault
+public class FeneconConfiguration {
+
+    public String hostname = "";
+    public String password = "user";
+    public int port = 8084;
+    public int refreshInterval = 30;
+}
diff --git a/bundles/org.openhab.binding.fenecon/src/main/java/org/openhab/binding/fenecon/internal/FeneconHandler.java b/bundles/org.openhab.binding.fenecon/src/main/java/org/openhab/binding/fenecon/internal/FeneconHandler.java
new file mode 100644 (file)
index 0000000..4598079
--- /dev/null
@@ -0,0 +1,189 @@
+/**
+ * Copyright (c) 2010-2024 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.fenecon.internal;
+
+import java.util.Optional;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.TimeUnit;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.eclipse.jetty.client.HttpClient;
+import org.openhab.binding.fenecon.internal.api.BatteryPower;
+import org.openhab.binding.fenecon.internal.api.FeneconController;
+import org.openhab.binding.fenecon.internal.api.FeneconResponse;
+import org.openhab.binding.fenecon.internal.api.GridPower;
+import org.openhab.binding.fenecon.internal.api.State;
+import org.openhab.binding.fenecon.internal.exception.FeneconException;
+import org.openhab.core.library.types.DateTimeType;
+import org.openhab.core.library.types.OnOffType;
+import org.openhab.core.library.types.QuantityType;
+import org.openhab.core.library.types.StringType;
+import org.openhab.core.library.unit.Units;
+import org.openhab.core.thing.ChannelUID;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.thing.ThingStatus;
+import org.openhab.core.thing.ThingStatusDetail;
+import org.openhab.core.thing.binding.BaseThingHandler;
+import org.openhab.core.types.Command;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The {@link FeneconHandler} is responsible for handling commands, which are
+ * sent to one of the channels.
+ *
+ * @author Philipp Schneider - Initial contribution
+ */
+@NonNullByDefault
+public class FeneconHandler extends BaseThingHandler {
+
+    private final Logger logger = LoggerFactory.getLogger(FeneconHandler.class);
+
+    private FeneconConfiguration config = new FeneconConfiguration();
+    private @Nullable ScheduledFuture<?> pollingJob;
+    private @Nullable FeneconController feneconController;
+    private final HttpClient httpClient;
+
+    public FeneconHandler(Thing thing, HttpClient httpClient) {
+        super(thing);
+        this.httpClient = httpClient;
+    }
+
+    @Override
+    public void initialize() {
+        config = getConfigAs(FeneconConfiguration.class);
+
+        updateStatus(ThingStatus.UNKNOWN);
+
+        feneconController = new FeneconController(config, this.httpClient);
+        pollingJob = scheduler.scheduleWithFixedDelay(this::pollingCode, 0, config.refreshInterval, TimeUnit.SECONDS);
+    }
+
+    private void pollingCode() {
+        for (String eachChannel : FeneconBindingConstants.ADDRESSES) {
+            try {
+                @SuppressWarnings("null")
+                Optional<FeneconResponse> response = feneconController.requestChannel(eachChannel);
+
+                if (response.isPresent()) {
+                    processDataPoint(response.get());
+                }
+
+                updateStatus(ThingStatus.ONLINE);
+            } catch (FeneconException err) {
+                logger.trace("FENECON - connection problem on FENECON channel {}", eachChannel, err);
+                updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, err.getMessage());
+                return;
+            }
+        }
+
+        // Set last successful update cycle
+        updateState(FeneconBindingConstants.LAST_UPDATE_CHANNEL, new DateTimeType());
+    }
+
+    private void processDataPoint(FeneconResponse response) throws FeneconException {
+        switch (response.address()) {
+            case FeneconBindingConstants.STATE_ADDRESS:
+                // {"address":"_sum/State","type":"INTEGER","accessMode":"RO","text":"0:Ok, 1:Info, 2:Warning,
+                // 3:Fault","unit":"","value":0}
+                State state = State.get(response);
+                updateState(FeneconBindingConstants.STATE_CHANNEL, new StringType(state.state()));
+                break;
+            case FeneconBindingConstants.ESS_SOC_ADDRESS:
+                updateState(FeneconBindingConstants.ESS_SOC_CHANNEL,
+                        new QuantityType<>(Integer.valueOf(response.value()), Units.PERCENT));
+                break;
+            case FeneconBindingConstants.CONSUMPTION_ACTIVE_POWER_ADDRESS:
+                updateState(FeneconBindingConstants.CONSUMPTION_ACTIVE_POWER_CHANNEL,
+                        new QuantityType<>(Integer.valueOf(response.value()), Units.WATT));
+                break;
+            case FeneconBindingConstants.CONSUMPTION_ACTIVE_POWER_PHASE1_ADDRESS:
+                updateState(FeneconBindingConstants.CONSUMPTION_ACTIVE_POWER_PHASE1_CHANNEL,
+                        new QuantityType<>(Integer.valueOf(response.value()), Units.WATT));
+                break;
+            case FeneconBindingConstants.CONSUMPTION_ACTIVE_POWER_PHASE2_ADDRESS:
+                updateState(FeneconBindingConstants.CONSUMPTION_ACTIVE_POWER_PHASE2_CHANNEL,
+                        new QuantityType<>(Integer.valueOf(response.value()), Units.WATT));
+                break;
+            case FeneconBindingConstants.CONSUMPTION_ACTIVE_POWER_PHASE3_ADDRESS:
+                updateState(FeneconBindingConstants.CONSUMPTION_ACTIVE_POWER_PHASE3_CHANNEL,
+                        new QuantityType<>(Integer.valueOf(response.value()), Units.WATT));
+                break;
+            case FeneconBindingConstants.CONSUMPTION_MAX_ACTIVE_POWER_ADDRESS:
+                updateState(FeneconBindingConstants.CONSUMPTION_MAX_ACTIVE_POWER_CHANNEL,
+                        new QuantityType<>(Integer.valueOf(response.value()), Units.WATT));
+                break;
+            case FeneconBindingConstants.PRODUCTION_MAX_ACTIVE_POWER_ADDRESS:
+                updateState(FeneconBindingConstants.PRODUCTION_MAX_ACTIVE_POWER_CHANNEL,
+                        new QuantityType<>(Integer.valueOf(response.value()), Units.WATT));
+                break;
+            case FeneconBindingConstants.PRODUCTION_ACTIVE_POWER_ADDRESS:
+                updateState(FeneconBindingConstants.PRODUCTION_ACTIVE_POWER_CHANNEL,
+                        new QuantityType<>(Integer.valueOf(response.value()), Units.WATT));
+                break;
+            case FeneconBindingConstants.GRID_ACTIVE_POWER_ADDRESS:
+                // Grid exchange power. Negative values for sell-to-grid; positive for buy-from-grid"
+                GridPower gridPower = GridPower.get(response);
+
+                updateState(FeneconBindingConstants.EXPORT_TO_GRID_POWER_CHANNEL,
+                        new QuantityType<>(gridPower.sellTo(), Units.WATT));
+                updateState(FeneconBindingConstants.IMPORT_FROM_GRID_POWER_CHANNEL,
+                        new QuantityType<>(gridPower.buyFrom(), Units.WATT));
+                break;
+            case FeneconBindingConstants.ESS_DISCHARGE_POWER_ADDRESS:
+                // Actual AC-side battery discharge power of Energy Storage System.
+                // Negative values for charge; positive for discharge
+                BatteryPower batteryPower = BatteryPower.get(response);
+
+                updateState(FeneconBindingConstants.ESS_CHARGER_POWER_CHANNEL,
+                        new QuantityType<>(batteryPower.chargerPower(), Units.WATT));
+                updateState(FeneconBindingConstants.ESS_DISCHARGER_POWER_CHANNEL,
+                        new QuantityType<>(batteryPower.dischargerPower(), Units.WATT));
+                break;
+            case FeneconBindingConstants.GRID_MODE_ADDRESS:
+                // text":"1:On-Grid, 2:Off-Grid","unit":"","value":1
+                Integer gridMod = Integer.valueOf(response.value());
+                updateState(FeneconBindingConstants.EMERGENCY_POWER_MODE_CHANNEL,
+                        gridMod == 2 ? OnOffType.ON : OnOffType.OFF);
+                break;
+            case FeneconBindingConstants.GRID_SELL_ACTIVE_ENERGY_ADDRESS:
+                // {"address":"_sum/GridSellActiveEnergy","type":"LONG","accessMode":"RO","text":"","unit":"Wh_Σ","value":374242}
+                updateState(FeneconBindingConstants.EXPORTED_TO_GRID_ENERGY_CHANNEL,
+                        new QuantityType<>(Integer.valueOf(response.value()), Units.WATT_HOUR));
+                break;
+            case FeneconBindingConstants.GRID_BUY_ACTIVE_ENERGY_ADDRESS:
+                // "address":"_sum/GridBuyActiveEnergy","type":"LONG","accessMode":"RO","text":"","unit":"Wh_Σ","value":1105}
+                updateState(FeneconBindingConstants.IMPORTED_FROM_GRID_ENERGY_CHANNEL,
+                        new QuantityType<>(Integer.valueOf(response.value()), Units.WATT_HOUR));
+                break;
+            default:
+                logger.trace("FENECON - No channel ID to address {} found.", response.address());
+                break;
+        }
+    }
+
+    @Override
+    public void dispose() {
+        ScheduledFuture<?> job = pollingJob;
+        if (job != null) {
+            job.cancel(true);
+            pollingJob = null;
+        }
+    }
+
+    @Override
+    public void handleCommand(ChannelUID channelUID, Command command) {
+        // Noop
+    }
+}
diff --git a/bundles/org.openhab.binding.fenecon/src/main/java/org/openhab/binding/fenecon/internal/FeneconHandlerFactory.java b/bundles/org.openhab.binding.fenecon/src/main/java/org/openhab/binding/fenecon/internal/FeneconHandlerFactory.java
new file mode 100644 (file)
index 0000000..0ac1eaa
--- /dev/null
@@ -0,0 +1,64 @@
+/**
+ * Copyright (c) 2010-2024 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.fenecon.internal;
+
+import static org.openhab.binding.fenecon.internal.FeneconBindingConstants.THING_TYPE_HOME_DEVICE;
+
+import java.util.Set;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.core.io.net.http.HttpClientFactory;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.thing.ThingTypeUID;
+import org.openhab.core.thing.binding.BaseThingHandlerFactory;
+import org.openhab.core.thing.binding.ThingHandler;
+import org.openhab.core.thing.binding.ThingHandlerFactory;
+import org.osgi.service.component.annotations.Activate;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Reference;
+
+/**
+ * The {@link FeneconHandlerFactory} is responsible for creating things and thing
+ * handlers.
+ *
+ * @author Philipp Schneider - Initial contribution
+ */
+@NonNullByDefault
+@Component(configurationPid = "binding.fenecon", service = ThingHandlerFactory.class)
+public class FeneconHandlerFactory extends BaseThingHandlerFactory {
+
+    private static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Set.of(THING_TYPE_HOME_DEVICE);
+    private final HttpClientFactory httpClientFactory;
+
+    @Activate
+    public FeneconHandlerFactory(@Reference HttpClientFactory httpClientFactory) {
+        this.httpClientFactory = httpClientFactory;
+    }
+
+    @Override
+    public boolean supportsThingType(ThingTypeUID thingTypeUID) {
+        return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID);
+    }
+
+    @Override
+    protected @Nullable ThingHandler createHandler(Thing thing) {
+        ThingTypeUID thingTypeUID = thing.getThingTypeUID();
+
+        if (THING_TYPE_HOME_DEVICE.equals(thingTypeUID)) {
+            return new FeneconHandler(thing, httpClientFactory.getCommonHttpClient());
+        }
+
+        return null;
+    }
+}
diff --git a/bundles/org.openhab.binding.fenecon/src/main/java/org/openhab/binding/fenecon/internal/api/BatteryPower.java b/bundles/org.openhab.binding.fenecon/src/main/java/org/openhab/binding/fenecon/internal/api/BatteryPower.java
new file mode 100644 (file)
index 0000000..01587ef
--- /dev/null
@@ -0,0 +1,39 @@
+/**
+ * Copyright (c) 2010-2024 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.fenecon.internal.api;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * The {@link BatteryPower} is a small helper class to convert the power value from battery.
+ *
+ * @author Philipp Schneider - Initial contribution
+ */
+@NonNullByDefault
+public record BatteryPower(int chargerPower, int dischargerPower) {
+
+    public static BatteryPower get(FeneconResponse response) {
+        // Actual AC-side battery discharge power of Energy Storage System.
+        // Negative values for charge; positive for discharge
+        Integer powerValue = Integer.valueOf(response.value());
+        int chargerPower = 0;
+        int dischargerPower = 0;
+        if (powerValue < 0) {
+            chargerPower = powerValue * -1;
+        } else {
+            dischargerPower = powerValue;
+        }
+
+        return new BatteryPower(chargerPower, dischargerPower);
+    }
+}
diff --git a/bundles/org.openhab.binding.fenecon/src/main/java/org/openhab/binding/fenecon/internal/api/FeneconController.java b/bundles/org.openhab.binding.fenecon/src/main/java/org/openhab/binding/fenecon/internal/api/FeneconController.java
new file mode 100644 (file)
index 0000000..36fff36
--- /dev/null
@@ -0,0 +1,125 @@
+/**
+ * Copyright (c) 2010-2024 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.fenecon.internal.api;
+
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.Optional;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jetty.client.HttpClient;
+import org.eclipse.jetty.client.api.AuthenticationStore;
+import org.eclipse.jetty.client.api.ContentResponse;
+import org.eclipse.jetty.client.api.Request;
+import org.eclipse.jetty.client.util.BasicAuthentication;
+import org.eclipse.jetty.http.HttpMethod;
+import org.openhab.binding.fenecon.internal.FeneconConfiguration;
+import org.openhab.binding.fenecon.internal.exception.FeneconAuthenticationException;
+import org.openhab.binding.fenecon.internal.exception.FeneconCommunicationException;
+import org.openhab.binding.fenecon.internal.exception.FeneconException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParser;
+import com.google.gson.JsonSyntaxException;
+
+/**
+ * The {@link FeneconController} class provides API access to the FENECON system.
+ *
+ * @author Philipp Schneider - Initial contribution
+ */
+@NonNullByDefault
+public class FeneconController {
+
+    private final Logger logger = LoggerFactory.getLogger(FeneconController.class);
+
+    private final FeneconConfiguration config;
+    private final HttpClient httpClient;
+
+    public FeneconController(FeneconConfiguration config, HttpClient httpClient) {
+        this.config = config;
+        this.httpClient = httpClient;
+
+        logger.debug("FENECON: initialize REST-API connection to {} with polling interval: {} sec", getBaseUrl(config),
+                config.refreshInterval);
+
+        // Set BasicAuthentication for all requests on the http connection
+        AuthenticationStore auth = httpClient.getAuthenticationStore();
+        URI uri = URI.create(getBaseUrl(config));
+        auth.addAuthenticationResult(new BasicAuthentication.BasicResult(uri, "x", config.password));
+    }
+
+    private String getBaseUrl(FeneconConfiguration config) {
+        return "http://" + config.hostname + ":" + config.port + "/";
+    }
+
+    /**
+     * Queries the data for a specified channel.
+     *
+     * @param channel Channel to be queried, e.g. _sum/State .
+     * @return {@link FeneconResponse} can be optional if values are not available.
+     * @throws FeneconException is thrown if there are problems with the connection or processing of data to the FENECON
+     *             system.
+     */
+    public Optional<FeneconResponse> requestChannel(String channel) throws FeneconException {
+        try {
+            URI uri = new URI(getBaseUrl(config) + "rest/channel/" + channel);
+
+            Request request = httpClient.newRequest(uri).timeout(10, TimeUnit.SECONDS).method(HttpMethod.GET);
+            logger.trace("FENECON - request: {}", request);
+
+            ContentResponse response = request.send();
+            logger.trace("FENECON - response status code: {} body: {}", response.getStatus(),
+                    response.getContentAsString());
+
+            int statusCode = response.getStatus();
+            if (statusCode > 300) {
+                // Authentication error
+                if (statusCode == 401) {
+                    throw new FeneconAuthenticationException(
+                            "Authentication on the FENECON system was not possible. Check password.");
+                } else {
+                    throw new FeneconCommunicationException("Unexpected http status code: " + statusCode);
+                }
+            } else {
+                return createResponseFromJson(JsonParser.parseString(response.getContentAsString()).getAsJsonObject());
+            }
+        } catch (TimeoutException | ExecutionException | UnsupportedOperationException | InterruptedException err) {
+            throw new FeneconCommunicationException("Communication error with FENECON system on channel: " + channel,
+                    err);
+        } catch (URISyntaxException | JsonSyntaxException err) {
+            throw new FeneconCommunicationException("Syntax error on channel: " + channel, err);
+        }
+    }
+
+    private Optional<FeneconResponse> createResponseFromJson(JsonObject response) {
+        // Example response: {"address":"_sum/EssSoc","type":"INTEGER","accessMode":"RO","text":"Range
+        // 0..100","unit":"%","value":99}
+
+        if (response.get("value").isJsonNull()) {
+            // Example problem response: {"address":"_sum/EssSoc","type":"INTEGER","accessMode":"RO","text":"Range
+            // 0..100","unit":"%","value":null}
+            return Optional.empty();
+        }
+
+        String address = response.get("address").getAsString();
+        String text = response.get("text").getAsString();
+        String value = response.get("value").getAsString();
+
+        return Optional.of(new FeneconResponse(address, text, value));
+    }
+}
diff --git a/bundles/org.openhab.binding.fenecon/src/main/java/org/openhab/binding/fenecon/internal/api/FeneconResponse.java b/bundles/org.openhab.binding.fenecon/src/main/java/org/openhab/binding/fenecon/internal/api/FeneconResponse.java
new file mode 100644 (file)
index 0000000..49b2943
--- /dev/null
@@ -0,0 +1,24 @@
+/**
+ * Copyright (c) 2010-2024 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.fenecon.internal.api;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * The {@link FeneconResponse} class provides the response from the FENECON system.
+ *
+ * @author Philipp Schneider - Initial contribution
+ */
+@NonNullByDefault
+public record FeneconResponse(String address, String text, String value) {
+};
diff --git a/bundles/org.openhab.binding.fenecon/src/main/java/org/openhab/binding/fenecon/internal/api/GridPower.java b/bundles/org.openhab.binding.fenecon/src/main/java/org/openhab/binding/fenecon/internal/api/GridPower.java
new file mode 100644 (file)
index 0000000..9ad0365
--- /dev/null
@@ -0,0 +1,38 @@
+/**
+ * Copyright (c) 2010-2024 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.fenecon.internal.api;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * The {@link GridPower} is a small helper class to convert the grid value.
+ *
+ * @author Philipp Schneider - Initial contribution
+ */
+@NonNullByDefault
+public record GridPower(int sellTo, int buyFrom) {
+
+    public static GridPower get(FeneconResponse response) {
+        // Grid exchange power. Negative values for sell-to-grid; positive for buy-from-grid"
+        Integer gridValue = Integer.valueOf(response.value());
+        int selltoGridPower = 0;
+        int buyFromGridPower = 0;
+        if (gridValue < 0) {
+            selltoGridPower = gridValue * -1;
+        } else {
+            buyFromGridPower = gridValue;
+        }
+
+        return new GridPower(selltoGridPower, buyFromGridPower);
+    }
+}
diff --git a/bundles/org.openhab.binding.fenecon/src/main/java/org/openhab/binding/fenecon/internal/api/State.java b/bundles/org.openhab.binding.fenecon/src/main/java/org/openhab/binding/fenecon/internal/api/State.java
new file mode 100644 (file)
index 0000000..4de497f
--- /dev/null
@@ -0,0 +1,43 @@
+/**
+ * Copyright (c) 2010-2024 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.fenecon.internal.api;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * The {@link State} is a small helper class to convert the state value.
+ *
+ * @author Philipp Schneider - Initial contribution
+ */
+@NonNullByDefault
+public record State(String state) {
+
+    public static State get(FeneconResponse response) {
+        // {"address":"_sum/State","type":"INTEGER","accessMode":"RO","text":"0:Ok, 1:Info, 2:Warning,
+        // 3:Fault","unit":"","value":0}
+        String text = response.text();
+        int begin = text.indexOf(response.value() + ":");
+        int end = text.indexOf(",", begin);
+
+        // No value to text mapping
+        if (begin < 0) {
+            return new State("Unknown");
+        }
+
+        // Last text
+        if (end < 0) {
+            end = text.length();
+        }
+        return new State(text.substring(begin + 2, end));
+    }
+}
diff --git a/bundles/org.openhab.binding.fenecon/src/main/java/org/openhab/binding/fenecon/internal/exception/FeneconAuthenticationException.java b/bundles/org.openhab.binding.fenecon/src/main/java/org/openhab/binding/fenecon/internal/exception/FeneconAuthenticationException.java
new file mode 100644 (file)
index 0000000..de93031
--- /dev/null
@@ -0,0 +1,30 @@
+/**
+ * Copyright (c) 2010-2024 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.fenecon.internal.exception;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * The class {@link FeneconAuthenticationException} is thrown if a authentication on the FENECON system is not possible.
+ *
+ * @author Philipp Schneider - Initial contribution
+ */
+@NonNullByDefault
+public class FeneconAuthenticationException extends FeneconException {
+
+    private static final long serialVersionUID = -9206453599559316730L;
+
+    public FeneconAuthenticationException(String message) {
+        super(message);
+    }
+}
diff --git a/bundles/org.openhab.binding.fenecon/src/main/java/org/openhab/binding/fenecon/internal/exception/FeneconCommunicationException.java b/bundles/org.openhab.binding.fenecon/src/main/java/org/openhab/binding/fenecon/internal/exception/FeneconCommunicationException.java
new file mode 100644 (file)
index 0000000..6099170
--- /dev/null
@@ -0,0 +1,34 @@
+/**
+ * Copyright (c) 2010-2024 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.fenecon.internal.exception;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * The class {@link FeneconCommunicationException} is thrown if a communication problem occurs with the FENECON system.
+ *
+ * @author Philipp Schneider - Initial contribution
+ */
+@NonNullByDefault
+public class FeneconCommunicationException extends FeneconException {
+
+    private static final long serialVersionUID = -4334759327203382902L;
+
+    public FeneconCommunicationException(String message) {
+        super(message);
+    }
+
+    public FeneconCommunicationException(String message, Exception exception) {
+        super(message, exception);
+    }
+}
diff --git a/bundles/org.openhab.binding.fenecon/src/main/java/org/openhab/binding/fenecon/internal/exception/FeneconException.java b/bundles/org.openhab.binding.fenecon/src/main/java/org/openhab/binding/fenecon/internal/exception/FeneconException.java
new file mode 100644 (file)
index 0000000..ffad818
--- /dev/null
@@ -0,0 +1,42 @@
+/**
+ * Copyright (c) 2010-2024 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.fenecon.internal.exception;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * The {@link FeneconException} class provides general exception for this binding.
+ *
+ * @author Philipp Schneider - Initial contribution
+ */
+@NonNullByDefault
+public class FeneconException extends Exception {
+
+    private static final long serialVersionUID = 4454633961827361165L;
+
+    public FeneconException() {
+        // noop
+    }
+
+    public FeneconException(String message) {
+        super(message);
+    }
+
+    public FeneconException(Exception exception) {
+        super(exception);
+    }
+
+    public FeneconException(String message, Exception exception) {
+        super(message, exception);
+    }
+}
diff --git a/bundles/org.openhab.binding.fenecon/src/main/resources/OH-INF/addon/addon.xml b/bundles/org.openhab.binding.fenecon/src/main/resources/OH-INF/addon/addon.xml
new file mode 100644 (file)
index 0000000..0602d75
--- /dev/null
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<addon:addon id="fenecon" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xmlns:addon="https://openhab.org/schemas/addon/v1.0.0"
+       xsi:schemaLocation="https://openhab.org/schemas/addon/v1.0.0 https://openhab.org/schemas/addon-1.0.0.xsd">
+
+       <type>binding</type>
+       <name>FENECON Home Binding</name>
+       <description>This is the binding for FENECON Home.</description>
+       <connection>local</connection>
+
+</addon:addon>
diff --git a/bundles/org.openhab.binding.fenecon/src/main/resources/OH-INF/i18n/fenecon.properties b/bundles/org.openhab.binding.fenecon/src/main/resources/OH-INF/i18n/fenecon.properties
new file mode 100644 (file)
index 0000000..ea8a61f
--- /dev/null
@@ -0,0 +1,57 @@
+# add-on
+
+addon.fenecon.name = FENECON Home Binding
+addon.fenecon.description = This is the binding for FENECON Home.
+
+# thing types
+
+thing-type.fenecon.home-device.label = FENECON Home
+thing-type.fenecon.home-device.description = Thing for FENECON Home Device
+
+# thing types config
+
+thing-type.config.fenecon.home-device.hostname.label = Hostname
+thing-type.config.fenecon.home-device.hostname.description = Hostname or IP address of the FENECON device, e.g. 192.168.1.11
+thing-type.config.fenecon.home-device.password.label = Password
+thing-type.config.fenecon.home-device.password.description = Password to access the device. The password for guest access is set by default.
+thing-type.config.fenecon.home-device.port.label = Port
+thing-type.config.fenecon.home-device.port.description = Port of the FENECON device
+thing-type.config.fenecon.home-device.refreshInterval.label = Refresh Interval
+thing-type.config.fenecon.home-device.refreshInterval.description = Interval the device is polled in sec.
+
+# channel types
+
+channel-type.fenecon.charger-power.label = Charger Power
+channel-type.fenecon.charger-power.description = Current charger power of energy storage system in watt.
+channel-type.fenecon.consumption-active-power-phase.label = Consumer Power Phase
+channel-type.fenecon.consumption-active-power-phase.description = Current active power consumer load in watt on the corresponding phase.
+channel-type.fenecon.consumption-active-power.label = Consumer Power
+channel-type.fenecon.consumption-active-power.description = Current active power consumer load in watt.
+channel-type.fenecon.consumption-max-active-power.label = Consumer Max Power
+channel-type.fenecon.consumption-max-active-power.description = Maximum active consumption power in watt that was measured.
+channel-type.fenecon.discharger-power.label = Discharger Power
+channel-type.fenecon.discharger-power.description = Current discharger power of energy storage system in watt.
+channel-type.fenecon.emergency-power-mode.label = Emergency Power Mode
+channel-type.fenecon.emergency-power-mode.description = Indicates if there is no power from the grid and the emergency power mode is on.
+channel-type.fenecon.ess-soc.label = Battery State
+channel-type.fenecon.ess-soc.description = Battery state of charge in percent
+channel-type.fenecon.export-to-grid-power.label = Export Grid Power
+channel-type.fenecon.export-to-grid-power.description = Current export power to grid in watt.
+channel-type.fenecon.exported-to-grid-energy.label = Exported Grid Energy
+channel-type.fenecon.exported-to-grid-energy.description = Total energy exported to the grid in watt per hour.
+channel-type.fenecon.import-from-grid-power.label = Import Grid Power
+channel-type.fenecon.import-from-grid-power.description = Current import power from grid in watt.
+channel-type.fenecon.imported-from-grid-energy.label = Imported Grid Energy
+channel-type.fenecon.imported-from-grid-energy.description = Total energy imported from the grid in watt per hour.
+channel-type.fenecon.last-update.label = Last Update
+channel-type.fenecon.last-update.description = Last successful update via REST-API from the FENECON system
+channel-type.fenecon.production-active-power.label = Producer Power
+channel-type.fenecon.production-active-power.description = Current active power producer load in watt.
+channel-type.fenecon.production-max-active-power.label = Producer Max Power
+channel-type.fenecon.production-max-active-power.description = Maximum active production power in watt that was measured.
+channel-type.fenecon.state.label = System State
+channel-type.fenecon.state.description = FENECON system state
+channel-type.fenecon.state.state.option.OK = Ok
+channel-type.fenecon.state.state.option.INFO = Info
+channel-type.fenecon.state.state.option.WARN = Warning
+channel-type.fenecon.state.state.option.FAULT = Fault
diff --git a/bundles/org.openhab.binding.fenecon/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.fenecon/src/main/resources/OH-INF/thing/thing-types.xml
new file mode 100644 (file)
index 0000000..1ae8c93
--- /dev/null
@@ -0,0 +1,177 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="fenecon"
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
+       xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
+
+       <!-- Sample Thing Type -->
+       <thing-type id="home-device">
+
+               <label>FENECON Home</label>
+               <description>Thing for FENECON Home Device</description>
+               <category>Solarplant</category>
+
+               <channels>
+                       <channel id="state" typeId="state"/>
+                       <channel id="last-update" typeId="last-update"/>
+                       <channel id="ess-soc" typeId="ess-soc"/>
+                       <channel id="charger-power" typeId="charger-power"/>
+                       <channel id="discharger-power" typeId="discharger-power"/>
+                       <channel id="emergency-power-mode" typeId="emergency-power-mode"/>
+                       <channel id="consumption-active-power" typeId="consumption-active-power"/>
+                       <channel id="consumption-active-power-l1" typeId="consumption-active-power-phase"/>
+                       <channel id="consumption-active-power-l2" typeId="consumption-active-power-phase"/>
+                       <channel id="consumption-active-power-l3" typeId="consumption-active-power-phase"/>
+                       <channel id="consumption-max-active-power" typeId="consumption-max-active-power"/>
+                       <channel id="production-max-active-power" typeId="production-max-active-power"/>
+                       <channel id="production-active-power" typeId="production-active-power"/>
+                       <channel id="export-to-grid-power" typeId="export-to-grid-power"/>
+                       <channel id="exported-to-grid-energy" typeId="exported-to-grid-energy"/>
+                       <channel id="import-from-grid-power" typeId="import-from-grid-power"/>
+                       <channel id="imported-from-grid-energy" typeId="imported-from-grid-energy"/>
+               </channels>
+
+               <config-description>
+                       <parameter name="hostname" type="text" required="true">
+                               <context>network-address</context>
+                               <label>Hostname</label>
+                               <description>Hostname or IP address of the FENECON device, e.g. 192.168.1.11</description>
+                       </parameter>
+                       <parameter name="password" type="text">
+                               <context>password</context>
+                               <label>Password</label>
+                               <default>user</default>
+                               <description>Password to access the device. The password for guest access is set by default.</description>
+                               <advanced>true</advanced>
+                       </parameter>
+                       <parameter name="port" type="integer" min="1">
+                               <context>network-address</context>
+                               <label>Port</label>
+                               <description>Port of the FENECON device</description>
+                               <default>8084</default>
+                               <advanced>true</advanced>
+                       </parameter>
+                       <parameter name="refreshInterval" type="integer" unit="s" min="1">
+                               <label>Refresh Interval</label>
+                               <description>Interval the device is polled in sec.</description>
+                               <default>30</default>
+                               <advanced>true</advanced>
+                       </parameter>
+               </config-description>
+       </thing-type>
+
+       <!-- Channel Types -->
+       <channel-type id="state">
+               <item-type>String</item-type>
+               <label>System State</label>
+               <description>FENECON system state</description>
+               <category>Text</category>
+               <state readOnly="true" pattern="%s">
+                       <options>
+                               <option value="OK">Ok</option>
+                               <option value="INFO">Info</option>
+                               <option value="WARN">Warning</option>
+                               <option value="FAULT">Fault</option>
+                       </options>
+               </state>
+       </channel-type>
+       <channel-type id="last-update">
+               <item-type>DateTime</item-type>
+               <label>Last Update</label>
+               <description>Last successful update via REST-API from the FENECON system</description>
+               <category>Time</category>
+               <state readOnly="true"></state>
+       </channel-type>
+       <channel-type id="ess-soc">
+               <item-type unitHint="%">Number:Dimensionless</item-type>
+               <label>Battery State</label>
+               <description>Battery state of charge in percent</description>
+               <category>BatteryLevel</category>
+               <state readOnly="true" pattern="%.0f %unit%"/>
+       </channel-type>
+       <channel-type id="charger-power">
+               <item-type>Number:Power</item-type>
+               <label>Charger Power</label>
+               <description>Current charger power of energy storage system in watt.</description>
+               <category>Energy</category>
+               <state readOnly="true" pattern="%.0f %unit%"/>
+       </channel-type>
+       <channel-type id="discharger-power">
+               <item-type>Number:Power</item-type>
+               <label>Discharger Power</label>
+               <description>Current discharger power of energy storage system in watt.</description>
+               <category>Energy</category>
+               <state readOnly="true" pattern="%.0f %unit%"/>
+       </channel-type>
+       <channel-type id="emergency-power-mode">
+               <item-type>Switch</item-type>
+               <label>Emergency Power Mode</label>
+               <description>Indicates if there is no power from the grid and the emergency power mode is on.</description>
+               <category>Switch</category>
+               <state readOnly="true"/>
+       </channel-type>
+       <channel-type id="production-active-power">
+               <item-type>Number:Power</item-type>
+               <label>Producer Power</label>
+               <description>Current active power producer load in watt.</description>
+               <category>Energy</category>
+               <state readOnly="true" pattern="%.0f %unit%"/>
+       </channel-type>
+       <channel-type id="export-to-grid-power">
+               <item-type>Number:Power</item-type>
+               <label>Export Grid Power</label>
+               <description>Current export power to grid in watt.</description>
+               <category>Energy</category>
+               <state readOnly="true" pattern="%.0f %unit%"/>
+       </channel-type>
+       <channel-type id="exported-to-grid-energy">
+               <item-type>Number:Energy</item-type>
+               <label>Exported Grid Energy</label>
+               <description>Total energy exported to the grid in watt per hour.</description>
+               <category>Energy</category>
+               <state readOnly="true" pattern="%.0f %unit%"/>
+       </channel-type>
+       <channel-type id="consumption-active-power">
+               <item-type>Number:Power</item-type>
+               <label>Consumer Power</label>
+               <description>Current active power consumer load in watt.</description>
+               <category>Energy</category>
+               <state readOnly="true" pattern="%.0f %unit%"/>
+       </channel-type>
+       <channel-type id="consumption-active-power-phase">
+               <item-type>Number:Power</item-type>
+               <label>Consumer Power Phase</label>
+               <description>Current active power consumer load in watt on the corresponding phase.</description>
+               <category>Energy</category>
+               <state readOnly="true" pattern="%.0f %unit%"/>
+       </channel-type>
+       <channel-type id="consumption-max-active-power">
+               <item-type>Number:Power</item-type>
+               <label>Consumer Max Power</label>
+               <description>Maximum active consumption power in watt that was measured.</description>
+               <category>Energy</category>
+               <state readOnly="true" pattern="%.0f %unit%"/>
+       </channel-type>
+       <channel-type id="production-max-active-power">
+               <item-type>Number:Power</item-type>
+               <label>Producer Max Power</label>
+               <description>Maximum active production power in watt that was measured.</description>
+               <category>Energy</category>
+               <state readOnly="true" pattern="%.0f %unit%"/>
+       </channel-type>
+       <channel-type id="import-from-grid-power">
+               <item-type>Number:Power</item-type>
+               <label>Import Grid Power</label>
+               <description>Current import power from grid in watt.</description>
+               <category>Energy</category>
+               <state readOnly="true" pattern="%.0f %unit%"/>
+       </channel-type>
+       <channel-type id="imported-from-grid-energy">
+               <item-type>Number:Energy</item-type>
+               <label>Imported Grid Energy</label>
+               <description>Total energy imported from the grid in watt per hour.</description>
+               <category>Energy</category>
+               <state readOnly="true" pattern="%.0f %unit%"/>
+       </channel-type>
+
+</thing:thing-descriptions>
diff --git a/bundles/org.openhab.binding.fenecon/src/test/java/org/openhab/binding/fenecon/internal/FeneconBindingConstantsTest.java b/bundles/org.openhab.binding.fenecon/src/test/java/org/openhab/binding/fenecon/internal/FeneconBindingConstantsTest.java
new file mode 100644 (file)
index 0000000..01d67c8
--- /dev/null
@@ -0,0 +1,48 @@
+/**
+ * Copyright (c) 2010-2024 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.fenecon.internal;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.lang.reflect.Field;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Test for {@link FeneconBindingConstants}.
+ *
+ * @author Philipp Schneider - Initial contribution
+ */
+@NonNullByDefault
+public class FeneconBindingConstantsTest {
+
+    @Test
+    void checkAllAddressesAreListed() throws IllegalArgumentException, IllegalAccessException {
+        List<String> findAddresses = new ArrayList<>();
+
+        for (Field eachDeclaredField : FeneconBindingConstants.class.getDeclaredFields()) {
+            if (eachDeclaredField.getName().endsWith("_ADDRESS")) {
+                String address = (String) eachDeclaredField.get(FeneconBindingConstants.class);
+                if (address != null) {
+                    findAddresses.add(address);
+                }
+            }
+        }
+
+        assertEquals(FeneconBindingConstants.ADDRESSES.size(), findAddresses.size());
+        assertTrue(findAddresses.containsAll(FeneconBindingConstants.ADDRESSES));
+    }
+}
diff --git a/bundles/org.openhab.binding.fenecon/src/test/java/org/openhab/binding/fenecon/internal/api/BatteryPowerTest.java b/bundles/org.openhab.binding.fenecon/src/test/java/org/openhab/binding/fenecon/internal/api/BatteryPowerTest.java
new file mode 100644 (file)
index 0000000..dfa78cb
--- /dev/null
@@ -0,0 +1,44 @@
+/**
+ * Copyright (c) 2010-2024 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.fenecon.internal.api;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.junit.jupiter.api.Test;
+import org.openhab.binding.fenecon.internal.FeneconBindingConstants;
+
+/**
+ * Test for {@link BatteryPower}.
+ *
+ * @author Philipp Schneider - Initial contribution
+ */
+@NonNullByDefault
+public class BatteryPowerTest {
+
+    @Test
+    void testCharging() {
+        BatteryPower batteryPower = BatteryPower
+                .get(new FeneconResponse(FeneconBindingConstants.ESS_DISCHARGE_POWER_ADDRESS, "comment", "-1777"));
+        assertEquals(1777, batteryPower.chargerPower());
+        assertEquals(0, batteryPower.dischargerPower());
+    }
+
+    @Test
+    void testDischarging() {
+        BatteryPower batteryPower = BatteryPower
+                .get(new FeneconResponse(FeneconBindingConstants.ESS_DISCHARGE_POWER_ADDRESS, "comment", "1777"));
+        assertEquals(1777, batteryPower.dischargerPower());
+        assertEquals(0, batteryPower.chargerPower());
+    }
+}
diff --git a/bundles/org.openhab.binding.fenecon/src/test/java/org/openhab/binding/fenecon/internal/api/GridPowerTest.java b/bundles/org.openhab.binding.fenecon/src/test/java/org/openhab/binding/fenecon/internal/api/GridPowerTest.java
new file mode 100644 (file)
index 0000000..1ed981d
--- /dev/null
@@ -0,0 +1,44 @@
+/**
+ * Copyright (c) 2010-2024 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.fenecon.internal.api;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.junit.jupiter.api.Test;
+import org.openhab.binding.fenecon.internal.FeneconBindingConstants;
+
+/**
+ * Test for {@link GridPower}.
+ *
+ * @author Philipp Schneider - Initial contribution
+ */
+@NonNullByDefault
+public class GridPowerTest {
+
+    @Test
+    void testSelling() {
+        GridPower gridValue = GridPower
+                .get(new FeneconResponse(FeneconBindingConstants.GRID_ACTIVE_POWER_ADDRESS, "comment", "-1777"));
+        assertEquals(1777, gridValue.sellTo());
+        assertEquals(0, gridValue.buyFrom());
+    }
+
+    @Test
+    void testBuying() {
+        GridPower gridValue = GridPower
+                .get(new FeneconResponse(FeneconBindingConstants.GRID_ACTIVE_POWER_ADDRESS, "comment", "1777"));
+        assertEquals(1777, gridValue.buyFrom());
+        assertEquals(0, gridValue.sellTo());
+    }
+}
diff --git a/bundles/org.openhab.binding.fenecon/src/test/java/org/openhab/binding/fenecon/internal/api/StateTest.java b/bundles/org.openhab.binding.fenecon/src/test/java/org/openhab/binding/fenecon/internal/api/StateTest.java
new file mode 100644 (file)
index 0000000..fa947a2
--- /dev/null
@@ -0,0 +1,63 @@
+/**
+ * Copyright (c) 2010-2024 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.fenecon.internal.api;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.junit.jupiter.api.Test;
+import org.openhab.binding.fenecon.internal.FeneconBindingConstants;
+
+/**
+ * Test for {@link State}.
+ *
+ * @author Philipp Schneider - Initial contribution
+ */
+@NonNullByDefault
+public class StateTest {
+
+    @Test
+    void testStateOk() {
+        State state = State.get(
+                new FeneconResponse(FeneconBindingConstants.STATE_ADDRESS, "0:Ok, 1:Info, 2:Warning, 3:Fault", "0"));
+        assertEquals("Ok", state.state());
+    }
+
+    @Test
+    void testStateInfo() {
+        State state = State.get(
+                new FeneconResponse(FeneconBindingConstants.STATE_ADDRESS, "0:Ok, 1:Info, 2:Warning, 3:Fault", "1"));
+        assertEquals("Info", state.state());
+    }
+
+    @Test
+    void testStateWarning() {
+        State state = State.get(
+                new FeneconResponse(FeneconBindingConstants.STATE_ADDRESS, "0:Ok, 1:Info, 2:Warning, 3:Fault", "2"));
+        assertEquals("Warning", state.state());
+    }
+
+    @Test
+    void testStateFault() {
+        State state = State.get(
+                new FeneconResponse(FeneconBindingConstants.STATE_ADDRESS, "0:Ok, 1:Info, 2:Warning, 3:Fault", "3"));
+        assertEquals("Fault", state.state());
+    }
+
+    @Test
+    void testStateUnknown() {
+        State state = State.get(
+                new FeneconResponse(FeneconBindingConstants.STATE_ADDRESS, "0:Ok, 1:Info, 2:Warning, 3:Fault", "4"));
+        assertEquals("Unknown", state.state());
+    }
+}
index d71fe087b22fb8598852832c8062dfbf8251f8fe..28596db8a284948d2f836afd450e634b0b3cb5cb 100644 (file)
     <module>org.openhab.binding.exec</module>
     <module>org.openhab.binding.feed</module>
     <module>org.openhab.binding.feican</module>
+    <module>org.openhab.binding.fenecon</module>
     <module>org.openhab.binding.fineoffsetweatherstation</module>
     <module>org.openhab.binding.flicbutton</module>
     <module>org.openhab.binding.fmiweather</module>