[fenecon] Initial contribution (#17174)

* Initial implementation of the FENECON Binding

Signed-off-by: Philipp Schneider <philipp.schneider@nixo-soft.de>
This commit is contained in:
Philipp S. 2024-09-08 14:48:26 +02:00 committed by GitHub
parent 66cfcc2f64
commit 6b2462ca22
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
26 changed files with 1382 additions and 0 deletions

View File

@ -112,6 +112,7 @@
/bundles/org.openhab.binding.exec/ @kgoderis
/bundles/org.openhab.binding.feed/ @openhab/add-ons-maintainers
/bundles/org.openhab.binding.feican/ @Hilbrand
/bundles/org.openhab.binding.fenecon/ @nixoso
/bundles/org.openhab.binding.fineoffsetweatherstation/ @Andy2003
/bundles/org.openhab.binding.flicbutton/ @pfink
/bundles/org.openhab.binding.fmiweather/ @ssalonen

View File

@ -551,6 +551,11 @@
<artifactId>org.openhab.binding.feican</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.openhab.addons.bundles</groupId>
<artifactId>org.openhab.binding.fenecon</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.openhab.addons.bundles</groupId>
<artifactId>org.openhab.binding.fineoffsetweatherstation</artifactId>

View File

@ -0,0 +1,13 @@
This content is produced and maintained by the openHAB project.
* Project home: https://www.openhab.org
== Declared Project Licenses
This program and the accompanying materials are made available under the terms
of the Eclipse Public License 2.0 which is available at
https://www.eclipse.org/legal/epl-2.0/.
== Source Code
https://github.com/openhab/openhab-addons

View File

@ -0,0 +1,160 @@
# FENECON Binding
The FENECON Binding integrates the [FENECON energy storage system](https://fenecon.de/) device into the openHAB system via [REST-API](https://docs.fenecon.de/_/de/fems/fems-app/OEM_App_REST_JSON.html).
With the binding, it is possible to request status information from FENECON Home to allow you home automation decisions based on the current energy management.
This makes it possible, for example, to switch on other consumers such as the dishwasher or washing machine in the case of power overproduction.
## Supported Things
Currently only one Thing is supported: The `home-device` connection to the FENECON energy storage system.
This Binding was tested with an [FENECON HOME 10](https://fenecon.de/fenecon-home-10/) device.
## Discovery
Auto-discovery is not supported.
## Thing Configuration
The FENECON Thing only needs to be configured with the `hostname`, all other parameters are optional and prefilled with the suitable default values:
| Parameter | Description |
|-----------------|----------------------------------------------------------------------------------|
| hostname | Hostname or IP address of the FENECON device, e.g. 192.168.1.11 |
| password | Password of the FENECON device. The password for guest access is set by default. |
| port | Port of the FENECON device. Default: 8084 |
| refreshInterval | Interval the device is polled in sec. Default 30 seconds |
## Channels
The FENECON binding currently only provides access to read out the values from the energy storage system.
| Channel | Type | Read/Write | Description |
|-------------------------------|----------------------|------------|-----------------------------------------------------------------------------|
| state | String | R | FENECON system state: Ok, Info, Warning or Fault |
| last-update | DateTime | R | Last successful update via REST-API from the FENECON system |
| ess-soc | Number:Dimensionless | R | Battery state of charge in percent |
| charger-power | Number:Power | R | Current charger power of energy storage system in watt. |
| discharger-power | Number:Power | R | Current discharger power of energy storage system in watt. |
| emergency-power-mode | Switch | R | Indicates if there is grid power is off and the emergency power mode is on. |
| production-active-power | Number:Power | R | Current active power producer load in watt. |
| production-max-active-power | Number:Power | R | Maximum active production power in watt that was measured. |
| export-to-grid-power | Number:Power | R | Current export power to grid in watt. |
| exported-to-grid-energy | Number:Energy | R | Total energy exported to the grid in watt per hour. |
| consumption-active-power | Number:Power | R | Current active power consumer load in watt. |
| consumption-max-active-power | Number:Power | R | Maximum active consumption power in watt that was measured. |
| consumption-active-power-l1 | Number:Power | R | Current active power consumer load in watt on phase 1. |
| consumption-active-power-l2 | Number:Power | R | Current active power consumer load in watt on phase 2. |
| consumption-active-power-l3 | Number:Power | R | Current active power consumer load in watt on phase 3. |
| import-from-grid-power | Number:Power | R | Current import power from grid in watt. |
| imported-from-grid-energy | Number:Energy | R | Total energy imported from the grid in watt per hour. |
## Full Example
### fenecon.things
```java
Thing fenecon:home-device:local "FENECON Home" [hostname="192.168.1.11", refreshInterval=5]
```
### demo.items
```java
// Sitemap Items
Group Home "MyHome" <house> ["Indoor"]
Group GF "GroundFloor" <groundfloor> (Home) ["GroundFloor"]
// Utility room
Group GF_UtilityRoom "Utility room" <energy> (Home, GF) ["Room"]
Group GF_UtilityRoomSolar "Utility room solar" <solarplant> (GF_UtilityRoom) ["Inverter"]
// FENECON items
String EssState <text> (GF_UtilityRoomSolar) ["Status"] {channel="fenecon:home-device:local:state"}
DateTime LastFeneconUpdate <time> (GF_UtilityRoomSolar) ["Status"] {channel="fenecon:home-device:local:last-update"}
Number:Dimensionless EssSoc <batterylevel> (GF_UtilityRoomSolar) ["Measurement"] {unit="%", channel="fenecon:home-device:local:ess-soc"}
Number:Power ChargerPower <energy> (GF_UtilityRoomSolar) ["Measurement", "Power"] {channel="fenecon:home-device:local:charger-power"}
Number:Power DischargerPower <energy> (GF_UtilityRoomSolar) ["Measurement", "Power"] {channel="fenecon:home-device:local:discharger-power"}
Switch EmergencyPowerMode <switch> (GF_UtilityRoomSolar) ["Switch"] {channel="fenecon:home-device:local:emergency-power-mode"}
Number:Power ProductionActivePower <energy> (GF_UtilityRoomSolar) ["Measurement", "Power"] {channel="fenecon:home-device:local:production-active-power"}
Number:Power ProductionMaxActivePower <energy> (GF_UtilityRoomSolar) ["Measurement", "Power"] {channel="fenecon:home-device:local:production-max-active-power"}
Number:Power SellToGridPower <energy> (GF_UtilityRoomSolar) ["Measurement", "Power"] {channel="fenecon:home-device:local:export-to-grid-power"}
Number:Energy TotalSellEnergy <energy> (GF_UtilityRoomSolar) ["Measurement", "Energy"] {channel="fenecon:home-device:local:exported-to-grid-energy"}
Number:Power ConsumptionActivePower <energy> (GF_UtilityRoomSolar) ["Measurement", "Power"] {channel="fenecon:home-device:local:consumption-active-power"}
Number:Power ConsumptionMaxActivePower <energy> (GF_UtilityRoomSolar) ["Measurement", "Power"] {channel="fenecon:home-device:local:consumption-max-active-power"}
Number:Power ConsumptionActivePowerPhase1 <energy> (GF_UtilityRoomSolar) ["Measurement", "Power"] {channel="fenecon:home-device:local:consumption-active-power-l1"}
Number:Power ConsumptionActivePowerPhase2 <energy> (GF_UtilityRoomSolar) ["Measurement", "Power"] {channel="fenecon:home-device:local:consumption-active-power-l2"}
Number:Power ConsumptionActivePowerPhase3 <energy> (GF_UtilityRoomSolar) ["Measurement", "Power"] {channel="fenecon:home-device:local:consumption-active-power-l3"}
Number:Power BuyFromGridPower <energy> (GF_UtilityRoomSolar) ["Measurement", "Power"] {channel="fenecon:home-device:local:import-from-grid-power"}
Number:Energy TotalBuyEnergy <energy> (GF_UtilityRoomSolar) ["Measurement", "Energy"] {channel="fenecon:home-device:local:imported-from-grid-energy"}
// Examples of items for calculating the energy purchased and sold. Look at the demo.rules section.
Number:Currency SoldEnergy "Total sold energy [%.2f €]" <price> (GF_UtilityRoomSolar)
Number:Currency PurchasedEnergy "Total purchased energy [%.2f €]" <price> (GF_UtilityRoomSolar)
```
### demo.sitemap
```perl
sitemap demo label="FENECON Example Sitemap" {
Frame label="Groundfloor" icon="groundfloor" {
Group item=GF_UtilityRoom
}
}
```
### demo.rules
:::: tabs
::: tab DSL
```java
rule "Blackout detection"
when
Item EmergencyPowerMode changed to ON
then
val msg = "🚨 Power blackout detected, emergency power mode running."
logInfo("PowerBlackout", msg)
sendBroadcastNotification(msg)
end
rule "Battery 100 percent"
when
Item EssSoc changed
then
var batteryState = (EssSoc.getState() as Number).intValue()
if(batteryState == 100){
val msg = "🔋 Full battery, consumers can be activated."
logInfo("FullBattery", msg)
sendBroadcastNotification(msg)
}
end
rule "Calculation sold energy"
when
Item TotalSellEnergy changed
then
val sellingPricePerKiloWattHour = 0.07 // €
var current = (TotalSellEnergy.getState() as Number).intValue()
var result = current * sellingPricePerKiloWattHour;
SoldEnergy.postUpdate(result)
end
rule "Calculation purchased energy"
when
Item TotalBuyEnergy changed
then
val purchasedPricePerKiloWattHour = 0.32 // €
var current = (TotalBuyEnergy.getState() as Number).intValue()
var result = current * purchasedPricePerKiloWattHour;
PurchasedEnergy.postUpdate(result)
end
```
:::
::::

View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.openhab.addons.bundles</groupId>
<artifactId>org.openhab.addons.reactor.bundles</artifactId>
<version>4.3.0-SNAPSHOT</version>
</parent>
<artifactId>org.openhab.binding.fenecon</artifactId>
<name>openHAB Add-ons :: Bundles :: FENECON Binding</name>
</project>

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<features name="org.openhab.binding.fenecon-${project.version}" xmlns="http://karaf.apache.org/xmlns/features/v1.4.0">
<repository>mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features</repository>
<feature name="openhab-binding-fenecon" description="FENECON Binding" version="${project.version}">
<feature>openhab-runtime-base</feature>
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.fenecon/${project.version}</bundle>
</feature>
</features>

View File

@ -0,0 +1,75 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.fenecon.internal;
import java.util.List;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.thing.ThingTypeUID;
/**
* The {@link FeneconBindingConstants} class defines common constants, which are
* used across the whole binding.
*
* @author Philipp Schneider - Initial contribution
*/
@NonNullByDefault
public class FeneconBindingConstants {
private static final String BINDING_ID = "fenecon";
// List of all Thing Type UIDs
public static final ThingTypeUID THING_TYPE_HOME_DEVICE = new ThingTypeUID(BINDING_ID, "home-device");
// List of all FENECON Addresses
public static final String STATE_ADDRESS = "_sum/State";
public static final String ESS_SOC_ADDRESS = "_sum/EssSoc";
public static final String CONSUMPTION_ACTIVE_POWER_ADDRESS = "_sum/ConsumptionActivePower";
public static final String CONSUMPTION_ACTIVE_POWER_PHASE1_ADDRESS = "_sum/ConsumptionActivePowerL1";
public static final String CONSUMPTION_ACTIVE_POWER_PHASE2_ADDRESS = "_sum/ConsumptionActivePowerL2";
public static final String CONSUMPTION_ACTIVE_POWER_PHASE3_ADDRESS = "_sum/ConsumptionActivePowerL3";
public static final String CONSUMPTION_MAX_ACTIVE_POWER_ADDRESS = "_sum/ConsumptionMaxActivePower";
public static final String PRODUCTION_MAX_ACTIVE_POWER_ADDRESS = "_sum/ProductionMaxActivePower";
public static final String PRODUCTION_ACTIVE_POWER_ADDRESS = "_sum/ProductionActivePower";
public static final String GRID_ACTIVE_POWER_ADDRESS = "_sum/GridActivePower";
public static final String ESS_DISCHARGE_POWER_ADDRESS = "_sum/EssDischargePower";
public static final String GRID_MODE_ADDRESS = "_sum/GridMode";
public static final String GRID_SELL_ACTIVE_ENERGY_ADDRESS = "_sum/GridSellActiveEnergy";
public static final String GRID_BUY_ACTIVE_ENERGY_ADDRESS = "_sum/GridBuyActiveEnergy";
// Group of all FENECON Addresses
public static final List<String> ADDRESSES = List.of(STATE_ADDRESS, GRID_MODE_ADDRESS,
CONSUMPTION_ACTIVE_POWER_ADDRESS, CONSUMPTION_ACTIVE_POWER_PHASE1_ADDRESS,
CONSUMPTION_ACTIVE_POWER_PHASE2_ADDRESS, CONSUMPTION_ACTIVE_POWER_PHASE3_ADDRESS,
CONSUMPTION_MAX_ACTIVE_POWER_ADDRESS, PRODUCTION_MAX_ACTIVE_POWER_ADDRESS, PRODUCTION_ACTIVE_POWER_ADDRESS,
GRID_ACTIVE_POWER_ADDRESS, GRID_BUY_ACTIVE_ENERGY_ADDRESS, GRID_SELL_ACTIVE_ENERGY_ADDRESS, ESS_SOC_ADDRESS,
ESS_DISCHARGE_POWER_ADDRESS);
// List of all Channel IDs
public static final String STATE_CHANNEL = "state";
public static final String ESS_SOC_CHANNEL = "ess-soc";
public static final String CONSUMPTION_ACTIVE_POWER_CHANNEL = "consumption-active-power";
public static final String CONSUMPTION_ACTIVE_POWER_PHASE1_CHANNEL = "consumption-active-power-l1";
public static final String CONSUMPTION_ACTIVE_POWER_PHASE2_CHANNEL = "consumption-active-power-l2";
public static final String CONSUMPTION_ACTIVE_POWER_PHASE3_CHANNEL = "consumption-active-power-l3";
public static final String CONSUMPTION_MAX_ACTIVE_POWER_CHANNEL = "consumption-max-active-power";
public static final String PRODUCTION_MAX_ACTIVE_POWER_CHANNEL = "production-max-active-power";
public static final String PRODUCTION_ACTIVE_POWER_CHANNEL = "production-active-power";
public static final String EXPORT_TO_GRID_POWER_CHANNEL = "export-to-grid-power";
public static final String IMPORT_FROM_GRID_POWER_CHANNEL = "import-from-grid-power";
public static final String ESS_CHARGER_POWER_CHANNEL = "charger-power";
public static final String ESS_DISCHARGER_POWER_CHANNEL = "discharger-power";
public static final String EMERGENCY_POWER_MODE_CHANNEL = "emergency-power-mode";
public static final String EXPORTED_TO_GRID_ENERGY_CHANNEL = "exported-to-grid-energy";
public static final String IMPORTED_FROM_GRID_ENERGY_CHANNEL = "imported-from-grid-energy";
public static final String LAST_UPDATE_CHANNEL = "last-update";
}

View File

@ -0,0 +1,29 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.fenecon.internal;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* The {@link FeneconConfiguration} class contains fields mapping thing configuration parameters.
*
* @author Philipp Schneider - Initial contribution
*/
@NonNullByDefault
public class FeneconConfiguration {
public String hostname = "";
public String password = "user";
public int port = 8084;
public int refreshInterval = 30;
}

View File

@ -0,0 +1,189 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.fenecon.internal;
import java.util.Optional;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jetty.client.HttpClient;
import org.openhab.binding.fenecon.internal.api.BatteryPower;
import org.openhab.binding.fenecon.internal.api.FeneconController;
import org.openhab.binding.fenecon.internal.api.FeneconResponse;
import org.openhab.binding.fenecon.internal.api.GridPower;
import org.openhab.binding.fenecon.internal.api.State;
import org.openhab.binding.fenecon.internal.exception.FeneconException;
import org.openhab.core.library.types.DateTimeType;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.QuantityType;
import org.openhab.core.library.types.StringType;
import org.openhab.core.library.unit.Units;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingStatusDetail;
import org.openhab.core.thing.binding.BaseThingHandler;
import org.openhab.core.types.Command;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link FeneconHandler} is responsible for handling commands, which are
* sent to one of the channels.
*
* @author Philipp Schneider - Initial contribution
*/
@NonNullByDefault
public class FeneconHandler extends BaseThingHandler {
private final Logger logger = LoggerFactory.getLogger(FeneconHandler.class);
private FeneconConfiguration config = new FeneconConfiguration();
private @Nullable ScheduledFuture<?> pollingJob;
private @Nullable FeneconController feneconController;
private final HttpClient httpClient;
public FeneconHandler(Thing thing, HttpClient httpClient) {
super(thing);
this.httpClient = httpClient;
}
@Override
public void initialize() {
config = getConfigAs(FeneconConfiguration.class);
updateStatus(ThingStatus.UNKNOWN);
feneconController = new FeneconController(config, this.httpClient);
pollingJob = scheduler.scheduleWithFixedDelay(this::pollingCode, 0, config.refreshInterval, TimeUnit.SECONDS);
}
private void pollingCode() {
for (String eachChannel : FeneconBindingConstants.ADDRESSES) {
try {
@SuppressWarnings("null")
Optional<FeneconResponse> response = feneconController.requestChannel(eachChannel);
if (response.isPresent()) {
processDataPoint(response.get());
}
updateStatus(ThingStatus.ONLINE);
} catch (FeneconException err) {
logger.trace("FENECON - connection problem on FENECON channel {}", eachChannel, err);
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, err.getMessage());
return;
}
}
// Set last successful update cycle
updateState(FeneconBindingConstants.LAST_UPDATE_CHANNEL, new DateTimeType());
}
private void processDataPoint(FeneconResponse response) throws FeneconException {
switch (response.address()) {
case FeneconBindingConstants.STATE_ADDRESS:
// {"address":"_sum/State","type":"INTEGER","accessMode":"RO","text":"0:Ok, 1:Info, 2:Warning,
// 3:Fault","unit":"","value":0}
State state = State.get(response);
updateState(FeneconBindingConstants.STATE_CHANNEL, new StringType(state.state()));
break;
case FeneconBindingConstants.ESS_SOC_ADDRESS:
updateState(FeneconBindingConstants.ESS_SOC_CHANNEL,
new QuantityType<>(Integer.valueOf(response.value()), Units.PERCENT));
break;
case FeneconBindingConstants.CONSUMPTION_ACTIVE_POWER_ADDRESS:
updateState(FeneconBindingConstants.CONSUMPTION_ACTIVE_POWER_CHANNEL,
new QuantityType<>(Integer.valueOf(response.value()), Units.WATT));
break;
case FeneconBindingConstants.CONSUMPTION_ACTIVE_POWER_PHASE1_ADDRESS:
updateState(FeneconBindingConstants.CONSUMPTION_ACTIVE_POWER_PHASE1_CHANNEL,
new QuantityType<>(Integer.valueOf(response.value()), Units.WATT));
break;
case FeneconBindingConstants.CONSUMPTION_ACTIVE_POWER_PHASE2_ADDRESS:
updateState(FeneconBindingConstants.CONSUMPTION_ACTIVE_POWER_PHASE2_CHANNEL,
new QuantityType<>(Integer.valueOf(response.value()), Units.WATT));
break;
case FeneconBindingConstants.CONSUMPTION_ACTIVE_POWER_PHASE3_ADDRESS:
updateState(FeneconBindingConstants.CONSUMPTION_ACTIVE_POWER_PHASE3_CHANNEL,
new QuantityType<>(Integer.valueOf(response.value()), Units.WATT));
break;
case FeneconBindingConstants.CONSUMPTION_MAX_ACTIVE_POWER_ADDRESS:
updateState(FeneconBindingConstants.CONSUMPTION_MAX_ACTIVE_POWER_CHANNEL,
new QuantityType<>(Integer.valueOf(response.value()), Units.WATT));
break;
case FeneconBindingConstants.PRODUCTION_MAX_ACTIVE_POWER_ADDRESS:
updateState(FeneconBindingConstants.PRODUCTION_MAX_ACTIVE_POWER_CHANNEL,
new QuantityType<>(Integer.valueOf(response.value()), Units.WATT));
break;
case FeneconBindingConstants.PRODUCTION_ACTIVE_POWER_ADDRESS:
updateState(FeneconBindingConstants.PRODUCTION_ACTIVE_POWER_CHANNEL,
new QuantityType<>(Integer.valueOf(response.value()), Units.WATT));
break;
case FeneconBindingConstants.GRID_ACTIVE_POWER_ADDRESS:
// Grid exchange power. Negative values for sell-to-grid; positive for buy-from-grid"
GridPower gridPower = GridPower.get(response);
updateState(FeneconBindingConstants.EXPORT_TO_GRID_POWER_CHANNEL,
new QuantityType<>(gridPower.sellTo(), Units.WATT));
updateState(FeneconBindingConstants.IMPORT_FROM_GRID_POWER_CHANNEL,
new QuantityType<>(gridPower.buyFrom(), Units.WATT));
break;
case FeneconBindingConstants.ESS_DISCHARGE_POWER_ADDRESS:
// Actual AC-side battery discharge power of Energy Storage System.
// Negative values for charge; positive for discharge
BatteryPower batteryPower = BatteryPower.get(response);
updateState(FeneconBindingConstants.ESS_CHARGER_POWER_CHANNEL,
new QuantityType<>(batteryPower.chargerPower(), Units.WATT));
updateState(FeneconBindingConstants.ESS_DISCHARGER_POWER_CHANNEL,
new QuantityType<>(batteryPower.dischargerPower(), Units.WATT));
break;
case FeneconBindingConstants.GRID_MODE_ADDRESS:
// text":"1:On-Grid, 2:Off-Grid","unit":"","value":1
Integer gridMod = Integer.valueOf(response.value());
updateState(FeneconBindingConstants.EMERGENCY_POWER_MODE_CHANNEL,
gridMod == 2 ? OnOffType.ON : OnOffType.OFF);
break;
case FeneconBindingConstants.GRID_SELL_ACTIVE_ENERGY_ADDRESS:
// {"address":"_sum/GridSellActiveEnergy","type":"LONG","accessMode":"RO","text":"","unit":"Wh_Σ","value":374242}
updateState(FeneconBindingConstants.EXPORTED_TO_GRID_ENERGY_CHANNEL,
new QuantityType<>(Integer.valueOf(response.value()), Units.WATT_HOUR));
break;
case FeneconBindingConstants.GRID_BUY_ACTIVE_ENERGY_ADDRESS:
// "address":"_sum/GridBuyActiveEnergy","type":"LONG","accessMode":"RO","text":"","unit":"Wh_Σ","value":1105}
updateState(FeneconBindingConstants.IMPORTED_FROM_GRID_ENERGY_CHANNEL,
new QuantityType<>(Integer.valueOf(response.value()), Units.WATT_HOUR));
break;
default:
logger.trace("FENECON - No channel ID to address {} found.", response.address());
break;
}
}
@Override
public void dispose() {
ScheduledFuture<?> job = pollingJob;
if (job != null) {
job.cancel(true);
pollingJob = null;
}
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
// Noop
}
}

View File

@ -0,0 +1,64 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.fenecon.internal;
import static org.openhab.binding.fenecon.internal.FeneconBindingConstants.THING_TYPE_HOME_DEVICE;
import java.util.Set;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.io.net.http.HttpClientFactory;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.binding.BaseThingHandlerFactory;
import org.openhab.core.thing.binding.ThingHandler;
import org.openhab.core.thing.binding.ThingHandlerFactory;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
/**
* The {@link FeneconHandlerFactory} is responsible for creating things and thing
* handlers.
*
* @author Philipp Schneider - Initial contribution
*/
@NonNullByDefault
@Component(configurationPid = "binding.fenecon", service = ThingHandlerFactory.class)
public class FeneconHandlerFactory extends BaseThingHandlerFactory {
private static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Set.of(THING_TYPE_HOME_DEVICE);
private final HttpClientFactory httpClientFactory;
@Activate
public FeneconHandlerFactory(@Reference HttpClientFactory httpClientFactory) {
this.httpClientFactory = httpClientFactory;
}
@Override
public boolean supportsThingType(ThingTypeUID thingTypeUID) {
return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID);
}
@Override
protected @Nullable ThingHandler createHandler(Thing thing) {
ThingTypeUID thingTypeUID = thing.getThingTypeUID();
if (THING_TYPE_HOME_DEVICE.equals(thingTypeUID)) {
return new FeneconHandler(thing, httpClientFactory.getCommonHttpClient());
}
return null;
}
}

