[fenecon] Provide additional data such as the temperature on the inverter (#18613)

* Update / expand the initial user examples (#18600)

Signed-off-by: Philipp Schneider <philipp.schneider@nixo-soft.de>
pull/18688/head
Philipp S. 2025-05-16 09:24:54 +02:00 committed by GitHub
parent 9ef01e5f47
commit 4dacea107b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 987 additions and 96 deletions

View File

@ -31,32 +31,48 @@ The FENECON Thing only needs to be configured with the `hostname`, all other par
The FENECON binding currently only provides access to read out the values from the energy storage system. The FENECON binding currently only provides access to read out the values from the energy storage system.
| Channel | Type | Read/Write | Description | | Channel | Type | Read/Write | Description |
|-------------------------------|----------------------|------------|-----------------------------------------------------------------------------| |-------------------------------|----------------------------|------------|--------------------------------------------------------------------------------|
| state | String | R | FENECON system state: Ok, Info, Warning or Fault | | 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 | | fems-version | String | R | FENECON energy management system (FEMS) version - e.g 2025.2.3 |
| ess-soc | Number:Dimensionless | R | Battery state of charge in percent | | last-update | DateTime | R | Last successful update via REST-API from the FENECON system |
| charger-power | Number:Power | R | Current charger power of energy storage system in watt. | | ess-soc | Number:Dimensionless | R | Battery state of charge. |
| discharger-power | Number:Power | R | Current discharger power of energy storage system in watt. | | batt-tower-soh | Number:Dimensionless | R | Battery state of health. |
| emergency-power-mode | Switch | R | Indicates if there is grid power is off and the emergency power mode is on. | | charger-power | Number:Power | R | Current charger power of energy storage system. |
| production-active-power | Number:Power | R | Current active power producer load in watt. | | discharger-power | Number:Power | R | Current discharger power of energy storage system. |
| production-max-active-power | Number:Power | R | Maximum active production power in watt that was measured. | | emergency-power-mode | Switch | R | Indicates if there is grid power is off and the emergency power mode is on. |
| export-to-grid-power | Number:Power | R | Current export power to grid in watt. | | production-active-power | Number:Power | R | Current active power producer load. |
| exported-to-grid-energy | Number:Energy | R | Total energy exported to the grid in watt per hour. | | production-max-active-power | Number:Power | R | Maximum active production power that was measured. |
| consumption-active-power | Number:Power | R | Current active power consumer load in watt. | | export-to-grid-power | Number:Power | R | Current export power to grid. |
| consumption-max-active-power | Number:Power | R | Maximum active consumption power in watt that was measured. | | exported-to-grid-energy | Number:Energy | R | Total energy exported to the grid. |
| consumption-active-power-l1 | Number:Power | R | Current active power consumer load in watt on phase 1. | | consumption-active-power | Number:Power | R | Current active power consumer load. |
| consumption-active-power-l2 | Number:Power | R | Current active power consumer load in watt on phase 2. | | consumption-max-active-power | Number:Power | R | Maximum active consumption power that was measured. |
| consumption-active-power-l3 | Number:Power | R | Current active power consumer load in watt on phase 3. | | consumption-active-power-l1 | Number:Power | R | Current active power consumer load on phase 1. |
| import-from-grid-power | Number:Power | R | Current import power from grid in watt. | | consumption-active-power-l2 | Number:Power | R | Current active power consumer load on phase 2. |
| imported-from-grid-energy | Number:Energy | R | Total energy imported from the grid in watt per hour. | | consumption-active-power-l3 | Number:Power | R | Current active power consumer load on phase 3. |
| import-from-grid-power | Number:Power | R | Current import power from grid. |
| imported-from-grid-energy | Number:Energy | R | Total energy imported from the grid. |
| inverter-air-temperature | Number:Temperature | R | Air temperature at the inverter. |
| inverter-radiator-temperature | Number:Temperature | R | Radiator temperature of the inverter. |
| bms-pack-temperature | Number:Temperature | R | Temperature in the battery management system (BMS) box. |
| batt-tower-voltage | Number:ElectricPotential | R | Battery voltage of the FENECON energy management system (FEMS). |
| batt-tower-current | Number:ElectricCurrent | R | Battery current of the FENECON energy management system (FEMS). |
| charger0-actual-power | Number:Power | R | Charger actual power on the charger 0 - e.g west roof, if available. |
| charger0-voltage | Number:ElectricPotential | R | Charger voltage on the charger 0 - e.g west roof, if available. |
| charger0-current | Number:ElectricCurrent | R | Charger current on the charger 0 - e.g west roof, if available. |
| charger1-actual-power | Number:Power | R | Charger actual power on the charger 1 - e.g east roof, if available. |
| charger1-voltage | Number:ElectricPotential | R | Charger voltage on the charger 1 - e.g east roof, if available. |
| charger1-current | Number:ElectricCurrent | R | Charger current on the charger 1 - e.g east roof, if available. |
| charger2-actual-power | Number:Power | R | Charger actual power on the charger 2 - e.g south roof, if available. |
| charger2-voltage | Number:ElectricPotential | R | Charger voltage on the charger 2 - e.g south roof, if available. |
| charger2-current | Number:ElectricCurrent | R | Charger current on the charger 2 - e.g south roof, if available. |
## Full Example ## Full Example
### fenecon.things ### fenecon.things
```java ```java
Thing fenecon:home-device:local "FENECON Home" [hostname="192.168.1.11", refreshInterval=5] Thing fenecon:home-device:local "FENECON Home" [hostname="192.168.1.11", refreshInterval=30]
``` ```
### demo.items ### demo.items
@ -66,33 +82,52 @@ Thing fenecon:home-device:local "FENECON Home" [hostname="192.168.1.11", refresh
Group Home "MyHome" <house> ["Indoor"] Group Home "MyHome" <house> ["Indoor"]
Group GF "GroundFloor" <groundfloor> (Home) ["GroundFloor"] Group GF "GroundFloor" <groundfloor> (Home) ["GroundFloor"]
// Utility room // Utility room
Group GF_UtilityRoom "Utility room" <energy> (Home, GF) ["Room"] Group GF_UtilityRoom "Utility room" <energy> (GF) ["Room"]
Group GF_UtilityRoomSolar "Utility room solar" <solarplant> (GF_UtilityRoom) ["Inverter"] Group GF_UtilityRoomSolar "Utility room solar" <solarplant> (GF_UtilityRoom) ["Inverter"]
// FENECON items // FENECON items
String EssState <text> (GF_UtilityRoomSolar) ["Status"] {channel="fenecon:home-device:local:state"} 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"} String FemsVersion <text> (GF_UtilityRoomSolar) ["Status"] {channel="fenecon:home-device:local:fems-version"}
Number:Dimensionless EssSoc <batterylevel> (GF_UtilityRoomSolar) ["Measurement"] {unit="%", channel="fenecon:home-device:local:ess-soc"} DateTime LastFeneconUpdate <time> (GF_UtilityRoomSolar) ["Status"] {channel="fenecon:home-device:local:last-update"}
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:Dimensionless EssSoc <batterylevel> (GF_UtilityRoomSolar) ["Measurement"] {unit="%", channel="fenecon:home-device:local:ess-soc"}
Number:Power ProductionMaxActivePower <energy> (GF_UtilityRoomSolar) ["Measurement", "Power"] {channel="fenecon:home-device:local:production-max-active-power"} Number:Dimensionless BattSoh <batterylevel> (GF_UtilityRoomSolar) ["Measurement"] {unit="%", channel="fenecon:home-device:local:batt-tower-soh"}
Number:Power SellToGridPower <energy> (GF_UtilityRoomSolar) ["Measurement", "Power"] {channel="fenecon:home-device:local:export-to-grid-power"} Number:Power ChargerPower <energy> (GF_UtilityRoomSolar) ["Measurement", "Power"] {channel="fenecon:home-device:local:charger-power"}
Number:Energy TotalSellEnergy <energy> (GF_UtilityRoomSolar) ["Measurement", "Energy"] {channel="fenecon:home-device:local:exported-to-grid-energy"} 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 ConsumptionActivePower <energy> (GF_UtilityRoomSolar) ["Measurement", "Power"] {channel="fenecon:home-device:local:consumption-active-power"} Number:Power ProductionActivePower <energy> (GF_UtilityRoomSolar) ["Measurement", "Power"] {channel="fenecon:home-device:local:production-active-power"}
Number:Power ConsumptionMaxActivePower <energy> (GF_UtilityRoomSolar) ["Measurement", "Power"] {channel="fenecon:home-device:local:consumption-max-active-power"} Number:Power ProductionMaxActivePower <energy> (GF_UtilityRoomSolar) ["Measurement", "Power"] {channel="fenecon:home-device:local:production-max-active-power"}
Number:Power ConsumptionActivePowerPhase1 <energy> (GF_UtilityRoomSolar) ["Measurement", "Power"] {channel="fenecon:home-device:local:consumption-active-power-l1"} Number:Power SellToGridPower <energy> (GF_UtilityRoomSolar) ["Measurement", "Power"] {channel="fenecon:home-device:local:export-to-grid-power"}
Number:Power ConsumptionActivePowerPhase2 <energy> (GF_UtilityRoomSolar) ["Measurement", "Power"] {channel="fenecon:home-device:local:consumption-active-power-l2"} Number:Energy TotalSellEnergy <energy> (GF_UtilityRoomSolar) ["Measurement", "Energy"] {channel="fenecon:home-device:local:exported-to-grid-energy"}
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:Power ConsumptionActivePower <energy> (GF_UtilityRoomSolar) ["Measurement", "Power"] {channel="fenecon:home-device:local:consumption-active-power"}
Number:Energy TotalBuyEnergy <energy> (GF_UtilityRoomSolar) ["Measurement", "Energy"] {channel="fenecon:home-device:local:imported-from-grid-energy"} 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"}
Number:Temperature InverterAirTemp <temperature> (GF_UtilityRoomSolar) ["Measurement", "Temperature"] {channel="fenecon:home-device:local:inverter-air-temperature"}
Number:Temperature InverterRadiatorTemp <temperature> (GF_UtilityRoomSolar) ["Measurement", "Temperature"] {channel="fenecon:home-device:local:inverter-radiator-temperature"}
Number:Temperature BmsBoxTemp <temperature> (GF_UtilityRoomSolar) ["Measurement", "Temperature"] {channel="fenecon:home-device:local:bms-pack-temperature"}
Number:ElectricPotential BattTowerVoltage <energy> (GF_UtilityRoomSolar) ["Measurement", "Voltage"] {channel="fenecon:home-device:local:batt-tower-voltage"}
Number:ElectricCurrent BattTowerCurrent <energy> (GF_UtilityRoomSolar) ["Measurement", "Current"] {channel="fenecon:home-device:local:batt-tower-current"}
// Charger corresponds to the solar power plant on the roof.
Number:Power ChargerWestActualPower <energy> (GF_UtilityRoomSolar) ["Measurement", "Power"] {channel="fenecon:home-device:local:charger0-actual-power"}
Number:ElectricPotential ChargerWestVoltage <energy> (GF_UtilityRoomSolar) ["Measurement", "Voltage"] {channel="fenecon:home-device:local:charger0-voltage"}
Number:ElectricCurrent ChargerWestCurrent <energy> (GF_UtilityRoomSolar) ["Measurement", "Current"] {channel="fenecon:home-device:local:charger0-current"}
Number:Power ChargerEastActualPower <energy> (GF_UtilityRoomSolar) ["Measurement", "Power"] {channel="fenecon:home-device:local:charger1-actual-power"}
Number:ElectricPotential ChargerEastVoltage <energy> (GF_UtilityRoomSolar) ["Measurement", "Voltage"] {channel="fenecon:home-device:local:charger1-voltage"}
Number:ElectricCurrent ChargerEastCurrent <energy> (GF_UtilityRoomSolar) ["Measurement", "Current"] {channel="fenecon:home-device:local:charger1-current"}
Number:Power ChargerSouthActualPower <energy> (GF_UtilityRoomSolar) ["Measurement", "Power"] {channel="fenecon:home-device:local:charger2-actual-power"}
Number:ElectricPotential ChargerSouthVoltage <energy> (GF_UtilityRoomSolar) ["Measurement", "Voltage"] {channel="fenecon:home-device:local:charger2-voltage"}
Number:ElectricCurrent ChargerSouthCurrent <energy> (GF_UtilityRoomSolar) ["Measurement", "Current"] {channel="fenecon:home-device:local:charger2-current"}
// 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 ### demo.sitemap
@ -105,6 +140,21 @@ sitemap demo label="FENECON Example Sitemap" {
} }
``` ```
### rrd4j.persist
```perl
Strategies {
everyMinute : "0 * * * * ?"
default = everyChange
}
Items {
ProductionActivePower: strategy = everyUpdate, everyMinute, restoreOnStartup
ConsumptionActivePower: strategy = everyUpdate, everyMinute, restoreOnStartup
BuyFromGridPower: strategy = everyUpdate, everyMinute, restoreOnStartup
}
```
### demo.rules ### demo.rules
:::: tabs :::: tabs
@ -152,6 +202,25 @@ then
var result = current * purchasedPricePerKiloWattHour; var result = current * purchasedPricePerKiloWattHour;
PurchasedEnergy.postUpdate(result) PurchasedEnergy.postUpdate(result)
end end
// !!! This is only designed as a demonstration, the calculation should only be executed every 30 or 60 minutes if necessary. And for the calculation, be sure to consider the persistence example: rrd4j.persist!
rule "Calculation daily power values"
when
Item LastFeneconUpdate changed
then
var dailyMax = (ProductionActivePower.maximumSince(now.with(LocalTime.of(0,0,0,0))).state as Number).floatValue()
MaxProductionActivePowerOfTheDay.postUpdate(dailyMax)
var dailyProduction = (ProductionActivePower.sumSince(now.with(LocalTime.of(0,0,0,0))) as Number).floatValue() / 60 / 1000
ProductionActivePowerOfTheDay.postUpdate(dailyProduction)
var dailyConsumption = (ConsumptionActivePower.sumSince(now.with(LocalTime.of(0,0,0,0))) as Number).floatValue() / 60 / 1000
ConsumptionActivePowerOfTheDay.postUpdate(dailyConsumption)
var dailyBuyFromGrid = (BuyFromGridPower.sumSince(now.with(LocalTime.of(0,0,0,0))) as Number).floatValue() / 60 / 1000
BuyFromGridPowerOfTheDay.postUpdate(dailyBuyFromGrid)
end
``` ```
::: :::

View File

@ -15,6 +15,7 @@ package org.openhab.binding.fenecon.internal;
import java.util.List; import java.util.List;
import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.fenecon.internal.api.Address;
import org.openhab.core.thing.ThingTypeUID; import org.openhab.core.thing.ThingTypeUID;
/** /**
@ -32,6 +33,7 @@ public class FeneconBindingConstants {
public static final ThingTypeUID THING_TYPE_HOME_DEVICE = new ThingTypeUID(BINDING_ID, "home-device"); public static final ThingTypeUID THING_TYPE_HOME_DEVICE = new ThingTypeUID(BINDING_ID, "home-device");
// List of all FENECON Addresses // List of all FENECON Addresses
// Group: _sum/...
public static final String STATE_ADDRESS = "_sum/State"; public static final String STATE_ADDRESS = "_sum/State";
public static final String ESS_SOC_ADDRESS = "_sum/EssSoc"; 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_ADDRESS = "_sum/ConsumptionActivePower";
@ -46,13 +48,45 @@ public class FeneconBindingConstants {
public static final String GRID_MODE_ADDRESS = "_sum/GridMode"; 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_SELL_ACTIVE_ENERGY_ADDRESS = "_sum/GridSellActiveEnergy";
public static final String GRID_BUY_ACTIVE_ENERGY_ADDRESS = "_sum/GridBuyActiveEnergy"; public static final String GRID_BUY_ACTIVE_ENERGY_ADDRESS = "_sum/GridBuyActiveEnergy";
// Group: _meta/...
public static final String FEMS_VERSION_ADDRESS = "_meta/Version";
// Group: batteryInverter0/...
public static final String BATT_INVERTER_AIR_TEMP_ADDRESS = "batteryInverter0/AirTemperature";
public static final String BATT_INVERTER_RADIATOR_TEMP_ADDRESS = "batteryInverter0/RadiatorTemperature";
public static final String BATT_INVERTER_BMS_PACK_TEMP_ADDRESS = "batteryInverter0/BmsPackTemperature";
// Group: battery0/...
public static final String BATT_TOWER_PACK_VOLTAGE_ADDRESS = "battery0/Tower0PackVoltage";
public static final String BATT_TOWER_CURRENT_ADDRESS = "battery0/Current";
public static final String BATT_SOH_ADDRESS = "battery0/Soh";
// Group: charger0/...
public static final String CHARGER0_ACTUAL_POWER_ADDRESS = "charger0/ActualPower";
public static final String CHARGER0_VOLTAGE_ADDRESS = "charger0/Voltage";
public static final String CHARGER0_CURRENT_ADDRESS = "charger0/Current";
// Group: charger1/...
public static final String CHARGER1_ACTUAL_POWER_ADDRESS = "charger1/ActualPower";
public static final String CHARGER1_VOLTAGE_ADDRESS = "charger1/Voltage";
public static final String CHARGER1_CURRENT_ADDRESS = "charger1/Current";
// Group: charger2/...
public static final String CHARGER2_ACTUAL_POWER_ADDRESS = "charger2/ActualPower";
public static final String CHARGER2_VOLTAGE_ADDRESS = "charger2/Voltage";
public static final String CHARGER2_CURRENT_ADDRESS = "charger2/Current";
// Group of all FENECON Addresses // Group of all FENECON Addresses
public static final List<String> ADDRESSES = List.of(STATE_ADDRESS, GRID_MODE_ADDRESS, public static final List<Address> ADDRESSES = List.of(new Address(STATE_ADDRESS), new Address(GRID_MODE_ADDRESS),
CONSUMPTION_ACTIVE_POWER_ADDRESS, CONSUMPTION_ACTIVE_POWER_PHASE1_ADDRESS, new Address(CONSUMPTION_ACTIVE_POWER_ADDRESS), new Address(CONSUMPTION_ACTIVE_POWER_PHASE1_ADDRESS),
CONSUMPTION_ACTIVE_POWER_PHASE2_ADDRESS, CONSUMPTION_ACTIVE_POWER_PHASE3_ADDRESS, new Address(CONSUMPTION_ACTIVE_POWER_PHASE2_ADDRESS), new Address(CONSUMPTION_ACTIVE_POWER_PHASE3_ADDRESS),
CONSUMPTION_MAX_ACTIVE_POWER_ADDRESS, PRODUCTION_MAX_ACTIVE_POWER_ADDRESS, PRODUCTION_ACTIVE_POWER_ADDRESS, new Address(CONSUMPTION_MAX_ACTIVE_POWER_ADDRESS), new Address(PRODUCTION_MAX_ACTIVE_POWER_ADDRESS),
GRID_ACTIVE_POWER_ADDRESS, GRID_BUY_ACTIVE_ENERGY_ADDRESS, GRID_SELL_ACTIVE_ENERGY_ADDRESS, ESS_SOC_ADDRESS, new Address(PRODUCTION_ACTIVE_POWER_ADDRESS), new Address(GRID_ACTIVE_POWER_ADDRESS),
ESS_DISCHARGE_POWER_ADDRESS); new Address(GRID_BUY_ACTIVE_ENERGY_ADDRESS), new Address(GRID_SELL_ACTIVE_ENERGY_ADDRESS),
new Address(ESS_SOC_ADDRESS), new Address(ESS_DISCHARGE_POWER_ADDRESS), new Address(FEMS_VERSION_ADDRESS),
new Address(BATT_INVERTER_AIR_TEMP_ADDRESS), new Address(BATT_INVERTER_RADIATOR_TEMP_ADDRESS),
new Address(BATT_INVERTER_BMS_PACK_TEMP_ADDRESS), new Address(BATT_TOWER_PACK_VOLTAGE_ADDRESS),
new Address(BATT_TOWER_CURRENT_ADDRESS), new Address(BATT_SOH_ADDRESS),
new Address(CHARGER0_ACTUAL_POWER_ADDRESS), new Address(CHARGER1_ACTUAL_POWER_ADDRESS),
new Address(CHARGER2_ACTUAL_POWER_ADDRESS), new Address(CHARGER0_VOLTAGE_ADDRESS),
new Address(CHARGER1_VOLTAGE_ADDRESS), new Address(CHARGER2_VOLTAGE_ADDRESS),
new Address(CHARGER0_CURRENT_ADDRESS), new Address(CHARGER1_CURRENT_ADDRESS),
new Address(CHARGER2_CURRENT_ADDRESS));
// List of all Channel IDs // List of all Channel IDs
public static final String STATE_CHANNEL = "state"; public static final String STATE_CHANNEL = "state";
@ -72,4 +106,20 @@ public class FeneconBindingConstants {
public static final String EXPORTED_TO_GRID_ENERGY_CHANNEL = "exported-to-grid-energy"; 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 IMPORTED_FROM_GRID_ENERGY_CHANNEL = "imported-from-grid-energy";
public static final String LAST_UPDATE_CHANNEL = "last-update"; public static final String LAST_UPDATE_CHANNEL = "last-update";
public static final String FEMS_VERSION_CHANNEL = "fems-version";
public static final String BATT_INVERTER_AIR_TEMP_CHANNEL = "inverter-air-temperature";
public static final String BATT_INVERTER_RADIATOR_TEMP_CHANNEL = "inverter-radiator-temperature";
public static final String BATT_INVERTER_BMS_PACK_TEMP_CHANNEL = "bms-pack-temperature";
public static final String BATT_TOWER_PACK_VOLTAGE_CHANNEL = "batt-tower-voltage";
public static final String BATT_TOWER_CURRENT_CHANNEL = "batt-tower-current";
public static final String BATT_SOH_CHANNEL = "batt-tower-soh";
public static final String CHARGER0_ACTUAL_POWER_CHANNEL = "charger0-actual-power";
public static final String CHARGER1_ACTUAL_POWER_CHANNEL = "charger1-actual-power";
public static final String CHARGER2_ACTUAL_POWER_CHANNEL = "charger2-actual-power";
public static final String CHARGER0_VOLTAGE_CHANNEL = "charger0-voltage";
public static final String CHARGER1_VOLTAGE_CHANNEL = "charger1-voltage";
public static final String CHARGER2_VOLTAGE_CHANNEL = "charger2-voltage";
public static final String CHARGER0_CURRENT_CHANNEL = "charger0-current";
public static final String CHARGER1_CURRENT_CHANNEL = "charger1-current";
public static final String CHARGER2_CURRENT_CHANNEL = "charger2-current";
} }

View File

@ -12,13 +12,14 @@
*/ */
package org.openhab.binding.fenecon.internal; package org.openhab.binding.fenecon.internal;
import java.util.Optional; import java.util.List;
import java.util.concurrent.ScheduledFuture; import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable; import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jetty.client.HttpClient; import org.eclipse.jetty.client.HttpClient;
import org.openhab.binding.fenecon.internal.api.AddressComponentChannelUtil;
import org.openhab.binding.fenecon.internal.api.BatteryPower; import org.openhab.binding.fenecon.internal.api.BatteryPower;
import org.openhab.binding.fenecon.internal.api.FeneconController; import org.openhab.binding.fenecon.internal.api.FeneconController;
import org.openhab.binding.fenecon.internal.api.FeneconResponse; import org.openhab.binding.fenecon.internal.api.FeneconResponse;
@ -29,6 +30,7 @@ import org.openhab.core.library.types.DateTimeType;
import org.openhab.core.library.types.OnOffType; import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.QuantityType; import org.openhab.core.library.types.QuantityType;
import org.openhab.core.library.types.StringType; import org.openhab.core.library.types.StringType;
import org.openhab.core.library.unit.SIUnits;
import org.openhab.core.library.unit.Units; import org.openhab.core.library.unit.Units;
import org.openhab.core.thing.ChannelUID; import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing; import org.openhab.core.thing.Thing;
@ -71,18 +73,21 @@ public class FeneconHandler extends BaseThingHandler {
} }
private void pollingCode() { private void pollingCode() {
for (String eachChannel : FeneconBindingConstants.ADDRESSES) { List<String> componentRequests = AddressComponentChannelUtil
.createComponentRequests(FeneconBindingConstants.ADDRESSES);
for (String eachComponentRequest : componentRequests) {
try { try {
@SuppressWarnings("null") @SuppressWarnings("null")
Optional<FeneconResponse> response = feneconController.requestChannel(eachChannel); List<FeneconResponse> responses = feneconController.requestChannel(eachComponentRequest);
if (response.isPresent()) { for (FeneconResponse eachResponse : responses) {
processDataPoint(response.get()); processDataPoint(eachResponse);
} }
updateStatus(ThingStatus.ONLINE); updateStatus(ThingStatus.ONLINE);
} catch (FeneconException err) { } catch (FeneconException err) {
logger.trace("FENECON - connection problem on FENECON channel {}", eachChannel, err); logger.trace("FENECON - connection problem on FENECON channel {}", eachComponentRequest, err);
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, err.getMessage()); updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, err.getMessage());
return; return;
} }
@ -167,6 +172,102 @@ public class FeneconHandler extends BaseThingHandler {
updateState(FeneconBindingConstants.IMPORTED_FROM_GRID_ENERGY_CHANNEL, updateState(FeneconBindingConstants.IMPORTED_FROM_GRID_ENERGY_CHANNEL,
new QuantityType<>(Integer.valueOf(response.value()), Units.WATT_HOUR)); new QuantityType<>(Integer.valueOf(response.value()), Units.WATT_HOUR));
break; break;
case FeneconBindingConstants.FEMS_VERSION_ADDRESS:
// { "address": "_meta/Version","type": "STRING", "accessMode": "RO", "text": "", "unit": "", "value":
// "2025.2.3"}
updateState(FeneconBindingConstants.FEMS_VERSION_CHANNEL, new StringType(response.value()));
break;
case FeneconBindingConstants.BATT_INVERTER_AIR_TEMP_ADDRESS:
// {"address": "batteryInverter0/AirTemperature","type": "INTEGER","accessMode": "RO", "text": "",
// "unit": "C", "value": 41 }
updateState(FeneconBindingConstants.BATT_INVERTER_AIR_TEMP_CHANNEL,
new QuantityType<>(Integer.valueOf(response.value()), SIUnits.CELSIUS));
break;
case FeneconBindingConstants.BATT_INVERTER_RADIATOR_TEMP_ADDRESS:
// {"address": "batteryInverter0/RadiatorTemperature","type": "INTEGER", "accessMode": "RO", "text": "",
// "unit": "C", "value": 37 }
updateState(FeneconBindingConstants.BATT_INVERTER_RADIATOR_TEMP_CHANNEL,
new QuantityType<>(Integer.valueOf(response.value()), SIUnits.CELSIUS));
break;
case FeneconBindingConstants.BATT_INVERTER_BMS_PACK_TEMP_ADDRESS:
// {"address": "batteryInverter0/BmsPackTemperature", "type": "INTEGER", "accessMode": "RO", "text": "",
// "unit": "C", "value": 26 }
updateState(FeneconBindingConstants.BATT_INVERTER_BMS_PACK_TEMP_CHANNEL,
new QuantityType<>(Integer.valueOf(response.value()), SIUnits.CELSIUS));
break;
case FeneconBindingConstants.BATT_TOWER_PACK_VOLTAGE_ADDRESS:
// {"address": "battery0/Tower0PackVoltage", "type": "INTEGER", "accessMode": "RO", "text": "", "unit":
// "", "value": 2749 }
// Tower pack voltage in mV
updateState(FeneconBindingConstants.BATT_TOWER_PACK_VOLTAGE_CHANNEL,
new QuantityType<>(Integer.valueOf(response.value()) / 1000.0, Units.VOLT));
break;
case FeneconBindingConstants.BATT_TOWER_CURRENT_ADDRESS:
// {"address": "battery0/Current", "type": "INTEGER", "accessMode": "RO", "text": "", "unit": "A",
// "value": 9 }
updateState(FeneconBindingConstants.BATT_TOWER_CURRENT_CHANNEL,
new QuantityType<>(Integer.valueOf(response.value()), Units.AMPERE));
break;
case FeneconBindingConstants.BATT_SOH_ADDRESS:
// { "address": "battery0/Soh", "type": "INTEGER", "accessMode": "RO", "text": "", "unit": "%", "value":
// 100 }
updateState(FeneconBindingConstants.BATT_SOH_CHANNEL,
new QuantityType<>(Integer.valueOf(response.value()), Units.PERCENT));
break;
case FeneconBindingConstants.CHARGER0_ACTUAL_POWER_ADDRESS:
// { "address": "charger0/ActualPower", "type": "INTEGER", "accessMode": "RO", "text": "", "unit": "W",
// "value": 312 }
updateState(FeneconBindingConstants.CHARGER0_ACTUAL_POWER_CHANNEL,
new QuantityType<>(Integer.valueOf(response.value()), Units.WATT));
break;
case FeneconBindingConstants.CHARGER1_ACTUAL_POWER_ADDRESS:
// { "address": "charger1/ActualPower", "type": "INTEGER", "accessMode": "RO", "text": "", "unit": "W",
// "value": 33 }
updateState(FeneconBindingConstants.CHARGER1_ACTUAL_POWER_CHANNEL,
new QuantityType<>(Integer.valueOf(response.value()), Units.WATT));
break;
case FeneconBindingConstants.CHARGER2_ACTUAL_POWER_ADDRESS:
// { "address": "charger2/ActualPower", "type": "INTEGER", "accessMode": "RO", "text": "", "unit": "W",
// "value": 412 }
updateState(FeneconBindingConstants.CHARGER2_ACTUAL_POWER_CHANNEL,
new QuantityType<>(Integer.valueOf(response.value()), Units.WATT));
break;
case FeneconBindingConstants.CHARGER0_VOLTAGE_ADDRESS:
// { "address": "charger0/Voltage", "type": "INTEGER", "accessMode": "RO", "text": "", "unit": "mV",
// "value": 193000 }
updateState(FeneconBindingConstants.CHARGER0_VOLTAGE_CHANNEL,
new QuantityType<>(Integer.valueOf(response.value()) / 1000.0, Units.VOLT));
break;
case FeneconBindingConstants.CHARGER1_VOLTAGE_ADDRESS:
// { "address": "charger1/Voltage", "type": "INTEGER", "accessMode": "RO", "text": "", "unit": "mV",
// "value": 193000 }
updateState(FeneconBindingConstants.CHARGER1_VOLTAGE_CHANNEL,
new QuantityType<>(Integer.valueOf(response.value()) / 1000.0, Units.VOLT));
break;
case FeneconBindingConstants.CHARGER2_VOLTAGE_ADDRESS:
// { "address": "charger2/Voltage", "type": "INTEGER", "accessMode": "RO", "text": "", "unit": "mV",
// "value": 193000 }
updateState(FeneconBindingConstants.CHARGER2_VOLTAGE_CHANNEL,
new QuantityType<>(Integer.valueOf(response.value()) / 1000.0, Units.VOLT));
break;
case FeneconBindingConstants.CHARGER0_CURRENT_ADDRESS:
// {"address": "charger0/Current", "type": "INTEGER", "accessMode": "RO", "text": "", "unit": "mA",
// "value": 1200 },
updateState(FeneconBindingConstants.CHARGER0_CURRENT_CHANNEL,
new QuantityType<>(Integer.valueOf(response.value()) / 1000.0, Units.AMPERE));
break;
case FeneconBindingConstants.CHARGER1_CURRENT_ADDRESS:
// {"address": "charger1/Current", "type": "INTEGER", "accessMode": "RO", "text": "", "unit": "mA",
// "value": 1000 },
updateState(FeneconBindingConstants.CHARGER1_CURRENT_CHANNEL,
new QuantityType<>(Integer.valueOf(response.value()) / 1000.0, Units.AMPERE));
break;
case FeneconBindingConstants.CHARGER2_CURRENT_ADDRESS:
// {"address": "charger2/Current", "type": "INTEGER", "accessMode": "RO", "text": "", "unit": "mA",
// "value": 1100 },
updateState(FeneconBindingConstants.CHARGER2_CURRENT_CHANNEL,
new QuantityType<>(Integer.valueOf(response.value()) / 1000.0, Units.AMPERE));
break;
default: default:
logger.trace("FENECON - No channel ID to address {} found.", response.address()); logger.trace("FENECON - No channel ID to address {} found.", response.address());
break; break;

View File

@ -0,0 +1,86 @@
/*
* Copyright (c) 2010-2025 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.util.Objects;
import javax.validation.constraints.NotNull;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* The {@link Address} is a small helper class to split a REST-API Address in component and channel.
*
* @author Philipp Schneider - Initial contribution
*/
@NonNullByDefault
public final class Address {
private final String address;
private final AddressComponent component;
private final AddressChannel channel;
public Address(@NotNull String address) {
this.address = address;
String[] parts = address.split("/");
if (parts.length != 2) {
throw new IllegalArgumentException("Invalid address format 'component/channel' for: " + address);
}
component = new AddressComponent(parts[0]);
channel = new AddressChannel(parts[1]);
}
public String getAddress() {
return address;
}
public AddressComponent getComponent() {
return component;
}
public AddressChannel getChannel() {
return channel;
}
@Override
public boolean equals(@Nullable Object other) {
if (this == other) {
return true;
}
if (other == null) {
return false;
}
if (!(other instanceof Address)) {
return false;
}
Address address = (Address) other;
if (address.address.equals(this.address)) {
return true;
}
return false;
}
@Override
public int hashCode() {
return Objects.hash(address);
}
@Override
public String toString() {
return address;
}
}

View File

@ -0,0 +1,32 @@
/*
* Copyright (c) 2010-2025 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.util.Comparator;
import java.util.Objects;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* The {@link AddressChannel} is a container class to identify a channel of a {@link Address}.
*
* @author Philipp Schneider - Initial contribution
*/
@NonNullByDefault
public record AddressChannel(String channel) implements Comparable<AddressChannel> {
@Override
public int compareTo(AddressChannel that) {
return Objects.compare(this, that,
Comparator.comparing(AddressChannel::channel).thenComparing(AddressChannel::channel));
}
}

View File

@ -0,0 +1,32 @@
/*
* Copyright (c) 2010-2025 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 AddressComponent} is a container class to identify a component of a {@link Address}.
*
* @author Philipp Schneider - Initial contribution
*/
@NonNullByDefault
public record AddressComponent(String component) {
public AddressComponent(String component) {
this.component = convertComponentWithRegEx(component);
}
// Bundle same components with regex if possible, to reduce the number of requests
private static String convertComponentWithRegEx(String component) {
return component.replaceFirst("\\d$", ".+");
}
}

View File

@ -0,0 +1,52 @@
/*
* Copyright (c) 2010-2025 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.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import java.util.stream.Collectors;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* The {@link AddressComponentChannelUtil} is a small helper class for e.g. to split a list of {@link Address} in
* {@link AddressComponent} and a list of {@link AddressChannel} for a group REST-API request.
*
* @author Philipp Schneider - Initial contribution
*/
@NonNullByDefault
public class AddressComponentChannelUtil {
public static List<String> createComponentRequests(List<Address> addresses) {
return split(addresses).entrySet().stream()
.map(entry -> createComponentRequest(entry.getKey(), entry.getValue())).toList();
}
protected static Map<AddressComponent, Set<AddressChannel>> split(List<Address> addresses) {
return addresses.stream().collect(Collectors.toMap(Address::getComponent,
value -> new TreeSet<AddressChannel>(List.of(value.getChannel())), (existing, newest) -> {
existing.addAll(newest);
return existing;
}));
}
protected static String createComponentRequest(AddressComponent component, Set<AddressChannel> channels) {
// Grouping REST-API requests - e.g. http://...:8084/rest/channel/_sum/(State|EssSoc)
// For valid URIs the pipe delimiter must be encoded as %7C
return component.component() + "/("
+ String.join("%7C", channels.stream().map(AddressChannel::channel).toList()) + ")";
}
}

View File

@ -14,6 +14,8 @@ package org.openhab.binding.fenecon.internal.api;
import java.net.URI; import java.net.URI;
import java.net.URISyntaxException; import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional; import java.util.Optional;
import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
@ -33,6 +35,8 @@ import org.openhab.binding.fenecon.internal.exception.FeneconException;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject; import com.google.gson.JsonObject;
import com.google.gson.JsonParser; import com.google.gson.JsonParser;
import com.google.gson.JsonSyntaxException; import com.google.gson.JsonSyntaxException;
@ -68,16 +72,17 @@ public class FeneconController {
} }
/** /**
* Queries the data for a specified channel. * Queries the data for a specified channel group.
* *
* @param channel Channel to be queried, e.g. _sum/State . * @param channel Channel group to be queried, e.g. _sum/(State|EssSoc) .
* @return {@link FeneconResponse} can be optional if values are not available. * @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 * @throws FeneconException is thrown if there are problems with the connection or processing of data to the FENECON
* system. * system.
*/ */
public Optional<FeneconResponse> requestChannel(String channel) throws FeneconException { public List<FeneconResponse> requestChannel(String channel) throws FeneconException {
try { try {
URI uri = new URI(getBaseUrl(config) + "rest/channel/" + channel); URI uri = new URI(getBaseUrl(config) + "rest/channel/" + channel);
logger.trace("FENECON - uri: {}", uri);
Request request = httpClient.newRequest(uri).timeout(10, TimeUnit.SECONDS).method(HttpMethod.GET); Request request = httpClient.newRequest(uri).timeout(10, TimeUnit.SECONDS).method(HttpMethod.GET);
logger.trace("FENECON - request: {}", request); logger.trace("FENECON - request: {}", request);
@ -88,37 +93,68 @@ public class FeneconController {
int statusCode = response.getStatus(); int statusCode = response.getStatus();
if (statusCode > 300) { if (statusCode > 300) {
// Authentication error if (statusCode == 401) { // Authentication error
if (statusCode == 401) {
throw new FeneconAuthenticationException( throw new FeneconAuthenticationException(
"Authentication on the FENECON system was not possible. Check password."); "Authentication on the FENECON system was not possible. Check password.");
} else if (statusCode == 404) { // Channel-URL not supported
logger.debug("Channel request '{}' not possible, is not supported by the FENECON system.", channel);
return List.of();
} else { } else {
throw new FeneconCommunicationException("Unexpected http status code: " + statusCode); throw new FeneconCommunicationException("Unexpected http status code: " + statusCode);
} }
} else { } else {
return createResponseFromJson(JsonParser.parseString(response.getContentAsString()).getAsJsonObject()); return createResponseFromJson(JsonParser.parseString(response.getContentAsString()));
} }
} catch (TimeoutException | ExecutionException | UnsupportedOperationException | InterruptedException err) { } catch (TimeoutException | ExecutionException | UnsupportedOperationException | InterruptedException err) {
throw new FeneconCommunicationException("Communication error with FENECON system on channel: " + channel, throw new FeneconCommunicationException(
"Communication error: " + err.getMessage() + " with FENECON system on channel: " + channel, err);
} catch (URISyntaxException | IllegalStateException | JsonSyntaxException err) {
throw new FeneconCommunicationException("Syntax error: " + err.getMessage() + " on channel: " + channel,
err); err);
} catch (URISyntaxException | JsonSyntaxException err) {
throw new FeneconCommunicationException("Syntax error on channel: " + channel, err);
} }
} }
private Optional<FeneconResponse> createResponseFromJson(JsonObject response) { private List<FeneconResponse> createResponseFromJson(JsonElement jsonElement) {
if (jsonElement.isJsonArray()) {
return createResponseFromJsonArray(jsonElement.getAsJsonArray());
} else if (jsonElement.isJsonObject()) {
return createResponseFromJsonObject(jsonElement.getAsJsonObject());
} else {
throw new IllegalStateException("Unexpected response format: " + jsonElement);
}
}
private List<FeneconResponse> createResponseFromJsonArray(JsonArray jsonArray) {
// Example response: [{"address":"_sum/EssSoc","type":"INTEGER","accessMode":"RO","text":"Range
// 0..100","unit":"%","value":99}]
List<FeneconResponse> result = new ArrayList<>();
for (JsonElement each : jsonArray) {
if (each.isJsonObject()) {
result.addAll(createResponseFromJsonObject(each.getAsJsonObject()));
}
}
return result;
}
private List<FeneconResponse> createResponseFromJsonObject(JsonObject jsonObject) {
// Example response: {"address":"_sum/EssSoc","type":"INTEGER","accessMode":"RO","text":"Range // Example response: {"address":"_sum/EssSoc","type":"INTEGER","accessMode":"RO","text":"Range
// 0..100","unit":"%","value":99} // 0..100","unit":"%","value":99}
List<FeneconResponse> result = new ArrayList<>();
convertJsonObjectToResponse(jsonObject).ifPresent(result::add);
return result;
}
if (response.get("value").isJsonNull()) { private Optional<FeneconResponse> convertJsonObjectToResponse(JsonObject jsonObject) {
// Example problem response: {"address":"_sum/EssSoc","type":"INTEGER","accessMode":"RO","text":"Range if (jsonObject.get("value").isJsonNull()) {
// Example problem response:
// {"address":"_sum/EssSoc","type":"INTEGER","accessMode":"RO","text":"Range
// 0..100","unit":"%","value":null} // 0..100","unit":"%","value":null}
return Optional.empty(); return Optional.empty();
} }
String address = response.get("address").getAsString(); String address = jsonObject.get("address").getAsString();
String text = response.get("text").getAsString(); String text = jsonObject.get("text").getAsString();
String value = response.get("value").getAsString(); String value = jsonObject.get("value").getAsString();
return Optional.of(new FeneconResponse(address, text, value)); return Optional.of(new FeneconResponse(address, text, value));
} }

View File

@ -21,34 +21,54 @@ thing-type.config.fenecon.home-device.refreshInterval.description = Interval the
# channel types # channel types
channel-type.fenecon.batt-tower-current.label = FEMS Battery Current
channel-type.fenecon.batt-tower-current.description = Battery current of the FENECON energy management system (FEMS).
channel-type.fenecon.batt-tower-soh.label = Battery Health State
channel-type.fenecon.batt-tower-soh.description = Battery state of health.
channel-type.fenecon.batt-tower-voltage.label = FEMS Battery Voltage
channel-type.fenecon.batt-tower-voltage.description = Battery voltage of the FENECON energy management system (FEMS).
channel-type.fenecon.bms-pack-temperature.label = BMS Pack Temperature
channel-type.fenecon.bms-pack-temperature.description = Temperature in the battery management system (BMS) box.
channel-type.fenecon.charger-actual-power.label = Charger Actual Power
channel-type.fenecon.charger-actual-power.description = Charger actual power on the corresponding charger.
channel-type.fenecon.charger-current.label = Charger Current
channel-type.fenecon.charger-current.description = Charger current on the corresponding charger.
channel-type.fenecon.charger-power.label = Charger Power 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.charger-power.description = Current charger power of energy storage system.
channel-type.fenecon.charger-voltage.label = Charger Voltage
channel-type.fenecon.charger-voltage.description = Charger voltage on the corresponding charger.
channel-type.fenecon.consumption-active-power-phase.label = Consumer Power Phase 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-phase.description = Current active power consumer load on the corresponding phase.
channel-type.fenecon.consumption-active-power.label = Consumer Power 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-active-power.description = Current active power consumer load.
channel-type.fenecon.consumption-max-active-power.label = Consumer Max Power 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.consumption-max-active-power.description = Maximum active consumption power that was measured.
channel-type.fenecon.discharger-power.label = Discharger Power 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.discharger-power.description = Current discharger power of energy storage system.
channel-type.fenecon.emergency-power-mode.label = Emergency Power Mode 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.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.label = Battery State
channel-type.fenecon.ess-soc.description = Battery state of charge in percent channel-type.fenecon.ess-soc.description = Battery state of charge.
channel-type.fenecon.export-to-grid-power.label = Export Grid Power 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.export-to-grid-power.description = Current export power to grid.
channel-type.fenecon.exported-to-grid-energy.label = Exported Grid Energy 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.exported-to-grid-energy.description = Total energy exported to the grid.
channel-type.fenecon.fems-version.label = FEMS Version
channel-type.fenecon.fems-version.description = FENECON energy management system (FEMS) version.
channel-type.fenecon.import-from-grid-power.label = Import Grid Power 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.import-from-grid-power.description = Current import power from grid.
channel-type.fenecon.imported-from-grid-energy.label = Imported Grid Energy 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.imported-from-grid-energy.description = Total energy imported from the grid.
channel-type.fenecon.inverter-air-temperature.label = Inverter Air Temperature
channel-type.fenecon.inverter-air-temperature.description = Air temperature at the inverter.
channel-type.fenecon.inverter-radiator-temperature.label = Inverter Radiator Temperature
channel-type.fenecon.inverter-radiator-temperature.description = Radiator temperature at the inverter.
channel-type.fenecon.last-update.label = Last Update 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.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.label = Producer Power
channel-type.fenecon.production-active-power.description = Current active power producer load in watt. channel-type.fenecon.production-active-power.description = Current active power producer load.
channel-type.fenecon.production-max-active-power.label = Producer Max Power 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.production-max-active-power.description = Maximum active production power that was measured.
channel-type.fenecon.state.label = System State channel-type.fenecon.state.label = System State
channel-type.fenecon.state.description = FENECON system state channel-type.fenecon.state.description = FENECON system state
channel-type.fenecon.state.state.option.OK = Ok channel-type.fenecon.state.state.option.OK = Ok

View File

@ -14,8 +14,10 @@
<channels> <channels>
<channel id="state" typeId="state"/> <channel id="state" typeId="state"/>
<channel id="fems-version" typeId="fems-version"/>
<channel id="last-update" typeId="last-update"/> <channel id="last-update" typeId="last-update"/>
<channel id="ess-soc" typeId="ess-soc"/> <channel id="ess-soc" typeId="ess-soc"/>
<channel id="batt-tower-soh" typeId="batt-tower-soh"/>
<channel id="charger-power" typeId="charger-power"/> <channel id="charger-power" typeId="charger-power"/>
<channel id="discharger-power" typeId="discharger-power"/> <channel id="discharger-power" typeId="discharger-power"/>
<channel id="emergency-power-mode" typeId="emergency-power-mode"/> <channel id="emergency-power-mode" typeId="emergency-power-mode"/>
@ -30,8 +32,26 @@
<channel id="exported-to-grid-energy" typeId="exported-to-grid-energy"/> <channel id="exported-to-grid-energy" typeId="exported-to-grid-energy"/>
<channel id="import-from-grid-power" typeId="import-from-grid-power"/> <channel id="import-from-grid-power" typeId="import-from-grid-power"/>
<channel id="imported-from-grid-energy" typeId="imported-from-grid-energy"/> <channel id="imported-from-grid-energy" typeId="imported-from-grid-energy"/>
<channel id="inverter-air-temperature" typeId="inverter-air-temperature"/>
<channel id="inverter-radiator-temperature" typeId="inverter-radiator-temperature"/>
<channel id="bms-pack-temperature" typeId="bms-pack-temperature"/>
<channel id="batt-tower-voltage" typeId="batt-tower-voltage"/>
<channel id="batt-tower-current" typeId="batt-tower-current"/>
<channel id="charger0-actual-power" typeId="charger-actual-power"/>
<channel id="charger0-voltage" typeId="charger-voltage"/>
<channel id="charger0-current" typeId="charger-current"/>
<channel id="charger1-actual-power" typeId="charger-actual-power"/>
<channel id="charger1-voltage" typeId="charger-voltage"/>
<channel id="charger1-current" typeId="charger-current"/>
<channel id="charger2-actual-power" typeId="charger-actual-power"/>
<channel id="charger2-voltage" typeId="charger-voltage"/>
<channel id="charger2-current" typeId="charger-current"/>
</channels> </channels>
<properties>
<property name="thingTypeVersion">1</property>
</properties>
<config-description> <config-description>
<parameter name="hostname" type="text" required="true"> <parameter name="hostname" type="text" required="true">
<context>network-address</context> <context>network-address</context>
@ -76,31 +96,45 @@
</options> </options>
</state> </state>
</channel-type> </channel-type>
<channel-type id="fems-version">
<item-type>String</item-type>
<label>FEMS Version</label>
<description>FENECON energy management system (FEMS) version.</description>
<category>Text</category>
<state readOnly="true" pattern="%s"></state>
</channel-type>
<channel-type id="last-update"> <channel-type id="last-update">
<item-type>DateTime</item-type> <item-type>DateTime</item-type>
<label>Last Update</label> <label>Last Update</label>
<description>Last successful update via REST-API from the FENECON system</description> <description>Last successful update via REST-API from the FENECON system.</description>
<category>Time</category> <category>Time</category>
<state readOnly="true"></state> <state readOnly="true"></state>
</channel-type> </channel-type>
<channel-type id="ess-soc"> <channel-type id="ess-soc">
<item-type unitHint="%">Number:Dimensionless</item-type> <item-type unitHint="%">Number:Dimensionless</item-type>
<label>Battery State</label> <label>Battery State</label>
<description>Battery state of charge in percent</description> <description>Battery state of charge.</description>
<category>BatteryLevel</category>
<state readOnly="true" pattern="%.0f %unit%"/>
</channel-type>
<channel-type id="batt-tower-soh">
<item-type unitHint="%">Number:Dimensionless</item-type>
<label>Battery Health State</label>
<description>Battery state of health.</description>
<category>BatteryLevel</category> <category>BatteryLevel</category>
<state readOnly="true" pattern="%.0f %unit%"/> <state readOnly="true" pattern="%.0f %unit%"/>
</channel-type> </channel-type>
<channel-type id="charger-power"> <channel-type id="charger-power">
<item-type>Number:Power</item-type> <item-type>Number:Power</item-type>
<label>Charger Power</label> <label>Charger Power</label>
<description>Current charger power of energy storage system in watt.</description> <description>Current charger power of energy storage system.</description>
<category>Energy</category> <category>Energy</category>
<state readOnly="true" pattern="%.0f %unit%"/> <state readOnly="true" pattern="%.0f %unit%"/>
</channel-type> </channel-type>
<channel-type id="discharger-power"> <channel-type id="discharger-power">
<item-type>Number:Power</item-type> <item-type>Number:Power</item-type>
<label>Discharger Power</label> <label>Discharger Power</label>
<description>Current discharger power of energy storage system in watt.</description> <description>Current discharger power of energy storage system.</description>
<category>Energy</category> <category>Energy</category>
<state readOnly="true" pattern="%.0f %unit%"/> <state readOnly="true" pattern="%.0f %unit%"/>
</channel-type> </channel-type>
@ -114,65 +148,121 @@
<channel-type id="production-active-power"> <channel-type id="production-active-power">
<item-type>Number:Power</item-type> <item-type>Number:Power</item-type>
<label>Producer Power</label> <label>Producer Power</label>
<description>Current active power producer load in watt.</description> <description>Current active power producer load.</description>
<category>Energy</category> <category>Energy</category>
<state readOnly="true" pattern="%.0f %unit%"/> <state readOnly="true" pattern="%.0f %unit%"/>
</channel-type> </channel-type>
<channel-type id="export-to-grid-power"> <channel-type id="export-to-grid-power">
<item-type>Number:Power</item-type> <item-type>Number:Power</item-type>
<label>Export Grid Power</label> <label>Export Grid Power</label>
<description>Current export power to grid in watt.</description> <description>Current export power to grid.</description>
<category>Energy</category> <category>Energy</category>
<state readOnly="true" pattern="%.0f %unit%"/> <state readOnly="true" pattern="%.0f %unit%"/>
</channel-type> </channel-type>
<channel-type id="exported-to-grid-energy"> <channel-type id="exported-to-grid-energy">
<item-type>Number:Energy</item-type> <item-type>Number:Energy</item-type>
<label>Exported Grid Energy</label> <label>Exported Grid Energy</label>
<description>Total energy exported to the grid in watt per hour.</description> <description>Total energy exported to the grid.</description>
<category>Energy</category> <category>Energy</category>
<state readOnly="true" pattern="%.0f %unit%"/> <state readOnly="true" pattern="%.0f %unit%"/>
</channel-type> </channel-type>
<channel-type id="consumption-active-power"> <channel-type id="consumption-active-power">
<item-type>Number:Power</item-type> <item-type>Number:Power</item-type>
<label>Consumer Power</label> <label>Consumer Power</label>
<description>Current active power consumer load in watt.</description> <description>Current active power consumer load.</description>
<category>Energy</category> <category>Energy</category>
<state readOnly="true" pattern="%.0f %unit%"/> <state readOnly="true" pattern="%.0f %unit%"/>
</channel-type> </channel-type>
<channel-type id="consumption-active-power-phase"> <channel-type id="consumption-active-power-phase">
<item-type>Number:Power</item-type> <item-type>Number:Power</item-type>
<label>Consumer Power Phase</label> <label>Consumer Power Phase</label>
<description>Current active power consumer load in watt on the corresponding phase.</description> <description>Current active power consumer load on the corresponding phase.</description>
<category>Energy</category> <category>Energy</category>
<state readOnly="true" pattern="%.0f %unit%"/> <state readOnly="true" pattern="%.0f %unit%"/>
</channel-type> </channel-type>
<channel-type id="consumption-max-active-power"> <channel-type id="consumption-max-active-power">
<item-type>Number:Power</item-type> <item-type>Number:Power</item-type>
<label>Consumer Max Power</label> <label>Consumer Max Power</label>
<description>Maximum active consumption power in watt that was measured.</description> <description>Maximum active consumption power that was measured.</description>
<category>Energy</category> <category>Energy</category>
<state readOnly="true" pattern="%.0f %unit%"/> <state readOnly="true" pattern="%.0f %unit%"/>
</channel-type> </channel-type>
<channel-type id="production-max-active-power"> <channel-type id="production-max-active-power">
<item-type>Number:Power</item-type> <item-type>Number:Power</item-type>
<label>Producer Max Power</label> <label>Producer Max Power</label>
<description>Maximum active production power in watt that was measured.</description> <description>Maximum active production power that was measured.</description>
<category>Energy</category> <category>Energy</category>
<state readOnly="true" pattern="%.0f %unit%"/> <state readOnly="true" pattern="%.0f %unit%"/>
</channel-type> </channel-type>
<channel-type id="import-from-grid-power"> <channel-type id="import-from-grid-power">
<item-type>Number:Power</item-type> <item-type>Number:Power</item-type>
<label>Import Grid Power</label> <label>Import Grid Power</label>
<description>Current import power from grid in watt.</description> <description>Current import power from grid.</description>
<category>Energy</category> <category>Energy</category>
<state readOnly="true" pattern="%.0f %unit%"/> <state readOnly="true" pattern="%.0f %unit%"/>
</channel-type> </channel-type>
<channel-type id="imported-from-grid-energy"> <channel-type id="imported-from-grid-energy">
<item-type>Number:Energy</item-type> <item-type>Number:Energy</item-type>
<label>Imported Grid Energy</label> <label>Imported Grid Energy</label>
<description>Total energy imported from the grid in watt per hour.</description> <description>Total energy imported from the grid.</description>
<category>Energy</category> <category>Energy</category>
<state readOnly="true" pattern="%.0f %unit%"/> <state readOnly="true" pattern="%.0f %unit%"/>
</channel-type> </channel-type>
<channel-type id="inverter-air-temperature">
<item-type>Number:Temperature</item-type>
<label>Inverter Air Temperature</label>
<description>Air temperature at the inverter.</description>
<category>Temperature</category>
<state readOnly="true" pattern="%.0f %unit%"/>
</channel-type>
<channel-type id="inverter-radiator-temperature">
<item-type>Number:Temperature</item-type>
<label>Inverter Radiator Temperature</label>
<description>Radiator temperature at the inverter.</description>
<category>Temperature</category>
<state readOnly="true" pattern="%.0f %unit%"/>
</channel-type>
<channel-type id="bms-pack-temperature">
<item-type>Number:Temperature</item-type>
<label>BMS Pack Temperature</label>
<description>Temperature in the battery management system (BMS) box.</description>
<category>Temperature</category>
<state readOnly="true" pattern="%.0f %unit%"/>
</channel-type>
<channel-type id="batt-tower-voltage">
<item-type>Number:ElectricPotential</item-type>
<label>FEMS Battery Voltage</label>
<description>Battery voltage of the FENECON energy management system (FEMS).</description>
<category>Voltage</category>
<state readOnly="true" pattern="%.2f %unit%"/>
</channel-type>
<channel-type id="batt-tower-current">
<item-type>Number:ElectricCurrent</item-type>
<label>FEMS Battery Current</label>
<description>Battery current of the FENECON energy management system (FEMS).</description>
<category>Current</category>
<state readOnly="true" pattern="%.0f %unit%"/>
</channel-type>
<channel-type id="charger-actual-power">
<item-type>Number:Power</item-type>
<label>Charger Actual Power</label>
<description>Charger actual power on the corresponding charger.</description>
<category>Energy</category>
<state readOnly="true" pattern="%.0f %unit%"/>
</channel-type>
<channel-type id="charger-voltage">
<item-type>Number:ElectricPotential</item-type>
<label>Charger Voltage</label>
<description>Charger voltage on the corresponding charger.</description>
<category>Voltage</category>
<state readOnly="true" pattern="%.2f %unit%"/>
</channel-type>
<channel-type id="charger-current">
<item-type>Number:ElectricCurrent</item-type>
<label>Charger Current</label>
<description>Charger current on the corresponding charger.</description>
<category>Current</category>
<state readOnly="true" pattern="%.2f %unit%"/>
</channel-type>
</thing:thing-descriptions> </thing:thing-descriptions>

View File

@ -0,0 +1,59 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>
<update:update-descriptions xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:update="https://openhab.org/schemas/update-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/update-description/v1.0.0 https://openhab.org/schemas/update-description-1.0.0.xsd">
<thing-type uid="fenecon:home-device">
<instruction-set targetVersion="1">
<add-channel id="fems-version">
<type>fenecon:fems-version</type>
</add-channel>
<add-channel id="batt-tower-soh">
<type>fenecon:batt-tower-soh</type>
</add-channel>
<add-channel id="inverter-air-temperature">
<type>fenecon:inverter-air-temperature</type>
</add-channel>
<add-channel id="inverter-radiator-temperature">
<type>fenecon:inverter-radiator-temperature</type>
</add-channel>
<add-channel id="bms-pack-temperature">
<type>fenecon:bms-pack-temperature</type>
</add-channel>
<add-channel id="batt-tower-voltage">
<type>fenecon:batt-tower-voltage</type>
</add-channel>
<add-channel id="batt-tower-current">
<type>fenecon:batt-tower-current</type>
</add-channel>
<add-channel id="charger0-actual-power">
<type>fenecon:charger-actual-power</type>
</add-channel>
<add-channel id="charger0-voltage">
<type>fenecon:charger-voltage</type>
</add-channel>
<add-channel id="charger0-current">
<type>fenecon:charger-current</type>
</add-channel>
<add-channel id="charger1-actual-power">
<type>fenecon:charger-actual-power</type>
</add-channel>
<add-channel id="charger1-voltage">
<type>fenecon:charger-voltage</type>
</add-channel>
<add-channel id="charger1-current">
<type>fenecon:charger-current</type>
</add-channel>
<add-channel id="charger2-actual-power">
<type>fenecon:charger-actual-power</type>
</add-channel>
<add-channel id="charger2-voltage">
<type>fenecon:charger-voltage</type>
</add-channel>
<add-channel id="charger2-current">
<type>fenecon:charger-current</type>
</add-channel>
</instruction-set>
</thing-type>
</update:update-descriptions>

View File

@ -20,6 +20,7 @@ import java.util.List;
import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.NonNullByDefault;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.openhab.binding.fenecon.internal.api.Address;
/** /**
* Test for {@link FeneconBindingConstants}. * Test for {@link FeneconBindingConstants}.
@ -31,13 +32,13 @@ public class FeneconBindingConstantsTest {
@Test @Test
void checkAllAddressesAreListed() throws IllegalArgumentException, IllegalAccessException { void checkAllAddressesAreListed() throws IllegalArgumentException, IllegalAccessException {
List<String> findAddresses = new ArrayList<>(); List<Address> findAddresses = new ArrayList<>();
for (Field eachDeclaredField : FeneconBindingConstants.class.getDeclaredFields()) { for (Field eachDeclaredField : FeneconBindingConstants.class.getDeclaredFields()) {
if (eachDeclaredField.getName().endsWith("_ADDRESS")) { if (eachDeclaredField.getName().endsWith("_ADDRESS")) {
String address = (String) eachDeclaredField.get(FeneconBindingConstants.class); String address = (String) eachDeclaredField.get(FeneconBindingConstants.class);
if (address != null) { if (address != null) {
findAddresses.add(address); findAddresses.add(new Address(address));
} }
} }
} }
@ -45,4 +46,33 @@ public class FeneconBindingConstantsTest {
assertEquals(FeneconBindingConstants.ADDRESSES.size(), findAddresses.size()); assertEquals(FeneconBindingConstants.ADDRESSES.size(), findAddresses.size());
assertTrue(findAddresses.containsAll(FeneconBindingConstants.ADDRESSES)); assertTrue(findAddresses.containsAll(FeneconBindingConstants.ADDRESSES));
} }
@Test
void checkAllAddressesAreUnique() throws IllegalArgumentException, IllegalAccessException {
List<Address> findAddresses = new ArrayList<>();
for (Field eachDeclaredField : FeneconBindingConstants.class.getDeclaredFields()) {
if (eachDeclaredField.getName().endsWith("_ADDRESS")) {
String address = (String) eachDeclaredField.get(FeneconBindingConstants.class);
if (address != null) {
Address findAddress = new Address(address);
assertFalse(findAddresses.contains(findAddress),
"Duplicate address found: " + findAddress + " for field " + eachDeclaredField.getName());
findAddresses.add(findAddress);
}
}
}
}
@Test
void checkAllAddressesConsistOfComponentAndChannel() throws IllegalArgumentException, IllegalAccessException {
for (Field eachDeclaredField : FeneconBindingConstants.class.getDeclaredFields()) {
if (eachDeclaredField.getName().endsWith("_ADDRESS")) {
String address = (String) eachDeclaredField.get(FeneconBindingConstants.class);
if (address != null) {
assertDoesNotThrow(() -> new Address(address));
}
}
}
}
} }

View File

@ -0,0 +1,109 @@
/*
* Copyright (c) 2010-2025 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.*;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Stream;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.junit.jupiter.api.Test;
import org.openhab.binding.fenecon.internal.FeneconBindingConstants;
/**
* Test for {@link AddressComponentChannelUtil}.
*
* @author Philipp Schneider - Initial contribution
*/
@NonNullByDefault
public class AddressComponentChannelUtilTest {
@Test
void testCreateComponentRequests() {
// ARRANGE
List<Address> expectedSumList = List.of(new Address(FeneconBindingConstants.STATE_ADDRESS),
new Address(FeneconBindingConstants.GRID_MODE_ADDRESS), new Address("system/Version"),
new Address("battery/SoH"), new Address("battery/Current"));
// ACT
List<String> result = AddressComponentChannelUtil.createComponentRequests(expectedSumList);
// ASSERT
assertTrue(result.size() == 3);
assertTrue(result.contains("_sum/(GridMode%7CState)"));
assertTrue(result.contains("system/(Version)"));
assertTrue(result.contains("battery/(Current%7CSoH)"));
}
@Test
void testCreateComponentRequestsWithRegEx() {
// ARRANGE
List<Address> expectedSumList = List.of(new Address("system/Version"), new Address("battery0/SoH"),
new Address("battery0/Current"), new Address("battery1/SoH"));
// ACT
List<String> result = AddressComponentChannelUtil.createComponentRequests(expectedSumList);
// ASSERT
assertTrue(result.size() == 2);
assertTrue(result.contains("system/(Version)"));
assertTrue(result.contains("battery.+/(Current%7CSoH)"));
}
@Test
void testSplit() {
// ARRANGE
List<Address> expectedSumList = List.of(new Address(FeneconBindingConstants.STATE_ADDRESS),
new Address(FeneconBindingConstants.GRID_MODE_ADDRESS),
new Address(FeneconBindingConstants.CONSUMPTION_ACTIVE_POWER_ADDRESS));
List<Address> expectedFantasyList = List.of(new Address("fantasy/Potter"));
List<Address> expectedScyFiList = List.of(new Address("scify/Dune"), new Address("scify/Expanse"));
List<Address> addresses = Stream.of(expectedSumList, expectedFantasyList, expectedScyFiList)
.flatMap(Collection::stream).toList();
// ACT
Map<AddressComponent, Set<AddressChannel>> result = AddressComponentChannelUtil.split(addresses);
// ASSERT
assertTrue(result.getOrDefault(new Address(FeneconBindingConstants.STATE_ADDRESS).getComponent(), Set.of())
.containsAll(expectedSumList.stream().map(Address::getChannel).toList()));
assertTrue(result.getOrDefault(new AddressComponent("fantasy"), Set.of())
.containsAll(expectedFantasyList.stream().map(Address::getChannel).toList()));
assertTrue(result.getOrDefault(new AddressComponent("scify"), Set.of())
.containsAll(expectedScyFiList.stream().map(Address::getChannel).toList()));
}
@Test
void testCreateRequest() {
// ARRANGE
List<Address> expectedSumList = List.of(new Address(FeneconBindingConstants.STATE_ADDRESS),
new Address(FeneconBindingConstants.GRID_MODE_ADDRESS));
// ACT
AddressComponent component = new AddressComponent("_sum");
Map<AddressComponent, Set<AddressChannel>> split = AddressComponentChannelUtil.split(expectedSumList);
Set<AddressChannel> sciFyChannels = split.getOrDefault(component, Set.of());
String result = AddressComponentChannelUtil.createComponentRequest(component, sciFyChannels);
// ASSERT
assertEquals("_sum/(GridMode%7CState)", result);
}
}

View File

@ -0,0 +1,54 @@
/*
* Copyright (c) 2010-2025 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;
/**
* Test for {@link AddressComponent}.
*
* @author Philipp Schneider - Initial contribution
*/
@NonNullByDefault
public class AddressComponentTest {
@Test
void testFixComponent() {
String component = "component";
AddressComponent result = new AddressComponent(component);
assertEquals("component", result.component());
}
@Test
void testVariableComponentChangedForBundleRegexRequest1() {
String component = "charger0";
AddressComponent result = new AddressComponent(component);
assertEquals("charger.+", result.component());
}
@Test
void testVariableComponentChangedForBundleRegexRequest2() {
String component = "charger1";
AddressComponent result = new AddressComponent(component);
assertEquals("charger.+", result.component());
}
}

View File

@ -0,0 +1,71 @@
/*
* Copyright (c) 2010-2025 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.*;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.junit.jupiter.api.Test;
/**
* Test for {@link Address}.
*
* @author Philipp Schneider - Initial contribution
*/
@NonNullByDefault
public class AddressTest {
@Test
void testSplitAddress() {
String adress = "component/channel";
Address restApiAddress = new Address(adress);
assertEquals("component", restApiAddress.getComponent().component());
assertEquals("channel", restApiAddress.getChannel().channel());
}
@Test
void testInvalidAddress1() {
String invalidAddress = "invalidAddress";
assertThrowsExactly(IllegalArgumentException.class, () -> {
new Address(invalidAddress);
});
}
@Test
void testInvalidAddress2() {
String invalidAddress = "in/valid/address";
assertThrowsExactly(IllegalArgumentException.class, () -> {
new Address(invalidAddress);
});
}
@Test
void testCompareSameAddress() {
Address adress1 = new Address("component/channel");
Address adress2 = new Address("component/channel");
assertEquals(adress1, adress2);
}
@Test
void testCompareNotSameAddress() {
Address adress1 = new Address("component/channel1");
Address adress2 = new Address("component/channel2");
assertNotEquals(adress1, adress2);
}
}