[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.
| 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. |
| Channel | Type | Read/Write | Description |
|-------------------------------|----------------------------|------------|--------------------------------------------------------------------------------|
| state | String | R | FENECON system state: Ok, Info, Warning or Fault |
| fems-version | String | R | FENECON energy management system (FEMS) version - e.g 2025.2.3 |
| last-update | DateTime | R | Last successful update via REST-API from the FENECON system |
| ess-soc | Number:Dimensionless | R | Battery state of charge. |
| batt-tower-soh | Number:Dimensionless | R | Battery state of health. |
| charger-power | Number:Power | R | Current charger power of energy storage system. |
| discharger-power | Number:Power | R | Current discharger power of energy storage system. |
| 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. |
| production-max-active-power | Number:Power | R | Maximum active production power that was measured. |
| export-to-grid-power | Number:Power | R | Current export power to grid. |
| exported-to-grid-energy | Number:Energy | R | Total energy exported to the grid. |
| consumption-active-power | Number:Power | R | Current active power consumer load. |
| consumption-max-active-power | Number:Power | R | Maximum active consumption power that was measured. |
| consumption-active-power-l1 | Number:Power | R | Current active power consumer load on phase 1. |
| consumption-active-power-l2 | Number:Power | R | Current active power consumer load on phase 2. |
| 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
### fenecon.things
```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
@ -66,33 +82,52 @@ Thing fenecon:home-device:local "FENECON Home" [hostname="192.168.1.11", refresh
Group Home "MyHome" <house> ["Indoor"]
Group GF "GroundFloor" <groundfloor> (Home) ["GroundFloor"]
// 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"]
// 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"}
String EssState <text> (GF_UtilityRoomSolar) ["Status"] {channel="fenecon:home-device:local:state"}
String FemsVersion <text> (GF_UtilityRoomSolar) ["Status"] {channel="fenecon:home-device:local:fems-version"}
DateTime LastFeneconUpdate <time> (GF_UtilityRoomSolar) ["Status"] {channel="fenecon:home-device:local:last-update"}
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:Dimensionless EssSoc <batterylevel> (GF_UtilityRoomSolar) ["Measurement"] {unit="%", channel="fenecon:home-device:local:ess-soc"}
Number:Dimensionless BattSoh <batterylevel> (GF_UtilityRoomSolar) ["Measurement"] {unit="%", channel="fenecon:home-device:local:batt-tower-soh"}
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 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"}
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"}
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
@ -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
:::: tabs
@ -152,6 +202,25 @@ then
var result = current * purchasedPricePerKiloWattHour;
PurchasedEnergy.postUpdate(result)
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 org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.fenecon.internal.api.Address;
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");
// List of all FENECON Addresses
// Group: _sum/...
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";
@ -46,13 +48,45 @@ public class FeneconBindingConstants {
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: _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
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);
public static final List<Address> ADDRESSES = List.of(new Address(STATE_ADDRESS), new Address(GRID_MODE_ADDRESS),
new Address(CONSUMPTION_ACTIVE_POWER_ADDRESS), new Address(CONSUMPTION_ACTIVE_POWER_PHASE1_ADDRESS),
new Address(CONSUMPTION_ACTIVE_POWER_PHASE2_ADDRESS), new Address(CONSUMPTION_ACTIVE_POWER_PHASE3_ADDRESS),
new Address(CONSUMPTION_MAX_ACTIVE_POWER_ADDRESS), new Address(PRODUCTION_MAX_ACTIVE_POWER_ADDRESS),
new Address(PRODUCTION_ACTIVE_POWER_ADDRESS), new Address(GRID_ACTIVE_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
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 IMPORTED_FROM_GRID_ENERGY_CHANNEL = "imported-from-grid-energy";
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;
import java.util.Optional;
import java.util.List;
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.AddressComponentChannelUtil;
import org.openhab.binding.fenecon.internal.api.BatteryPower;
import org.openhab.binding.fenecon.internal.api.FeneconController;
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.QuantityType;
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.thing.ChannelUID;
import org.openhab.core.thing.Thing;
@ -71,18 +73,21 @@ public class FeneconHandler extends BaseThingHandler {
}
private void pollingCode() {
for (String eachChannel : FeneconBindingConstants.ADDRESSES) {
List<String> componentRequests = AddressComponentChannelUtil
.createComponentRequests(FeneconBindingConstants.ADDRESSES);
for (String eachComponentRequest : componentRequests) {
try {
@SuppressWarnings("null")
Optional<FeneconResponse> response = feneconController.requestChannel(eachChannel);
List<FeneconResponse> responses = feneconController.requestChannel(eachComponentRequest);
if (response.isPresent()) {
processDataPoint(response.get());
for (FeneconResponse eachResponse : responses) {
processDataPoint(eachResponse);
}
updateStatus(ThingStatus.ONLINE);
} 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());
return;
}
@ -167,6 +172,102 @@ public class FeneconHandler extends BaseThingHandler {
updateState(FeneconBindingConstants.IMPORTED_FROM_GRID_ENERGY_CHANNEL,
new QuantityType<>(Integer.valueOf(response.value()), Units.WATT_HOUR));
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:
logger.trace("FENECON - No channel ID to address {} found.", response.address());
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.URISyntaxException;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
@ -33,6 +35,8 @@ import org.openhab.binding.fenecon.internal.exception.FeneconException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
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.
* @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 {
public List<FeneconResponse> requestChannel(String channel) throws FeneconException {
try {
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);
logger.trace("FENECON - request: {}", request);
@ -88,37 +93,68 @@ public class FeneconController {
int statusCode = response.getStatus();
if (statusCode > 300) {
// Authentication error
if (statusCode == 401) {
if (statusCode == 401) { // Authentication error
throw new FeneconAuthenticationException(
"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 {
throw new FeneconCommunicationException("Unexpected http status code: " + statusCode);
}
} else {
return createResponseFromJson(JsonParser.parseString(response.getContentAsString()).getAsJsonObject());
return createResponseFromJson(JsonParser.parseString(response.getContentAsString()));
}
} 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);
} 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
// 0..100","unit":"%","value":99}
List<FeneconResponse> result = new ArrayList<>();
convertJsonObjectToResponse(jsonObject).ifPresent(result::add);
return result;
}
if (response.get("value").isJsonNull()) {
// Example problem response: {"address":"_sum/EssSoc","type":"INTEGER","accessMode":"RO","text":"Range
private Optional<FeneconResponse> convertJsonObjectToResponse(JsonObject jsonObject) {
if (jsonObject.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();
String address = jsonObject.get("address").getAsString();
String text = jsonObject.get("text").getAsString();
String value = jsonObject.get("value").getAsString();
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-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.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.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.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.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.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.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.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.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.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.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.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.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.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.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.description = FENECON system state
channel-type.fenecon.state.state.option.OK = Ok

View File

@ -14,8 +14,10 @@
<channels>
<channel id="state" typeId="state"/>
<channel id="fems-version" typeId="fems-version"/>
<channel id="last-update" typeId="last-update"/>
<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="discharger-power" typeId="discharger-power"/>
<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="import-from-grid-power" typeId="import-from-grid-power"/>
<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>
<properties>
<property name="thingTypeVersion">1</property>
</properties>
<config-description>
<parameter name="hostname" type="text" required="true">
<context>network-address</context>
@ -76,31 +96,45 @@
</options>
</state>
</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">
<item-type>DateTime</item-type>
<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>
<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>
<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>
<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>
<description>Current charger power of energy storage system.</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>
<description>Current discharger power of energy storage system.</description>
<category>Energy</category>
<state readOnly="true" pattern="%.0f %unit%"/>
</channel-type>
@ -114,65 +148,121 @@
<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>
<description>Current active power producer load.</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>
<description>Current export power to grid.</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>
<description>Total energy exported to the grid.</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>
<description>Current active power consumer load.</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>
<description>Current active power consumer load 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>
<description>Maximum active consumption power 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>
<description>Maximum active production power 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>
<description>Current import power from grid.</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>
<description>Total energy imported from the grid.</description>
<category>Energy</category>
<state readOnly="true" pattern="%.0f %unit%"/>
</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>

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.junit.jupiter.api.Test;
import org.openhab.binding.fenecon.internal.api.Address;
/**
* Test for {@link FeneconBindingConstants}.
@ -31,13 +32,13 @@ public class FeneconBindingConstantsTest {
@Test
void checkAllAddressesAreListed() throws IllegalArgumentException, IllegalAccessException {
List<String> findAddresses = new ArrayList<>();
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) {
findAddresses.add(address);
findAddresses.add(new Address(address));
}
}
}
@ -45,4 +46,33 @@ public class FeneconBindingConstantsTest {
assertEquals(FeneconBindingConstants.ADDRESSES.size(), findAddresses.size());
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);
}
}