View File

@ -0,0 +1,39 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.fenecon.internal.api;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* The {@link BatteryPower} is a small helper class to convert the power value from battery.
*
* @author Philipp Schneider - Initial contribution
*/
@NonNullByDefault
public record BatteryPower(int chargerPower, int dischargerPower) {
public static BatteryPower get(FeneconResponse response) {
// Actual AC-side battery discharge power of Energy Storage System.
// Negative values for charge; positive for discharge
Integer powerValue = Integer.valueOf(response.value());
int chargerPower = 0;
int dischargerPower = 0;
if (powerValue < 0) {
chargerPower = powerValue * -1;
} else {
dischargerPower = powerValue;
}
return new BatteryPower(chargerPower, dischargerPower);
}
}

View File

@ -0,0 +1,125 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.fenecon.internal.api;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Optional;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.client.api.AuthenticationStore;
import org.eclipse.jetty.client.api.ContentResponse;
import org.eclipse.jetty.client.api.Request;
import org.eclipse.jetty.client.util.BasicAuthentication;
import org.eclipse.jetty.http.HttpMethod;
import org.openhab.binding.fenecon.internal.FeneconConfiguration;
import org.openhab.binding.fenecon.internal.exception.FeneconAuthenticationException;
import org.openhab.binding.fenecon.internal.exception.FeneconCommunicationException;
import org.openhab.binding.fenecon.internal.exception.FeneconException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import com.google.gson.JsonSyntaxException;
/**
* The {@link FeneconController} class provides API access to the FENECON system.
*
* @author Philipp Schneider - Initial contribution
*/
@NonNullByDefault
public class FeneconController {
private final Logger logger = LoggerFactory.getLogger(FeneconController.class);
private final FeneconConfiguration config;
private final HttpClient httpClient;
public FeneconController(FeneconConfiguration config, HttpClient httpClient) {
this.config = config;
this.httpClient = httpClient;
logger.debug("FENECON: initialize REST-API connection to {} with polling interval: {} sec", getBaseUrl(config),
config.refreshInterval);
// Set BasicAuthentication for all requests on the http connection
AuthenticationStore auth = httpClient.getAuthenticationStore();
URI uri = URI.create(getBaseUrl(config));
auth.addAuthenticationResult(new BasicAuthentication.BasicResult(uri, "x", config.password));
}
private String getBaseUrl(FeneconConfiguration config) {
return "http://" + config.hostname + ":" + config.port + "/";
}
/**
* Queries the data for a specified channel.
*
* @param channel Channel to be queried, e.g. _sum/State .
* @return {@link FeneconResponse} can be optional if values are not available.
* @throws FeneconException is thrown if there are problems with the connection or processing of data to the FENECON
* system.
*/
public Optional<FeneconResponse> requestChannel(String channel) throws FeneconException {
try {
URI uri = new URI(getBaseUrl(config) + "rest/channel/" + channel);
Request request = httpClient.newRequest(uri).timeout(10, TimeUnit.SECONDS).method(HttpMethod.GET);
logger.trace("FENECON - request: {}", request);
ContentResponse response = request.send();
logger.trace("FENECON - response status code: {} body: {}", response.getStatus(),
response.getContentAsString());
int statusCode = response.getStatus();
if (statusCode > 300) {
// Authentication error
if (statusCode == 401) {
throw new FeneconAuthenticationException(
"Authentication on the FENECON system was not possible. Check password.");
} else {
throw new FeneconCommunicationException("Unexpected http status code: " + statusCode);
}
} else {
return createResponseFromJson(JsonParser.parseString(response.getContentAsString()).getAsJsonObject());
}
} catch (TimeoutException | ExecutionException | UnsupportedOperationException | InterruptedException err) {
throw new FeneconCommunicationException("Communication error with FENECON system on channel: " + channel,
err);
} catch (URISyntaxException | JsonSyntaxException err) {
throw new FeneconCommunicationException("Syntax error on channel: " + channel, err);
}
}
private Optional<FeneconResponse> createResponseFromJson(JsonObject response) {
// Example response: {"address":"_sum/EssSoc","type":"INTEGER","accessMode":"RO","text":"Range
// 0..100","unit":"%","value":99}
if (response.get("value").isJsonNull()) {
// Example problem response: {"address":"_sum/EssSoc","type":"INTEGER","accessMode":"RO","text":"Range
// 0..100","unit":"%","value":null}
return Optional.empty();
}
String address = response.get("address").getAsString();
String text = response.get("text").getAsString();
String value = response.get("value").getAsString();
return Optional.of(new FeneconResponse(address, text, value));
}
}

