[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
parent
9ef01e5f47
commit
4dacea107b
|
@ -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
|
||||||
```
|
```
|
||||||
|
|
||||||
:::
|
:::
|
||||||
|
|
|
@ -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";
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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));
|
||||||
|
}
|
||||||
|
}
|
|
@ -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$", ".+");
|
||||||
|
}
|
||||||
|
}
|
|
@ -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()) + ")";
|
||||||
|
}
|
||||||
|
}
|
|
@ -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));
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
|
@ -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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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());
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue