mirror of
https://github.com/openhab/openhab-addons.git
synced 2025-01-10 15:11:59 +01:00
[fronius] Add battery control Thing actions (#17170)
* [fronius] Add DTOs for /config/timeofuse HTTP endpoint Signed-off-by: Florian Hotze <florianh_dev@icloud.com> Signed-off-by: Ciprian Pascu <contact@ciprianpascu.ro>
This commit is contained in:
parent
c370ba2ba3
commit
a9ce328497
@ -2,23 +2,24 @@
|
||||
|
||||
This binding uses the [Fronius Solar API V1](https://www.fronius.com/en/solar-energy/installers-partners/technical-data/all-products/system-monitoring/open-interfaces/fronius-solar-api-json-) to obtain data from Fronius devices.
|
||||
|
||||
It supports Fronius inverters and Fronius Smart Meter.
|
||||
Supports:
|
||||
It supports Fronius inverters, smart meters and Ohmpilot devices connected to a Fronius Datamanager 1.0 / 2.0, Fronius Datalogger or with integrated Solar API V1 support.
|
||||
|
||||
Inverters with integrated Solar API V1 support include:
|
||||
|
||||
- Fronius Galvo
|
||||
- Fronius Primo
|
||||
- Fronius Symo
|
||||
- Fronius Symo Gen24
|
||||
- Fronius Smart Meter 63A
|
||||
- Fronius Smart Meter TS 65A-3
|
||||
- Fronius Ohmpilot
|
||||
- Fronius Symo Gen24 Plus
|
||||
|
||||
## Supported Things
|
||||
|
||||
| Thing Type | Description |
|
||||
| --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `bridge` | The Bridge |
|
||||
| `powerinverter` | Fronius Galvo, Symo and other Fronius inverters in combination with the Fronius Datamanager 1.0 / 2.0 or Fronius Datalogger. You can add multiple inverters that depend on the same datalogger with different device ids. (Default 1) |
|
||||
| `meter` | Fronius Smart Meter. You can add multiple smart meters with different device ids. (The default id = 0) |
|
||||
| `ohmpilot` | Fronius Ohmpilot. (The default id = 0) |
|
||||
| Thing Type | Description |
|
||||
|-----------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `bridge` | The Bridge |
|
||||
| `powerinverter` | Fronius Galvo, Symo and other Fronius inverters: You can add multiple inverters that depend on the same datalogger with different device ids. (default id = 1) |
|
||||
| `meter` | Fronius Smart Meter: You can add multiple smart meters with different device ids. (default id = 0) |
|
||||
| `ohmpilot` | Fronius Ohmpilot ( default id = 0) |
|
||||
|
||||
## Discovery
|
||||
|
||||
@ -32,10 +33,12 @@ The binding has no configuration options, all configuration is done at `bridge`,
|
||||
|
||||
### Bridge Thing Configuration
|
||||
|
||||
| Parameter | Description |
|
||||
| ----------------- | ----------------------------------------------------- |
|
||||
| `hostname` | The hostname or IP address of your Fronius Datalogger |
|
||||
| `refreshInterval` | Refresh interval in seconds |
|
||||
| Parameter | Description | Required |
|
||||
|-------------------|--------------------------------------------------------------------------------|----------|
|
||||
| `hostname` | The hostname or IP address of your Fronius Datamanager, Datalogger or inverter | Yes |
|
||||
| `username` | The username to authenticate with the inverter settings for battery control | No |
|
||||
| `password` | The password to authenticate with the inverter settings for battery control | No |
|
||||
| `refreshInterval` | Refresh interval in seconds | No |
|
||||
|
||||
### Powerinverter Thing Configuration
|
||||
|
||||
@ -138,6 +141,65 @@ The binding has no configuration options, all configuration is done at `bridge`,
|
||||
| `modelId` | The model name of the ohmpilot |
|
||||
| `serialNumber` | The serial number of the ohmpilot |
|
||||
|
||||
## Actions
|
||||
|
||||
:::tip Warning
|
||||
Battery control uses the battery management's time-dependent battery control settings of the inverter settings and therefore overrides user-specified time of use settings.
|
||||
Please note that user-specified time of use plans cannot be used together with battery control, as battery control will override the user-specified time of use settings.
|
||||
:::
|
||||
|
||||
The `powerinverter` Thing provides actions to control the battery charging and discharging behaviour of hybrid inverters, such as Symo Gen24 Plus, if username and password are provided in the bridge configuration.
|
||||
|
||||
You can retrieve the actions as follows:
|
||||
|
||||
:::: tabs
|
||||
|
||||
::: tab DSL
|
||||
|
||||
```java
|
||||
val froniusInverterActions = getActions("fronius", "fronius:powerinverter:mybridge:myinverter")
|
||||
```
|
||||
:::
|
||||
|
||||
::: tab JS
|
||||
|
||||
```javascript
|
||||
var froniusInverterActions = actions.thingActions('fronius', 'fronius:powerinverter:mybridge:myinverter');
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
::::
|
||||
|
||||
Where the first parameter must always be `fronius` and the second must be the full Thing UID of the inverter.
|
||||
|
||||
### Available Actions
|
||||
|
||||
Once the actions instance has been retrieved, you can invoke the following methods:
|
||||
|
||||
- `resetBatteryControl()`: Remove all battery control schedules from the inverter.
|
||||
- `holdBatteryCharge()`: Prevent the battery from discharging (removes all battery control schedules first and applies all the time).
|
||||
- `addHoldBatteryChargeSchedule(LocalTime from, LocalTime until)`: Add a schedule to prevent the battery from discharging in the specified time range.
|
||||
- `addHoldBatteryChargeSchedule(ZonedDateTime from, ZonedDateTime until)`: Add a schedule to prevent the battery from discharging in the specified time range.
|
||||
- `forceBatteryCharging(QuantityType<Power> power)`: Force the battery to charge with the specified power (removes all battery control schedules first and applies all the time).
|
||||
- `addForcedBatteryChargingSchedule(LocalTime from, LocalTime until, QuantityType<Power> power)`: Add a schedule to force the battery to charge with the specified power in the specified time range.
|
||||
- `addForcedBatteryChargingSchedule(ZonedDateTime from, ZonedDateTime until, QuantityType<Power> power)`: Add a schedule to force the battery to charge with the specified power in the specified time range.
|
||||
|
||||
### Examples
|
||||
|
||||
```javascript
|
||||
var froniusInverterActions = actions.thingActions('fronius', 'fronius:powerinverter:mybridge:myinverter');
|
||||
|
||||
froniusInverterActions.resetBatteryControl();
|
||||
froniusInverterActions.holdBatteryCharge();
|
||||
froniusInverterActions.forceBatteryCharging(Quantity('5 kW'));
|
||||
|
||||
froniusInverterActions.resetBatteryControl();
|
||||
froniusInverterActions.addHoldBatteryChargeSchedule(time.toZDT('18:00'), time.toZDT('22:00'));
|
||||
froniusInverterActions.addForcedBatteryChargingSchedule(time.toZDT('22:00'), time.toZDT('23:59'), Quantity('5 kW'));
|
||||
froniusInverterActions.addForcedBatteryChargingSchedule(time.toZDT('00:00'), time.toZDT('06:00'), Quantity('5 kW'));
|
||||
```
|
||||
|
||||
## Full Example
|
||||
|
||||
demo.things:
|
||||
|
@ -20,5 +20,7 @@ package org.openhab.binding.fronius.internal;
|
||||
*/
|
||||
public class FroniusBridgeConfiguration {
|
||||
public String hostname;
|
||||
public String username;
|
||||
public String password;
|
||||
public Integer refreshInterval;
|
||||
}
|
||||
|
@ -23,13 +23,16 @@ import org.openhab.binding.fronius.internal.handler.FroniusBridgeHandler;
|
||||
import org.openhab.binding.fronius.internal.handler.FroniusMeterHandler;
|
||||
import org.openhab.binding.fronius.internal.handler.FroniusOhmpilotHandler;
|
||||
import org.openhab.binding.fronius.internal.handler.FroniusSymoInverterHandler;
|
||||
import org.openhab.core.io.net.http.HttpClientFactory;
|
||||
import org.openhab.core.thing.Bridge;
|
||||
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 FroniusHandlerFactory} is responsible for creating things and thing
|
||||
@ -54,6 +57,13 @@ public class FroniusHandlerFactory extends BaseThingHandlerFactory {
|
||||
}
|
||||
};
|
||||
|
||||
private final HttpClientFactory httpClientFactory;
|
||||
|
||||
@Activate
|
||||
public FroniusHandlerFactory(@Reference HttpClientFactory httpClientFactory) {
|
||||
this.httpClientFactory = httpClientFactory;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsThingType(ThingTypeUID thingTypeUID) {
|
||||
return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID);
|
||||
@ -64,7 +74,7 @@ public class FroniusHandlerFactory extends BaseThingHandlerFactory {
|
||||
ThingTypeUID thingTypeUID = thing.getThingTypeUID();
|
||||
|
||||
if (thingTypeUID.equals(THING_TYPE_INVERTER)) {
|
||||
return new FroniusSymoInverterHandler(thing);
|
||||
return new FroniusSymoInverterHandler(thing, httpClientFactory.getCommonHttpClient());
|
||||
} else if (thingTypeUID.equals(THING_TYPE_BRIDGE)) {
|
||||
return new FroniusBridgeHandler((Bridge) thing);
|
||||
} else if (thingTypeUID.equals(THING_TYPE_METER)) {
|
||||
|
@ -0,0 +1,162 @@
|
||||
/**
|
||||
* 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.fronius.internal.action;
|
||||
|
||||
import java.time.LocalTime;
|
||||
import java.time.ZonedDateTime;
|
||||
|
||||
import javax.measure.quantity.Power;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.fronius.internal.handler.FroniusSymoInverterHandler;
|
||||
import org.openhab.core.automation.annotation.ActionInput;
|
||||
import org.openhab.core.automation.annotation.RuleAction;
|
||||
import org.openhab.core.library.types.QuantityType;
|
||||
import org.openhab.core.thing.binding.ThingActions;
|
||||
import org.openhab.core.thing.binding.ThingActionsScope;
|
||||
import org.openhab.core.thing.binding.ThingHandler;
|
||||
import org.osgi.service.component.annotations.Component;
|
||||
import org.osgi.service.component.annotations.ServiceScope;
|
||||
|
||||
/**
|
||||
* Implementation of the {@link ThingActions} interface used for controlling battery charging and discharging for
|
||||
* Fronius hybrid inverters.
|
||||
*
|
||||
* @author Florian Hotze - Initial contribution
|
||||
*/
|
||||
@Component(scope = ServiceScope.PROTOTYPE, service = FroniusSymoInverterActions.class)
|
||||
@ThingActionsScope(name = "fronius")
|
||||
@NonNullByDefault
|
||||
public class FroniusSymoInverterActions implements ThingActions {
|
||||
private @Nullable FroniusSymoInverterHandler handler;
|
||||
|
||||
public static void resetBatteryControl(ThingActions actions) {
|
||||
if (actions instanceof FroniusSymoInverterActions froniusSymoInverterActions) {
|
||||
froniusSymoInverterActions.resetBatteryControl();
|
||||
} else {
|
||||
throw new IllegalArgumentException(
|
||||
"The 'actions' argument is not an instance of FroniusSymoInverterActions");
|
||||
}
|
||||
}
|
||||
|
||||
public static void holdBatteryCharge(ThingActions actions) {
|
||||
if (actions instanceof FroniusSymoInverterActions froniusSymoInverterActions) {
|
||||
froniusSymoInverterActions.holdBatteryCharge();
|
||||
} else {
|
||||
throw new IllegalArgumentException(
|
||||
"The 'actions' argument is not an instance of FroniusSymoInverterActions");
|
||||
}
|
||||
}
|
||||
|
||||
public static void addHoldBatteryChargeSchedule(ThingActions actions, LocalTime from, LocalTime until) {
|
||||
if (actions instanceof FroniusSymoInverterActions froniusSymoInverterActions) {
|
||||
froniusSymoInverterActions.addHoldBatteryChargeSchedule(from, until);
|
||||
} else {
|
||||
throw new IllegalArgumentException(
|
||||
"The 'actions' argument is not an instance of FroniusSymoInverterActions");
|
||||
}
|
||||
}
|
||||
|
||||
public static void addHoldBatteryChargeSchedule(ThingActions actions, ZonedDateTime from, ZonedDateTime until) {
|
||||
addHoldBatteryChargeSchedule(actions, from.toLocalTime(), until.toLocalTime());
|
||||
}
|
||||
|
||||
public static void forceBatteryCharging(ThingActions actions, QuantityType<Power> power) {
|
||||
if (actions instanceof FroniusSymoInverterActions froniusSymoInverterActions) {
|
||||
froniusSymoInverterActions.forceBatteryCharging(power);
|
||||
} else {
|
||||
throw new IllegalArgumentException(
|
||||
"The 'actions' argument is not an instance of FroniusSymoInverterActions");
|
||||
}
|
||||
}
|
||||
|
||||
public static void addForcedBatteryChargingSchedule(ThingActions actions, LocalTime from, LocalTime until,
|
||||
QuantityType<Power> power) {
|
||||
if (actions instanceof FroniusSymoInverterActions froniusSymoInverterActions) {
|
||||
froniusSymoInverterActions.addForcedBatteryChargingSchedule(from, until, power);
|
||||
} else {
|
||||
throw new IllegalArgumentException(
|
||||
"The 'actions' argument is not an instance of FroniusSymoInverterActions");
|
||||
}
|
||||
}
|
||||
|
||||
public static void addForcedBatteryChargingSchedule(ThingActions actions, ZonedDateTime from, ZonedDateTime until,
|
||||
QuantityType<Power> power) {
|
||||
addForcedBatteryChargingSchedule(actions, from.toLocalTime(), until.toLocalTime(), power);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setThingHandler(@Nullable ThingHandler handler) {
|
||||
this.handler = (FroniusSymoInverterHandler) handler;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable ThingHandler getThingHandler() {
|
||||
return handler;
|
||||
}
|
||||
|
||||
@RuleAction(label = "@text/actions.reset-battery-control.label", description = "@text/actions.reset-battery-control.description")
|
||||
public void resetBatteryControl() {
|
||||
FroniusSymoInverterHandler handler = this.handler;
|
||||
if (handler != null) {
|
||||
handler.resetBatteryControl();
|
||||
}
|
||||
}
|
||||
|
||||
@RuleAction(label = "@text/actions.hold-battery-charge.label", description = "@text/actions.hold-battery-charge.description")
|
||||
public void holdBatteryCharge() {
|
||||
FroniusSymoInverterHandler handler = this.handler;
|
||||
if (handler != null) {
|
||||
handler.holdBatteryCharge();
|
||||
}
|
||||
}
|
||||
|
||||
@RuleAction(label = "@text/actions.add-hold-battery-charge-schedule.label", description = "@text/actions.add-hold-battery-charge-schedule.description")
|
||||
public void addHoldBatteryChargeSchedule(
|
||||
@ActionInput(name = "from", label = "@text/actions.from.label", description = "@text/actions.from.description") LocalTime from,
|
||||
@ActionInput(name = "until", label = "@text/actions.until.label", description = "@text/actions.until.description") LocalTime until) {
|
||||
FroniusSymoInverterHandler handler = this.handler;
|
||||
if (handler != null) {
|
||||
handler.addHoldBatteryChargeSchedule(from, until);
|
||||
}
|
||||
}
|
||||
|
||||
public void addHoldBatteryChargeSchedule(ZonedDateTime from, ZonedDateTime until) {
|
||||
addHoldBatteryChargeSchedule(from.toLocalTime(), until.toLocalTime());
|
||||
}
|
||||
|
||||
@RuleAction(label = "@text/actions.force-battery-charging.label", description = "@text/actions.force-battery-charging.description")
|
||||
public void forceBatteryCharging(
|
||||
@ActionInput(name = "power", label = "@text/actions.power.label", description = "@text/actions.power.label") QuantityType<Power> power) {
|
||||
FroniusSymoInverterHandler handler = this.handler;
|
||||
if (handler != null) {
|
||||
handler.forceBatteryCharging(power);
|
||||
}
|
||||
}
|
||||
|
||||
@RuleAction(label = "@text/actions.add-forced-battery-charging-schedule.label", description = "@text/actions.add-forced-battery-charging-schedule.description")
|
||||
public void addForcedBatteryChargingSchedule(
|
||||
@ActionInput(name = "from", label = "@text/actions.from.label", description = "@text/actions.from.description") LocalTime from,
|
||||
@ActionInput(name = "until", label = "@text/actions.until.label", description = "@text/actions.until.description") LocalTime until,
|
||||
@ActionInput(name = "power", label = "@text/actions.power.label", description = "@text/actions.power.label") QuantityType<Power> power) {
|
||||
FroniusSymoInverterHandler handler = this.handler;
|
||||
if (handler != null) {
|
||||
handler.addForcedBatteryChargingSchedule(from, until, power);
|
||||
}
|
||||
}
|
||||
|
||||
public void addForcedBatteryChargingSchedule(ZonedDateTime from, ZonedDateTime until, QuantityType<Power> power) {
|
||||
addForcedBatteryChargingSchedule(from.toLocalTime(), until.toLocalTime(), power);
|
||||
}
|
||||
}
|
@ -0,0 +1,195 @@
|
||||
/**
|
||||
* 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.fronius.internal.api;
|
||||
|
||||
import static org.openhab.binding.fronius.internal.FroniusBindingConstants.API_TIMEOUT;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.net.URI;
|
||||
import java.time.LocalTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.Properties;
|
||||
|
||||
import javax.measure.quantity.Power;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jetty.client.HttpClient;
|
||||
import org.eclipse.jetty.http.HttpHeader;
|
||||
import org.eclipse.jetty.http.HttpMethod;
|
||||
import org.openhab.binding.fronius.internal.api.dto.inverter.batterycontrol.ScheduleType;
|
||||
import org.openhab.binding.fronius.internal.api.dto.inverter.batterycontrol.TimeOfUseRecord;
|
||||
import org.openhab.binding.fronius.internal.api.dto.inverter.batterycontrol.TimeOfUseRecords;
|
||||
import org.openhab.binding.fronius.internal.api.dto.inverter.batterycontrol.TimeTableRecord;
|
||||
import org.openhab.binding.fronius.internal.api.dto.inverter.batterycontrol.WeekdaysRecord;
|
||||
import org.openhab.core.library.types.QuantityType;
|
||||
import org.openhab.core.library.unit.Units;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.JsonSyntaxException;
|
||||
|
||||
/**
|
||||
* The {@link FroniusBatteryControl} is responsible for controlling the battery of Fronius hybrid inverters through the
|
||||
* battery management's time-dependent battery control settings.
|
||||
*
|
||||
* @author Florian Hotze - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class FroniusBatteryControl {
|
||||
private static final String TIME_OF_USE_ENDPOINT = "/config/timeofuse";
|
||||
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(FroniusBatteryControl.class);
|
||||
private static final DateTimeFormatter TIME_FORMATTER = DateTimeFormatter.ofPattern("HH:mm");
|
||||
|
||||
private static final WeekdaysRecord ALL_WEEKDAYS_RECORD = new WeekdaysRecord(true, true, true, true, true, true,
|
||||
true);
|
||||
private static final LocalTime BEGIN_OF_DAY = LocalTime.of(0, 0);
|
||||
private static final LocalTime END_OF_DAY = LocalTime.of(23, 59);
|
||||
|
||||
private final Gson gson = new Gson();
|
||||
private final HttpClient httpClient;
|
||||
private final URI baseUri;
|
||||
private final String username;
|
||||
private final String password;
|
||||
private final URI timeOfUseUri;
|
||||
|
||||
public FroniusBatteryControl(HttpClient httpClient, URI baseUri, String username, String password) {
|
||||
this.httpClient = httpClient;
|
||||
this.baseUri = baseUri;
|
||||
this.username = username;
|
||||
this.password = password;
|
||||
this.timeOfUseUri = baseUri.resolve(URI.create(TIME_OF_USE_ENDPOINT));
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the time of use settings of the Fronius hybrid inverter.
|
||||
*
|
||||
* @return the time of use settings
|
||||
* @throws FroniusCommunicationException if an error occurs during communication with the inverter
|
||||
*/
|
||||
private TimeOfUseRecords getTimeOfUse() throws FroniusCommunicationException {
|
||||
// Login and get the auth header for the next request
|
||||
String authHeader = FroniusConfigAuthUtil.login(httpClient, baseUri, username, password, HttpMethod.GET,
|
||||
timeOfUseUri.getPath(), API_TIMEOUT);
|
||||
Properties headers = new Properties();
|
||||
headers.put(HttpHeader.AUTHORIZATION.asString(), authHeader);
|
||||
// Get the time of use settings
|
||||
String response = FroniusHttpUtil.executeUrl(HttpMethod.GET, timeOfUseUri.toString(), headers, null, null,
|
||||
API_TIMEOUT);
|
||||
LOGGER.trace("Time of Use settings read successfully");
|
||||
|
||||
// Parse the response body
|
||||
TimeOfUseRecords records;
|
||||
try {
|
||||
records = gson.fromJson(response, TimeOfUseRecords.class);
|
||||
} catch (JsonSyntaxException jse) {
|
||||
throw new FroniusCommunicationException("Failed to parse Time of Use settings", jse);
|
||||
}
|
||||
if (records == null) {
|
||||
throw new FroniusCommunicationException("Failed to parse Time of Use settings");
|
||||
}
|
||||
return records;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the time of use settings of the Fronius hybrid inverter.
|
||||
*
|
||||
* @param records the time of use settings
|
||||
* @throws FroniusCommunicationException if an error occurs during communication with the inverter
|
||||
*/
|
||||
private void setTimeOfUse(TimeOfUseRecords records) throws FroniusCommunicationException {
|
||||
// Login and get the auth header for the next request
|
||||
String authHeader = FroniusConfigAuthUtil.login(httpClient, baseUri, username, password, HttpMethod.POST,
|
||||
timeOfUseUri.getPath(), API_TIMEOUT);
|
||||
Properties headers = new Properties();
|
||||
headers.put(HttpHeader.AUTHORIZATION.asString(), authHeader);
|
||||
|
||||
// Set the time of use settings
|
||||
String json = gson.toJson(records);
|
||||
FroniusHttpUtil.executeUrl(HttpMethod.POST, timeOfUseUri.toString(), headers,
|
||||
new ByteArrayInputStream(json.getBytes()), "application/json", API_TIMEOUT);
|
||||
LOGGER.trace("Time of Use settings set successfully");
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the time of use settings (i.e. removes all time-dependent battery control settings) of the Fronius hybrid
|
||||
* inverter.
|
||||
*
|
||||
* @throws FroniusCommunicationException when an error occurs during communication with the inverter
|
||||
*/
|
||||
public void reset() throws FroniusCommunicationException {
|
||||
setTimeOfUse(new TimeOfUseRecords(new TimeOfUseRecord[0]));
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds the battery charge right now, i.e. prevents the battery from discharging.
|
||||
*
|
||||
* @throws FroniusCommunicationException when an error occurs during communication with the inverter
|
||||
*/
|
||||
public void holdBatteryCharge() throws FroniusCommunicationException {
|
||||
reset();
|
||||
addHoldBatteryChargeSchedule(BEGIN_OF_DAY, END_OF_DAY);
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds the battery charge during a specific time period, i.e. prevents the battery from discharging in that
|
||||
* period.
|
||||
*
|
||||
* @param from start time of the hold charge period
|
||||
* @param until end time of the hold charge period
|
||||
* @throws FroniusCommunicationException when an error occurs during communication with the inverter
|
||||
*/
|
||||
public void addHoldBatteryChargeSchedule(LocalTime from, LocalTime until) throws FroniusCommunicationException {
|
||||
TimeOfUseRecord[] currentTimeOfUse = getTimeOfUse().records();
|
||||
TimeOfUseRecord[] timeOfUse = new TimeOfUseRecord[currentTimeOfUse.length + 1];
|
||||
System.arraycopy(currentTimeOfUse, 0, timeOfUse, 0, currentTimeOfUse.length);
|
||||
|
||||
TimeOfUseRecord holdCharge = new TimeOfUseRecord(true, 0, ScheduleType.DISCHARGE_MAX,
|
||||
new TimeTableRecord(from.format(TIME_FORMATTER), until.format(TIME_FORMATTER)), ALL_WEEKDAYS_RECORD);
|
||||
timeOfUse[timeOfUse.length - 1] = holdCharge;
|
||||
setTimeOfUse(new TimeOfUseRecords(timeOfUse));
|
||||
}
|
||||
|
||||
/**
|
||||
* Forces the battery to charge right now with the specified power.
|
||||
*
|
||||
* @param power the power to charge the battery with
|
||||
* @throws FroniusCommunicationException when an error occurs during communication with the inverter
|
||||
*/
|
||||
public void forceBatteryCharging(QuantityType<Power> power) throws FroniusCommunicationException {
|
||||
reset();
|
||||
addForcedBatteryChargingSchedule(BEGIN_OF_DAY, END_OF_DAY, power);
|
||||
}
|
||||
|
||||
/**
|
||||
* Forces the battery to charge during a specific time period with the specified power.
|
||||
*
|
||||
* @param from start time of the forced charge period
|
||||
* @param until end time of the forced charge period
|
||||
* @param power the power to charge the battery with
|
||||
* @throws FroniusCommunicationException when an error occurs during communication with the inverter
|
||||
*/
|
||||
public void addForcedBatteryChargingSchedule(LocalTime from, LocalTime until, QuantityType<Power> power)
|
||||
throws FroniusCommunicationException {
|
||||
TimeOfUseRecords currentTimeOfUse = getTimeOfUse();
|
||||
TimeOfUseRecord[] timeOfUse = new TimeOfUseRecord[currentTimeOfUse.records().length + 1];
|
||||
System.arraycopy(currentTimeOfUse.records(), 0, timeOfUse, 0, currentTimeOfUse.records().length);
|
||||
|
||||
TimeOfUseRecord holdCharge = new TimeOfUseRecord(true, power.toUnit(Units.WATT).intValue(),
|
||||
ScheduleType.CHARGE_MIN, new TimeTableRecord(from.format(TIME_FORMATTER), until.format(TIME_FORMATTER)),
|
||||
ALL_WEEKDAYS_RECORD);
|
||||
timeOfUse[timeOfUse.length - 1] = holdCharge;
|
||||
setTimeOfUse(new TimeOfUseRecords(timeOfUse));
|
||||
}
|
||||
}
|
@ -0,0 +1,291 @@
|
||||
/**
|
||||
* 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.fronius.internal.api;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.eclipse.jetty.client.HttpClient;
|
||||
import org.eclipse.jetty.client.api.ContentResponse;
|
||||
import org.eclipse.jetty.client.api.Request;
|
||||
import org.eclipse.jetty.client.api.Response;
|
||||
import org.eclipse.jetty.http.HttpHeader;
|
||||
import org.eclipse.jetty.http.HttpMethod;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* The {@link FroniusConfigAuthUtil} handles the authentication process to access Fronius inverter settings, which are
|
||||
* available on the <code>/config</code> HTTP endpoints.
|
||||
* <br>
|
||||
* Due to Fronius not using the standard HTTP authorization header, it is not possible to use
|
||||
* {@link org.eclipse.jetty.client.api.AuthenticationStore} together with
|
||||
* {@link org.eclipse.jetty.client.util.DigestAuthentication} to authenticate against the Fronius inverter settings.
|
||||
*
|
||||
* @author Florian Hotze - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class FroniusConfigAuthUtil {
|
||||
private static final String AUTHENTICATE_HEADER = "X-Www-Authenticate";
|
||||
private static final String DIGEST_AUTH_HEADER_FORMAT = "Digest username=\"%s\", realm=\"%s\", nonce=\"%s\", uri=\"%s\", response=\"%s\", qop=%s, nc=%08x, cnonce=\"%s\"";
|
||||
private static final String LOGIN_ENDPOINT = "/commands/Login";
|
||||
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(FroniusConfigAuthUtil.class);
|
||||
|
||||
/**
|
||||
* Sends a HTTP GET request to the given login URI and extracts the authentication parameters from the
|
||||
* authentication header.
|
||||
* This method uses a {@link Response.Listener.Adapter} to intercept the response headers and extract the
|
||||
* authentication header, as normal digest authentication using
|
||||
* {@link org.eclipse.jetty.client.util.DigestAuthentication} does not work because Fronius uses a custom
|
||||
* authentication header.
|
||||
*
|
||||
* @param httpClient the {@link HttpClient} to use for the request
|
||||
* @param loginUri the {@link URI} of the login endpoint
|
||||
* @return a {@link Map} containing the authentication parameters of the authentication challenge
|
||||
* @throws IOException when the response does not contain the expected authentication header
|
||||
*/
|
||||
private static Map<String, String> getAuthParams(HttpClient httpClient, URI loginUri, int timeout)
|
||||
throws IOException {
|
||||
LOGGER.debug("Sending login request to get authentication challenge");
|
||||
CountDownLatch latch = new CountDownLatch(1);
|
||||
Request initialRequest = httpClient.newRequest(loginUri).timeout(timeout, TimeUnit.MILLISECONDS);
|
||||
XWwwAuthenticateHeaderListener XWwwAuthenticateHeaderListener = new XWwwAuthenticateHeaderListener(latch);
|
||||
initialRequest.onResponseHeaders(XWwwAuthenticateHeaderListener);
|
||||
initialRequest.send(result -> latch.countDown());
|
||||
// Wait for the request to complete
|
||||
try {
|
||||
latch.await();
|
||||
} catch (InterruptedException ie) {
|
||||
throw new RuntimeException(ie);
|
||||
}
|
||||
|
||||
String authHeader = XWwwAuthenticateHeaderListener.getAuthHeader();
|
||||
if (authHeader == null) {
|
||||
throw new IOException("No authentication header found in login response");
|
||||
}
|
||||
LOGGER.debug("Parsing authentication challenge");
|
||||
|
||||
// Extract parameters from the header
|
||||
Map<String, String> params = new HashMap<>();
|
||||
String[] parts = authHeader.split(" ", 2)[1].split(",");
|
||||
for (String part : parts) {
|
||||
part = part.trim();
|
||||
String[] keyValue = part.split("=");
|
||||
if (keyValue.length == 2) {
|
||||
String key = keyValue[0].trim();
|
||||
String value = keyValue[1].replace("\"", "").trim();
|
||||
params.put(key, value);
|
||||
}
|
||||
}
|
||||
return params;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a Digest Authentication header for the given parameters.
|
||||
*
|
||||
* @param nonce
|
||||
* @param realm
|
||||
* @param qop
|
||||
* @param uri
|
||||
* @param method
|
||||
* @param username
|
||||
* @param password
|
||||
* @param nc
|
||||
* @param cnonce
|
||||
* @return the digest authentication header
|
||||
* @throws FroniusCommunicationException if an authentication parameter is missing
|
||||
*/
|
||||
private static String createDigestHeader(@Nullable String nonce, @Nullable String realm, @Nullable String qop,
|
||||
String uri, HttpMethod method, String username, String password, int nc, String cnonce)
|
||||
throws FroniusCommunicationException {
|
||||
if (nonce == null || realm == null || qop == null) {
|
||||
throw new FroniusCommunicationException("Missing authentication parameter");
|
||||
}
|
||||
LOGGER.debug("Creating digest authentication header");
|
||||
String ha1 = md5Hex(username + ":" + realm + ":" + password);
|
||||
String ha2 = md5Hex(method.asString() + ":" + uri);
|
||||
String response = md5Hex(
|
||||
ha1 + ":" + nonce + ":" + String.format("%08x", nc) + ":" + cnonce + ":" + qop + ":" + ha2);
|
||||
|
||||
return String.format(DIGEST_AUTH_HEADER_FORMAT, username, realm, nonce, uri, response, qop, nc, cnonce);
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes the MD5 has of the given data and returns it as a hex string.
|
||||
*
|
||||
* @param data the data to hash
|
||||
* @return the hashed data as a hex string
|
||||
*/
|
||||
private static String md5Hex(String data) {
|
||||
MessageDigest md;
|
||||
try {
|
||||
md = MessageDigest.getInstance("MD5");
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
// should never occur
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
byte[] array = md.digest(data.getBytes());
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (byte b : array) {
|
||||
sb.append(String.format("%02x", b));
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs the login request to the Fronius inverter's settings.
|
||||
*
|
||||
* @param httpClient the {@link HttpClient} to use for the request
|
||||
* @param loginUri the {@link URI} of the login endpoint
|
||||
* @param authHeader the authentication header to use for the login request
|
||||
* @throws InterruptedException when the request is interrupted
|
||||
* @throws FroniusCommunicationException when the login request failed
|
||||
*/
|
||||
private static void performLoginRequest(HttpClient httpClient, URI loginUri, String authHeader, int timeout)
|
||||
throws InterruptedException, FroniusCommunicationException {
|
||||
Request loginRequest = httpClient.newRequest(loginUri).header(HttpHeader.AUTHORIZATION, authHeader)
|
||||
.timeout(timeout, TimeUnit.MILLISECONDS);
|
||||
ContentResponse loginResponse;
|
||||
try {
|
||||
loginResponse = loginRequest.send();
|
||||
if (loginResponse.getStatus() != 200) {
|
||||
throw new FroniusCommunicationException(
|
||||
"Failed to send login request, status code: " + loginResponse.getStatus());
|
||||
}
|
||||
} catch (TimeoutException | ExecutionException e) {
|
||||
throw new FroniusCommunicationException("Failed to send login request", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs in to the Fronius inverter settings, retries on failure and returns the authentication header for the next
|
||||
* request.
|
||||
*
|
||||
* @param httpClient the {@link HttpClient} to use for the request
|
||||
* @param baseUri the base URI of the Fronius inverter
|
||||
* @param username the username to use for the login
|
||||
* @param password the password to use for the login
|
||||
* @param method the {@link HttpMethod} to be used by the next request
|
||||
* @param relativeUrl the relative URL to be accessed with the next request
|
||||
* @param timeout the timeout in milliseconds for the login requests
|
||||
* @return the authentication header for the next request
|
||||
* @throws FroniusCommunicationException when the login failed or interrupted
|
||||
*/
|
||||
public static synchronized String login(HttpClient httpClient, URI baseUri, String username, String password,
|
||||
HttpMethod method, String relativeUrl, int timeout) throws FroniusCommunicationException {
|
||||
// Perform request to get authentication parameters
|
||||
LOGGER.debug("Getting authentication parameters");
|
||||
URI loginUri = baseUri.resolve(URI.create(LOGIN_ENDPOINT + "?user=" + username));
|
||||
String relativeLoginUrl = LOGIN_ENDPOINT + "?user=" + username;
|
||||
Map<String, String> authDetails;
|
||||
|
||||
int attemptCount = 1;
|
||||
try {
|
||||
while (true) {
|
||||
Throwable lastException;
|
||||
try {
|
||||
authDetails = getAuthParams(httpClient, loginUri, timeout);
|
||||
break;
|
||||
} catch (IOException e) {
|
||||
LOGGER.debug("HTTP error on attempt #{} {}", attemptCount, loginUri);
|
||||
Thread.sleep(500 * attemptCount);
|
||||
attemptCount++;
|
||||
lastException = e;
|
||||
}
|
||||
|
||||
if (attemptCount >= 3) {
|
||||
LOGGER.debug("Failed connecting to {} after {} attempts.", loginUri, attemptCount, lastException);
|
||||
throw new FroniusCommunicationException("Unable to connect", lastException);
|
||||
}
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
throw new FroniusCommunicationException("Interrupted", e);
|
||||
}
|
||||
|
||||
// Create auth header for login request
|
||||
int nc = 1;
|
||||
String cnonce = md5Hex(String.valueOf(System.currentTimeMillis()));
|
||||
String authHeader = createDigestHeader(authDetails.get("nonce"), authDetails.get("realm"),
|
||||
authDetails.get("qop"), relativeLoginUrl, HttpMethod.GET, username, password, nc, cnonce);
|
||||
|
||||
// Perform login request with Digest Authentication
|
||||
LOGGER.debug("Sending login request");
|
||||
attemptCount = 1;
|
||||
try {
|
||||
while (true) {
|
||||
Throwable lastException;
|
||||
try {
|
||||
performLoginRequest(httpClient, loginUri, authHeader, timeout);
|
||||
break;
|
||||
} catch (InterruptedException ie) {
|
||||
throw new FroniusCommunicationException("Failed to send login request", ie);
|
||||
} catch (FroniusCommunicationException e) {
|
||||
LOGGER.debug("HTTP error on attempt #{} {}", attemptCount, loginUri);
|
||||
Thread.sleep(500 * attemptCount);
|
||||
attemptCount++;
|
||||
lastException = e;
|
||||
}
|
||||
|
||||
if (attemptCount >= 3) {
|
||||
LOGGER.debug("Failed connecting to {} after {} attempts.", loginUri, attemptCount, lastException);
|
||||
throw new FroniusCommunicationException("Unable to connect", lastException);
|
||||
}
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
throw new FroniusCommunicationException("Interrupted", e);
|
||||
}
|
||||
|
||||
// Create new auth header for next request
|
||||
LOGGER.debug("Login successful, creating auth header for next request");
|
||||
nc++;
|
||||
authHeader = createDigestHeader(authDetails.get("nonce"), authDetails.get("realm"), authDetails.get("qop"),
|
||||
relativeUrl, method, username, password, nc, cnonce);
|
||||
|
||||
return authHeader;
|
||||
}
|
||||
|
||||
/**
|
||||
* Listener to extract the X-Www-Authenticate header from the response of a {@link Request}.
|
||||
*/
|
||||
private static class XWwwAuthenticateHeaderListener extends Response.Listener.Adapter {
|
||||
private final CountDownLatch latch;
|
||||
private @Nullable String authHeader;
|
||||
|
||||
public XWwwAuthenticateHeaderListener(CountDownLatch latch) {
|
||||
this.latch = latch;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onHeaders(Response response) {
|
||||
authHeader = response.getHeaders().get(AUTHENTICATE_HEADER);
|
||||
latch.countDown();
|
||||
}
|
||||
|
||||
public @Nullable String getAuthHeader() {
|
||||
return authHeader;
|
||||
}
|
||||
}
|
||||
}
|
@ -13,19 +13,20 @@
|
||||
package org.openhab.binding.fronius.internal.api;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.Properties;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.eclipse.jetty.http.HttpMethod;
|
||||
import org.openhab.core.io.net.http.HttpUtil;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
*
|
||||
* A version of HttpUtil implementation that retries on failure.
|
||||
*
|
||||
* @author Jimmy Tanagra - Initial contribution
|
||||
*
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class FroniusHttpUtil {
|
||||
@ -42,13 +43,33 @@ public class FroniusHttpUtil {
|
||||
*/
|
||||
public static synchronized String executeUrl(HttpMethod httpMethod, String url, int timeout)
|
||||
throws FroniusCommunicationException {
|
||||
return executeUrl(httpMethod, url, null, null, null, timeout);
|
||||
}
|
||||
|
||||
/**
|
||||
* Issue a HTTP request and retry on failure.
|
||||
*
|
||||
* @param httpMethod the {@link HttpMethod} to use
|
||||
* @param url the url to execute
|
||||
* @param httpHeaders optional http request headers which has to be sent within request
|
||||
* @param content the content to be sent to the given <code>url</code> or <code>null</code> if no content should be
|
||||
* sent.
|
||||
* @param contentType the content type of the given <code>content</code>
|
||||
* @param timeout the socket timeout in milliseconds to wait for data
|
||||
* @return the response body
|
||||
* @throws FroniusCommunicationException when the request execution failed or interrupted
|
||||
*/
|
||||
public static synchronized String executeUrl(HttpMethod httpMethod, String url, @Nullable Properties httpHeaders,
|
||||
@Nullable InputStream content, @Nullable String contentType, int timeout)
|
||||
throws FroniusCommunicationException {
|
||||
int attemptCount = 1;
|
||||
try {
|
||||
while (true) {
|
||||
Throwable lastException = null;
|
||||
String result = null;
|
||||
try {
|
||||
result = HttpUtil.executeUrl(httpMethod.asString(), url, timeout);
|
||||
result = HttpUtil.executeUrl(httpMethod.asString(), url, httpHeaders, content, contentType,
|
||||
timeout);
|
||||
} catch (IOException e) {
|
||||
// HttpUtil::executeUrl wraps InterruptedException into IOException.
|
||||
// Unwrap and rethrow it so that we don't retry on InterruptedException
|
||||
|
@ -0,0 +1,25 @@
|
||||
/**
|
||||
* 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.fronius.internal.api.dto.inverter.batterycontrol;
|
||||
|
||||
/**
|
||||
* Enum for the schedule type of the battery control.
|
||||
*
|
||||
* @author Florian Hotze - Initial contribution
|
||||
*/
|
||||
public enum ScheduleType {
|
||||
CHARGE_MIN,
|
||||
CHARGE_MAX,
|
||||
DISCHARGE_MIN,
|
||||
DISCHARGE_MAX
|
||||
}
|
@ -0,0 +1,28 @@
|
||||
/**
|
||||
* 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.fronius.internal.api.dto.inverter.batterycontrol;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
|
||||
/**
|
||||
* Record representing an entry of {@link TimeOfUseRecords}.
|
||||
*
|
||||
* @author Florian Hotze - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public record TimeOfUseRecord(@SerializedName("Active") boolean active, @SerializedName("Power") int power,
|
||||
@SerializedName("ScheduleType") ScheduleType scheduleType,
|
||||
@SerializedName("TimeTable") TimeTableRecord timeTable, @SerializedName("Weekdays") WeekdaysRecord weekdays) {
|
||||
}
|
@ -0,0 +1,27 @@
|
||||
/**
|
||||
* 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.fronius.internal.api.dto.inverter.batterycontrol;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
|
||||
/**
|
||||
* Record representing the data received from or sent to the <code>/config/timeofuse</code> HTTP endpoint of Fronius
|
||||
* hybrid inverters.
|
||||
*
|
||||
* @author Florian Hotze - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public record TimeOfUseRecords(@SerializedName("timeofuse") TimeOfUseRecord[] records) {
|
||||
}
|
@ -0,0 +1,26 @@
|
||||
/**
|
||||
* 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.fronius.internal.api.dto.inverter.batterycontrol;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
|
||||
/**
|
||||
* Record representing the "TimeTable" node of a {@link TimeOfUseRecord}.
|
||||
*
|
||||
* @author Florian Hotze - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public record TimeTableRecord(@SerializedName("Start") String start, @SerializedName("End") String end) {
|
||||
}
|
@ -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.fronius.internal.api.dto.inverter.batterycontrol;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
|
||||
/**
|
||||
* Record representing the "Weekdays" node of {@link TimeOfUseRecord}.
|
||||
*
|
||||
* @author Florian Hotze - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public record WeekdaysRecord(@SerializedName("Mon") boolean monday, @SerializedName("Tue") boolean tuesday,
|
||||
@SerializedName("Wed") boolean wednesday, @SerializedName("Thu") boolean thursday,
|
||||
@SerializedName("Fri") boolean friday, @SerializedName("Sat") boolean saturday,
|
||||
@SerializedName("Sun") boolean sunday) {
|
||||
}
|
@ -12,14 +12,22 @@
|
||||
*/
|
||||
package org.openhab.binding.fronius.internal.handler;
|
||||
|
||||
import java.net.URI;
|
||||
import java.time.LocalTime;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
import javax.measure.Unit;
|
||||
import javax.measure.quantity.Power;
|
||||
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.eclipse.jetty.client.HttpClient;
|
||||
import org.openhab.binding.fronius.internal.FroniusBaseDeviceConfiguration;
|
||||
import org.openhab.binding.fronius.internal.FroniusBindingConstants;
|
||||
import org.openhab.binding.fronius.internal.FroniusBridgeConfiguration;
|
||||
import org.openhab.binding.fronius.internal.action.FroniusSymoInverterActions;
|
||||
import org.openhab.binding.fronius.internal.api.FroniusBatteryControl;
|
||||
import org.openhab.binding.fronius.internal.api.FroniusCommunicationException;
|
||||
import org.openhab.binding.fronius.internal.api.dto.ValueUnit;
|
||||
import org.openhab.binding.fronius.internal.api.dto.inverter.InverterDeviceStatus;
|
||||
@ -35,7 +43,10 @@ import org.openhab.core.library.types.DecimalType;
|
||||
import org.openhab.core.library.types.QuantityType;
|
||||
import org.openhab.core.library.unit.Units;
|
||||
import org.openhab.core.thing.Thing;
|
||||
import org.openhab.core.thing.binding.ThingHandlerService;
|
||||
import org.openhab.core.types.State;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* The {@link FroniusSymoInverterHandler} is responsible for updating the data, which are
|
||||
@ -45,15 +56,21 @@ import org.openhab.core.types.State;
|
||||
* @author Peter Schraffl - Added device status and error status channels
|
||||
* @author Thomas Kordelle - Added inverter power, battery state of charge and PV solar yield
|
||||
* @author Jimmy Tanagra - Add powerflow autonomy, self consumption channels
|
||||
* @author Florian Hotze - Add battery control actions
|
||||
*/
|
||||
public class FroniusSymoInverterHandler extends FroniusBaseThingHandler {
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(FroniusSymoInverterHandler.class);
|
||||
private final HttpClient httpClient;
|
||||
|
||||
private @Nullable InverterRealtimeResponse inverterRealtimeResponse;
|
||||
private @Nullable PowerFlowRealtimeResponse powerFlowResponse;
|
||||
private FroniusBaseDeviceConfiguration config;
|
||||
private @Nullable FroniusBatteryControl batteryControl;
|
||||
|
||||
public FroniusSymoInverterHandler(Thing thing) {
|
||||
public FroniusSymoInverterHandler(Thing thing, HttpClient httpClient) {
|
||||
super(thing);
|
||||
this.httpClient = httpClient;
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -70,9 +87,81 @@ public class FroniusSymoInverterHandler extends FroniusBaseThingHandler {
|
||||
@Override
|
||||
public void initialize() {
|
||||
config = getConfigAs(FroniusBaseDeviceConfiguration.class);
|
||||
FroniusBridgeConfiguration bridgeConfig = getBridge().getConfiguration().as(FroniusBridgeConfiguration.class);
|
||||
if (bridgeConfig.username != null && bridgeConfig.password != null) {
|
||||
batteryControl = new FroniusBatteryControl(httpClient, URI.create("http://" + bridgeConfig.hostname + "/"),
|
||||
bridgeConfig.username, bridgeConfig.password);
|
||||
}
|
||||
super.initialize();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<Class<? extends ThingHandlerService>> getServices() {
|
||||
return List.of(FroniusSymoInverterActions.class);
|
||||
}
|
||||
|
||||
private @Nullable FroniusBatteryControl getBatteryControl() {
|
||||
if (batteryControl == null) {
|
||||
logger.warn("Battery control is not available. Check the bridge configuration.");
|
||||
}
|
||||
return batteryControl;
|
||||
}
|
||||
|
||||
public void resetBatteryControl() {
|
||||
FroniusBatteryControl batteryControl = getBatteryControl();
|
||||
if (batteryControl != null) {
|
||||
try {
|
||||
batteryControl.reset();
|
||||
} catch (FroniusCommunicationException e) {
|
||||
logger.warn("Failed to reset battery control", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void holdBatteryCharge() {
|
||||
FroniusBatteryControl batteryControl = getBatteryControl();
|
||||
if (batteryControl != null) {
|
||||
try {
|
||||
batteryControl.holdBatteryCharge();
|
||||
} catch (FroniusCommunicationException e) {
|
||||
logger.warn("Failed to set battery control to hold battery charge", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void addHoldBatteryChargeSchedule(LocalTime from, LocalTime until) {
|
||||
FroniusBatteryControl batteryControl = getBatteryControl();
|
||||
if (batteryControl != null) {
|
||||
try {
|
||||
batteryControl.addHoldBatteryChargeSchedule(from, until);
|
||||
} catch (FroniusCommunicationException e) {
|
||||
logger.warn("Failed to add hold battery charge schedule to battery control", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void forceBatteryCharging(QuantityType<Power> power) {
|
||||
FroniusBatteryControl batteryControl = getBatteryControl();
|
||||
if (batteryControl != null) {
|
||||
try {
|
||||
batteryControl.forceBatteryCharging(power);
|
||||
} catch (FroniusCommunicationException e) {
|
||||
logger.warn("Failed to set battery control to force battery charge", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void addForcedBatteryChargingSchedule(LocalTime from, LocalTime until, QuantityType<Power> power) {
|
||||
FroniusBatteryControl batteryControl = getBatteryControl();
|
||||
if (batteryControl != null) {
|
||||
try {
|
||||
batteryControl.addForcedBatteryChargingSchedule(from, until, power);
|
||||
} catch (FroniusCommunicationException e) {
|
||||
logger.warn("Failed to add forced battery charge schedule to battery control", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the channel from the last data retrieved
|
||||
*
|
||||
@ -92,6 +181,7 @@ public class FroniusSymoInverterHandler extends FroniusBaseThingHandler {
|
||||
return null;
|
||||
}
|
||||
|
||||
InverterDeviceStatus deviceStatus;
|
||||
switch (fieldName) {
|
||||
case FroniusBindingConstants.INVERTER_DATA_CHANNEL_PAC:
|
||||
return getQuantityOrZero(inverterData.getPac(), Units.WATT);
|
||||
@ -129,7 +219,7 @@ public class FroniusSymoInverterHandler extends FroniusBaseThingHandler {
|
||||
// Convert the unit to MWh for backwards compatibility with non-quantity type
|
||||
return getQuantityOrZero(inverterData.getYearEnergy(), Units.MEGAWATT_HOUR).toUnit("MWh");
|
||||
case FroniusBindingConstants.INVERTER_DATA_CHANNEL_DEVICE_STATUS_ERROR_CODE:
|
||||
InverterDeviceStatus deviceStatus = inverterData.getDeviceStatus();
|
||||
deviceStatus = inverterData.getDeviceStatus();
|
||||
if (deviceStatus == null) {
|
||||
return null;
|
||||
}
|
||||
|
@ -33,8 +33,12 @@ thing-type.fronius.powerinverter.description = Fronius Symo power inverter
|
||||
|
||||
thing-type.config.fronius.bridge.hostname.label = Hostname
|
||||
thing-type.config.fronius.bridge.hostname.description = The hostname or IP address of the Fronius gateway/device
|
||||
thing-type.config.fronius.bridge.password.label = Password
|
||||
thing-type.config.fronius.bridge.password.description = The password to access the configuration of the Fronius gateway/device, required only for battery control
|
||||
thing-type.config.fronius.bridge.refreshInterval.label = Refresh Interval
|
||||
thing-type.config.fronius.bridge.refreshInterval.description = Specifies the refresh interval in seconds.
|
||||
thing-type.config.fronius.bridge.username.label = Username
|
||||
thing-type.config.fronius.bridge.username.description = The username to access the configuration of the Fronius gateway/device, required only for battery control
|
||||
thing-type.config.fronius.meter.deviceId.label = Device ID
|
||||
thing-type.config.fronius.meter.deviceId.description = Specific device identifier
|
||||
thing-type.config.fronius.ohmpilot.deviceId.label = Device ID
|
||||
@ -111,3 +115,22 @@ channel-type.fronius.udc3.label = DC Voltage 3
|
||||
channel-type.fronius.udc3.description = DC voltage of MPPT tracker 3
|
||||
channel-type.fronius.year_energy.label = Year Energy
|
||||
channel-type.fronius.year_energy.description = Energy generated in current year
|
||||
|
||||
# actions
|
||||
|
||||
actions.reset-battery-control.label = Reset Battery Control
|
||||
actions.reset-battery-control.description = Remove all battery control schedules from the inverter
|
||||
actions.hold-battery-charge.label = Hold Battery Charge
|
||||
actions.hold-battery-charge.description = Prevent the battery from discharging
|
||||
actions.add-hold-battery-charge-schedule.label = Add Hold Battery Charge Schedule
|
||||
actions.add-hold-battery-charge-schedule.description = Add a schedule to prevent the battery from discharging in the specified time range
|
||||
actions.force-battery-charging.label = Force Battery Charging
|
||||
actions.force-battery-charging.description = Force the battery to charge with the specified power
|
||||
actions.add-forced-battery-charging-schedule.label = Add Forced Battery Charging Schedule
|
||||
actions.add-forced-battery-charging-schedule.description = Add a schedule to force the battery to charge with the specified power in the specified time range
|
||||
actions.from.label = Begin Timestamp
|
||||
actions.from.description = The beginning of the time range
|
||||
actions.until.label = End Timestamp
|
||||
actions.until.description = The (inclusive) end of the time range
|
||||
actions.power.label = Power
|
||||
actions.power.description = The power to charge the battery with
|
||||
|
@ -13,6 +13,18 @@
|
||||
<label>Hostname</label>
|
||||
<description>The hostname or IP address of the Fronius gateway/device</description>
|
||||
</parameter>
|
||||
<parameter name="username" type="text" required="false">
|
||||
<label>Username</label>
|
||||
<description>The username to access the configuration of the Fronius gateway/device, required only for battery
|
||||
control</description>
|
||||
<default>customer</default>
|
||||
</parameter>
|
||||
<parameter name="password" type="text" required="false">
|
||||
<context>password</context>
|
||||
<label>Password</label>
|
||||
<description>The password to access the configuration of the Fronius gateway/device, required only for battery
|
||||
control</description>
|
||||
</parameter>
|
||||
<parameter name="refreshInterval" type="integer" min="1">
|
||||
<label>Refresh Interval</label>
|
||||
<description>Specifies the refresh interval in seconds.</description>
|
||||
|
Loading…
Reference in New Issue
Block a user