View File

@ -0,0 +1,24 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.fenecon.internal.api;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* The {@link FeneconResponse} class provides the response from the FENECON system.
*
* @author Philipp Schneider - Initial contribution
*/
@NonNullByDefault
public record FeneconResponse(String address, String text, String value) {
};

View File

@ -0,0 +1,38 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.fenecon.internal.api;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* The {@link GridPower} is a small helper class to convert the grid value.
*
* @author Philipp Schneider - Initial contribution
*/
@NonNullByDefault
public record GridPower(int sellTo, int buyFrom) {
public static GridPower get(FeneconResponse response) {
// Grid exchange power. Negative values for sell-to-grid; positive for buy-from-grid"
Integer gridValue = Integer.valueOf(response.value());
int selltoGridPower = 0;
int buyFromGridPower = 0;
if (gridValue < 0) {
selltoGridPower = gridValue * -1;
} else {
buyFromGridPower = gridValue;
}
return new GridPower(selltoGridPower, buyFromGridPower);
}
}

View File

@ -0,0 +1,43 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.fenecon.internal.api;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* The {@link State} is a small helper class to convert the state value.
*
* @author Philipp Schneider - Initial contribution
*/
@NonNullByDefault
public record State(String state) {
public static State get(FeneconResponse response) {
// {"address":"_sum/State","type":"INTEGER","accessMode":"RO","text":"0:Ok, 1:Info, 2:Warning,
// 3:Fault","unit":"","value":0}
String text = response.text();
int begin = text.indexOf(response.value() + ":");
int end = text.indexOf(",", begin);
// No value to text mapping
if (begin < 0) {
return new State("Unknown");
}
// Last text
if (end < 0) {
end = text.length();
}
return new State(text.substring(begin + 2, end));
}
}

View File

@ -0,0 +1,30 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.fenecon.internal.exception;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* The class {@link FeneconAuthenticationException} is thrown if a authentication on the FENECON system is not possible.
*
* @author Philipp Schneider - Initial contribution
*/
@NonNullByDefault
public class FeneconAuthenticationException extends FeneconException {
private static final long serialVersionUID = -9206453599559316730L;
public FeneconAuthenticationException(String message) {
super(message);
}
}

View File

@ -0,0 +1,34 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.fenecon.internal.exception;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* The class {@link FeneconCommunicationException} is thrown if a communication problem occurs with the FENECON system.
*
* @author Philipp Schneider - Initial contribution
*/
@NonNullByDefault
public class FeneconCommunicationException extends FeneconException {
private static final long serialVersionUID = -4334759327203382902L;
public FeneconCommunicationException(String message) {
super(message);
}
public FeneconCommunicationException(String message, Exception exception) {
super(message, exception);
}
}

View File

@ -0,0 +1,42 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.fenecon.internal.exception;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* The {@link FeneconException} class provides general exception for this binding.
*
* @author Philipp Schneider - Initial contribution
*/
@NonNullByDefault
public class FeneconException extends Exception {
private static final long serialVersionUID = 4454633961827361165L;
public FeneconException() {
// noop
}
public FeneconException(String message) {
super(message);
}
public FeneconException(Exception exception) {
super(exception);
}
public FeneconException(String message, Exception exception) {
super(message, exception);
}
}

View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<addon:addon id="fenecon" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:addon="https://openhab.org/schemas/addon/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/addon/v1.0.0 https://openhab.org/schemas/addon-1.0.0.xsd">
<type>binding</type>
<name>FENECON Home Binding</name>
<description>This is the binding for FENECON Home.</description>
<connection>local</connection>
</addon:addon>

View File

@ -0,0 +1,57 @@
# add-on
addon.fenecon.name = FENECON Home Binding
addon.fenecon.description = This is the binding for FENECON Home.
# thing types
thing-type.fenecon.home-device.label = FENECON Home
thing-type.fenecon.home-device.description = Thing for FENECON Home Device
# thing types config
thing-type.config.fenecon.home-device.hostname.label = Hostname
thing-type.config.fenecon.home-device.hostname.description = Hostname or IP address of the FENECON device, e.g. 192.168.1.11
thing-type.config.fenecon.home-device.password.label = Password
thing-type.config.fenecon.home-device.password.description = Password to access the device. The password for guest access is set by default.
thing-type.config.fenecon.home-device.port.label = Port
thing-type.config.fenecon.home-device.port.description = Port of the FENECON device
thing-type.config.fenecon.home-device.refreshInterval.label = Refresh Interval
thing-type.config.fenecon.home-device.refreshInterval.description = Interval the device is polled in sec.
# channel types
channel-type.fenecon.charger-power.label = Charger Power
channel-type.fenecon.charger-power.description = Current charger power of energy storage system in watt.
channel-type.fenecon.consumption-active-power-phase.label = Consumer Power Phase
channel-type.fenecon.consumption-active-power-phase.description = Current active power consumer load in watt on the corresponding phase.
channel-type.fenecon.consumption-active-power.label = Consumer Power
channel-type.fenecon.consumption-active-power.description = Current active power consumer load in watt.
channel-type.fenecon.consumption-max-active-power.label = Consumer Max Power
channel-type.fenecon.consumption-max-active-power.description = Maximum active consumption power in watt that was measured.
channel-type.fenecon.discharger-power.label = Discharger Power
channel-type.fenecon.discharger-power.description = Current discharger power of energy storage system in watt.
channel-type.fenecon.emergency-power-mode.label = Emergency Power Mode
channel-type.fenecon.emergency-power-mode.description = Indicates if there is no power from the grid and the emergency power mode is on.
channel-type.fenecon.ess-soc.label = Battery State
channel-type.fenecon.ess-soc.description = Battery state of charge in percent
channel-type.fenecon.export-to-grid-power.label = Export Grid Power
channel-type.fenecon.export-to-grid-power.description = Current export power to grid in watt.
channel-type.fenecon.exported-to-grid-energy.label = Exported Grid Energy
channel-type.fenecon.exported-to-grid-energy.description = Total energy exported to the grid in watt per hour.
channel-type.fenecon.import-from-grid-power.label = Import Grid Power
channel-type.fenecon.import-from-grid-power.description = Current import power from grid in watt.
channel-type.fenecon.imported-from-grid-energy.label = Imported Grid Energy
channel-type.fenecon.imported-from-grid-energy.description = Total energy imported from the grid in watt per hour.
channel-type.fenecon.last-update.label = Last Update
channel-type.fenecon.last-update.description = Last successful update via REST-API from the FENECON system
channel-type.fenecon.production-active-power.label = Producer Power
channel-type.fenecon.production-active-power.description = Current active power producer load in watt.
channel-type.fenecon.production-max-active-power.label = Producer Max Power
channel-type.fenecon.production-max-active-power.description = Maximum active production power in watt that was measured.
channel-type.fenecon.state.label = System State
channel-type.fenecon.state.description = FENECON system state
channel-type.fenecon.state.state.option.OK = Ok
channel-type.fenecon.state.state.option.INFO = Info
channel-type.fenecon.state.state.option.WARN = Warning
channel-type.fenecon.state.state.option.FAULT = Fault

View File

@ -0,0 +1,177 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="fenecon"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
<!-- Sample Thing Type -->
<thing-type id="home-device">
<label>FENECON Home</label>
<description>Thing for FENECON Home Device</description>
<category>Solarplant</category>
<channels>
<channel id="state" typeId="state"/>
<channel id="last-update" typeId="last-update"/>
<channel id="ess-soc" typeId="ess-soc"/>
<channel id="charger-power" typeId="charger-power"/>
<channel id="discharger-power" typeId="discharger-power"/>
<channel id="emergency-power-mode" typeId="emergency-power-mode"/>
<channel id="consumption-active-power" typeId="consumption-active-power"/>
<channel id="consumption-active-power-l1" typeId="consumption-active-power-phase"/>
<channel id="consumption-active-power-l2" typeId="consumption-active-power-phase"/>
<channel id="consumption-active-power-l3" typeId="consumption-active-power-phase"/>
<channel id="consumption-max-active-power" typeId="consumption-max-active-power"/>
<channel id="production-max-active-power" typeId="production-max-active-power"/>
<channel id="production-active-power" typeId="production-active-power"/>
<channel id="export-to-grid-power" typeId="export-to-grid-power"/>
<channel id="exported-to-grid-energy" typeId="exported-to-grid-energy"/>
<channel id="import-from-grid-power" typeId="import-from-grid-power"/>
<channel id="imported-from-grid-energy" typeId="imported-from-grid-energy"/>
</channels>
<config-description>
<parameter name="hostname" type="text" required="true">
<context>network-address</context>
<label>Hostname</label>
<description>Hostname or IP address of the FENECON device, e.g. 192.168.1.11</description>
</parameter>
<parameter name="password" type="text">
<context>password</context>
<label>Password</label>
<default>user</default>
<description>Password to access the device. The password for guest access is set by default.</description>
<advanced>true</advanced>
</parameter>
<parameter name="port" type="integer" min="1">
<context>network-address</context>
<label>Port</label>
<description>Port of the FENECON device</description>
<default>8084</default>
<advanced>true</advanced>
</parameter>
<parameter name="refreshInterval" type="integer" unit="s" min="1">
<label>Refresh Interval</label>
<description>Interval the device is polled in sec.</description>
<default>30</default>
<advanced>true</advanced>
</parameter>
</config-description>
</thing-type>
<!-- Channel Types -->
<channel-type id="state">
<item-type>String</item-type>
<label>System State</label>
<description>FENECON system state</description>
<category>Text</category>
<state readOnly="true" pattern="%s">
<options>
<option value="OK">Ok</option>
<option value="INFO">Info</option>
<option value="WARN">Warning</option>
<option value="FAULT">Fault</option>
</options>
</state>
</channel-type>
<channel-type id="last-update">
<item-type>DateTime</item-type>
<label>Last Update</label>
<description>Last successful update via REST-API from the FENECON system</description>
<category>Time</category>
<state readOnly="true"></state>
</channel-type>
<channel-type id="ess-soc">
<item-type unitHint="%">Number:Dimensionless</item-type>
<label>Battery State</label>
<description>Battery state of charge in percent</description>
<category>BatteryLevel</category>
<state readOnly="true" pattern="%.0f %unit%"/>
</channel-type>
<channel-type id="charger-power">
<item-type>Number:Power</item-type>
<label>Charger Power</label>
<description>Current charger power of energy storage system in watt.</description>
<category>Energy</category>
<state readOnly="true" pattern="%.0f %unit%"/>
</channel-type>
<channel-type id="discharger-power">
<item-type>Number:Power</item-type>
<label>Discharger Power</label>
<description>Current discharger power of energy storage system in watt.</description>
<category>Energy</category>
<state readOnly="true" pattern="%.0f %unit%"/>
</channel-type>
<channel-type id="emergency-power-mode">
<item-type>Switch</item-type>
<label>Emergency Power Mode</label>
<description>Indicates if there is no power from the grid and the emergency power mode is on.</description>
<category>Switch</category>
<state readOnly="true"/>
</channel-type>
<channel-type id="production-active-power">
<item-type>Number:Power</item-type>
<label>Producer Power</label>
<description>Current active power producer load in watt.</description>
<category>Energy</category>
<state readOnly="true" pattern="%.0f %unit%"/>
</channel-type>
<channel-type id="export-to-grid-power">
<item-type>Number:Power</item-type>
<label>Export Grid Power</label>
<description>Current export power to grid in watt.</description>
<category>Energy</category>
<state readOnly="true" pattern="%.0f %unit%"/>
</channel-type>
<channel-type id="exported-to-grid-energy">
<item-type>Number:Energy</item-type>
<label>Exported Grid Energy</label>
<description>Total energy exported to the grid in watt per hour.</description>
<category>Energy</category>
<state readOnly="true" pattern="%.0f %unit%"/>
</channel-type>
<channel-type id="consumption-active-power">
<item-type>Number:Power</item-type>
<label>Consumer Power</label>
<description>Current active power consumer load in watt.</description>
<category>Energy</category>
<state readOnly="true" pattern="%.0f %unit%"/>
</channel-type>
<channel-type id="consumption-active-power-phase">
<item-type>Number:Power</item-type>
<label>Consumer Power Phase</label>
<description>Current active power consumer load in watt on the corresponding phase.</description>
<category>Energy</category>
<state readOnly="true" pattern="%.0f %unit%"/>
</channel-type>
<channel-type id="consumption-max-active-power">
<item-type>Number:Power</item-type>
<label>Consumer Max Power</label>
<description>Maximum active consumption power in watt that was measured.</description>
<category>Energy</category>
<state readOnly="true" pattern="%.0f %unit%"/>
</channel-type>
<channel-type id="production-max-active-power">
<item-type>Number:Power</item-type>
<label>Producer Max Power</label>
<description>Maximum active production power in watt that was measured.</description>
<category>Energy</category>
<state readOnly="true" pattern="%.0f %unit%"/>
</channel-type>
<channel-type id="import-from-grid-power">
<item-type>Number:Power</item-type>
<label>Import Grid Power</label>
<description>Current import power from grid in watt.</description>
<category>Energy</category>
<state readOnly="true" pattern="%.0f %unit%"/>
</channel-type>
<channel-type id="imported-from-grid-energy">
<item-type>Number:Energy</item-type>
<label>Imported Grid Energy</label>
<description>Total energy imported from the grid in watt per hour.</description>
<category>Energy</category>
<state readOnly="true" pattern="%.0f %unit%"/>
</channel-type>
</thing:thing-descriptions>

View File

@ -0,0 +1,48 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.fenecon.internal;
import static org.junit.jupiter.api.Assertions.*;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.junit.jupiter.api.Test;
/**
* Test for {@link FeneconBindingConstants}.
*
* @author Philipp Schneider - Initial contribution
*/
@NonNullByDefault
public class FeneconBindingConstantsTest {
@Test
void checkAllAddressesAreListed() throws IllegalArgumentException, IllegalAccessException {
List<String> findAddresses = new ArrayList<>();
for (Field eachDeclaredField : FeneconBindingConstants.class.getDeclaredFields()) {
if (eachDeclaredField.getName().endsWith("_ADDRESS")) {
String address = (String) eachDeclaredField.get(FeneconBindingConstants.class);
if (address != null) {
findAddresses.add(address);
}
}
}
assertEquals(FeneconBindingConstants.ADDRESSES.size(), findAddresses.size());
assertTrue(findAddresses.containsAll(FeneconBindingConstants.ADDRESSES));
}
}

View File

@ -0,0 +1,44 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.fenecon.internal.api;
import static org.junit.jupiter.api.Assertions.assertEquals;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.junit.jupiter.api.Test;
import org.openhab.binding.fenecon.internal.FeneconBindingConstants;
/**
* Test for {@link BatteryPower}.
*
* @author Philipp Schneider - Initial contribution
*/
@NonNullByDefault
public class BatteryPowerTest {
@Test
void testCharging() {
BatteryPower batteryPower = BatteryPower
.get(new FeneconResponse(FeneconBindingConstants.ESS_DISCHARGE_POWER_ADDRESS, "comment", "-1777"));
assertEquals(1777, batteryPower.chargerPower());
assertEquals(0, batteryPower.dischargerPower());
}
@Test
void testDischarging() {
BatteryPower batteryPower = BatteryPower
.get(new FeneconResponse(FeneconBindingConstants.ESS_DISCHARGE_POWER_ADDRESS, "comment", "1777"));
assertEquals(1777, batteryPower.dischargerPower());
assertEquals(0, batteryPower.chargerPower());
}
}

View File

@ -0,0 +1,44 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.fenecon.internal.api;
import static org.junit.jupiter.api.Assertions.assertEquals;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.junit.jupiter.api.Test;
import org.openhab.binding.fenecon.internal.FeneconBindingConstants;
/**
* Test for {@link GridPower}.
*
* @author Philipp Schneider - Initial contribution
*/
@NonNullByDefault
public class GridPowerTest {
@Test
void testSelling() {
GridPower gridValue = GridPower
.get(new FeneconResponse(FeneconBindingConstants.GRID_ACTIVE_POWER_ADDRESS, "comment", "-1777"));
assertEquals(1777, gridValue.sellTo());
assertEquals(0, gridValue.buyFrom());
}
@Test
void testBuying() {
GridPower gridValue = GridPower
.get(new FeneconResponse(FeneconBindingConstants.GRID_ACTIVE_POWER_ADDRESS, "comment", "1777"));
assertEquals(1777, gridValue.buyFrom());
assertEquals(0, gridValue.sellTo());
}
}

View File

@ -0,0 +1,63 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.fenecon.internal.api;
import static org.junit.jupiter.api.Assertions.assertEquals;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.junit.jupiter.api.Test;
import org.openhab.binding.fenecon.internal.FeneconBindingConstants;
/**
* Test for {@link State}.
*
* @author Philipp Schneider - Initial contribution
*/
@NonNullByDefault
public class StateTest {
@Test
void testStateOk() {
State state = State.get(
new FeneconResponse(FeneconBindingConstants.STATE_ADDRESS, "0:Ok, 1:Info, 2:Warning, 3:Fault", "0"));
assertEquals("Ok", state.state());
}
@Test
void testStateInfo() {
State state = State.get(
new FeneconResponse(FeneconBindingConstants.STATE_ADDRESS, "0:Ok, 1:Info, 2:Warning, 3:Fault", "1"));
assertEquals("Info", state.state());
}
@Test
void testStateWarning() {
State state = State.get(
new FeneconResponse(FeneconBindingConstants.STATE_ADDRESS, "0:Ok, 1:Info, 2:Warning, 3:Fault", "2"));
assertEquals("Warning", state.state());
}
@Test
void testStateFault() {
State state = State.get(
new FeneconResponse(FeneconBindingConstants.STATE_ADDRESS, "0:Ok, 1:Info, 2:Warning, 3:Fault", "3"));
assertEquals("Fault", state.state());
}
@Test
void testStateUnknown() {
State state = State.get(
new FeneconResponse(FeneconBindingConstants.STATE_ADDRESS, "0:Ok, 1:Info, 2:Warning, 3:Fault", "4"));
assertEquals("Unknown", state.state());
}
}

View File

@ -145,6 +145,7 @@
<module>org.openhab.binding.exec</module>
<module>org.openhab.binding.feed</module>
<module>org.openhab.binding.feican</module>
<module>org.openhab.binding.fenecon</module>
<module>org.openhab.binding.fineoffsetweatherstation</module>
<module>org.openhab.binding.flicbutton</module>
<module>org.openhab.binding.fmiweather</module>