diff --git a/bundles/org.openhab.binding.fronius/README.md b/bundles/org.openhab.binding.fronius/README.md index 678657c37bb9c..b7e314db991f9 100644 --- a/bundles/org.openhab.binding.fronius/README.md +++ b/bundles/org.openhab.binding.fronius/README.md @@ -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)`: 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)`: 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)`: 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: diff --git a/bundles/org.openhab.binding.fronius/src/main/java/org/openhab/binding/fronius/internal/FroniusBridgeConfiguration.java b/bundles/org.openhab.binding.fronius/src/main/java/org/openhab/binding/fronius/internal/FroniusBridgeConfiguration.java index 139dc912d5d5d..a056406f9c035 100644 --- a/bundles/org.openhab.binding.fronius/src/main/java/org/openhab/binding/fronius/internal/FroniusBridgeConfiguration.java +++ b/bundles/org.openhab.binding.fronius/src/main/java/org/openhab/binding/fronius/internal/FroniusBridgeConfiguration.java @@ -20,5 +20,7 @@ */ public class FroniusBridgeConfiguration { public String hostname; + public String username; + public String password; public Integer refreshInterval; } diff --git a/bundles/org.openhab.binding.fronius/src/main/java/org/openhab/binding/fronius/internal/FroniusHandlerFactory.java b/bundles/org.openhab.binding.fronius/src/main/java/org/openhab/binding/fronius/internal/FroniusHandlerFactory.java index 5c621d6043949..4eb2304d9fc06 100644 --- a/bundles/org.openhab.binding.fronius/src/main/java/org/openhab/binding/fronius/internal/FroniusHandlerFactory.java +++ b/bundles/org.openhab.binding.fronius/src/main/java/org/openhab/binding/fronius/internal/FroniusHandlerFactory.java @@ -23,13 +23,16 @@ 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 boolean supportsThingType(ThingTypeUID thingTypeUID) { 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)) { diff --git a/bundles/org.openhab.binding.fronius/src/main/java/org/openhab/binding/fronius/internal/action/FroniusSymoInverterActions.java b/bundles/org.openhab.binding.fronius/src/main/java/org/openhab/binding/fronius/internal/action/FroniusSymoInverterActions.java new file mode 100644 index 0000000000000..fb2e67ecb847d --- /dev/null +++ b/bundles/org.openhab.binding.fronius/src/main/java/org/openhab/binding/fronius/internal/action/FroniusSymoInverterActions.java @@ -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) { + 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) { + 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) { + 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) { + 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) { + FroniusSymoInverterHandler handler = this.handler; + if (handler != null) { + handler.addForcedBatteryChargingSchedule(from, until, power); + } + } + + public void addForcedBatteryChargingSchedule(ZonedDateTime from, ZonedDateTime until, QuantityType power) { + addForcedBatteryChargingSchedule(from.toLocalTime(), until.toLocalTime(), power); + } +} diff --git a/bundles/org.openhab.binding.fronius/src/main/java/org/openhab/binding/fronius/internal/api/FroniusBatteryControl.java b/bundles/org.openhab.binding.fronius/src/main/java/org/openhab/binding/fronius/internal/api/FroniusBatteryControl.java new file mode 100644 index 0000000000000..3a56320dcb9db --- /dev/null +++ b/bundles/org.openhab.binding.fronius/src/main/java/org/openhab/binding/fronius/internal/api/FroniusBatteryControl.java @@ -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) 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) + 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)); + } +} diff --git a/bundles/org.openhab.binding.fronius/src/main/java/org/openhab/binding/fronius/internal/api/FroniusConfigAuthUtil.java b/bundles/org.openhab.binding.fronius/src/main/java/org/openhab/binding/fronius/internal/api/FroniusConfigAuthUtil.java new file mode 100644 index 0000000000000..b7e151d66b9d0 --- /dev/null +++ b/bundles/org.openhab.binding.fronius/src/main/java/org/openhab/binding/fronius/internal/api/FroniusConfigAuthUtil.java @@ -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 /config HTTP endpoints. + *
+ * 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 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 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 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; + } + } +} diff --git a/bundles/org.openhab.binding.fronius/src/main/java/org/openhab/binding/fronius/internal/api/FroniusHttpUtil.java b/bundles/org.openhab.binding.fronius/src/main/java/org/openhab/binding/fronius/internal/api/FroniusHttpUtil.java index ccc26119a4b9d..d68245b19cc48 100644 --- a/bundles/org.openhab.binding.fronius/src/main/java/org/openhab/binding/fronius/internal/api/FroniusHttpUtil.java +++ b/bundles/org.openhab.binding.fronius/src/main/java/org/openhab/binding/fronius/internal/api/FroniusHttpUtil.java @@ -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 url or null if no content should be + * sent. + * @param contentType the content type of the given content + * @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 diff --git a/bundles/org.openhab.binding.fronius/src/main/java/org/openhab/binding/fronius/internal/api/dto/inverter/batterycontrol/ScheduleType.java b/bundles/org.openhab.binding.fronius/src/main/java/org/openhab/binding/fronius/internal/api/dto/inverter/batterycontrol/ScheduleType.java new file mode 100644 index 0000000000000..9644ec2555476 --- /dev/null +++ b/bundles/org.openhab.binding.fronius/src/main/java/org/openhab/binding/fronius/internal/api/dto/inverter/batterycontrol/ScheduleType.java @@ -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 +} diff --git a/bundles/org.openhab.binding.fronius/src/main/java/org/openhab/binding/fronius/internal/api/dto/inverter/batterycontrol/TimeOfUseRecord.java b/bundles/org.openhab.binding.fronius/src/main/java/org/openhab/binding/fronius/internal/api/dto/inverter/batterycontrol/TimeOfUseRecord.java new file mode 100644 index 0000000000000..26b537ac2066a --- /dev/null +++ b/bundles/org.openhab.binding.fronius/src/main/java/org/openhab/binding/fronius/internal/api/dto/inverter/batterycontrol/TimeOfUseRecord.java @@ -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) { +} diff --git a/bundles/org.openhab.binding.fronius/src/main/java/org/openhab/binding/fronius/internal/api/dto/inverter/batterycontrol/TimeOfUseRecords.java b/bundles/org.openhab.binding.fronius/src/main/java/org/openhab/binding/fronius/internal/api/dto/inverter/batterycontrol/TimeOfUseRecords.java new file mode 100644 index 0000000000000..e88086c23ae2b --- /dev/null +++ b/bundles/org.openhab.binding.fronius/src/main/java/org/openhab/binding/fronius/internal/api/dto/inverter/batterycontrol/TimeOfUseRecords.java @@ -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 /config/timeofuse HTTP endpoint of Fronius + * hybrid inverters. + * + * @author Florian Hotze - Initial contribution + */ +@NonNullByDefault +public record TimeOfUseRecords(@SerializedName("timeofuse") TimeOfUseRecord[] records) { +} diff --git a/bundles/org.openhab.binding.fronius/src/main/java/org/openhab/binding/fronius/internal/api/dto/inverter/batterycontrol/TimeTableRecord.java b/bundles/org.openhab.binding.fronius/src/main/java/org/openhab/binding/fronius/internal/api/dto/inverter/batterycontrol/TimeTableRecord.java new file mode 100644 index 0000000000000..6045ef3657481 --- /dev/null +++ b/bundles/org.openhab.binding.fronius/src/main/java/org/openhab/binding/fronius/internal/api/dto/inverter/batterycontrol/TimeTableRecord.java @@ -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) { +} diff --git a/bundles/org.openhab.binding.fronius/src/main/java/org/openhab/binding/fronius/internal/api/dto/inverter/batterycontrol/WeekdaysRecord.java b/bundles/org.openhab.binding.fronius/src/main/java/org/openhab/binding/fronius/internal/api/dto/inverter/batterycontrol/WeekdaysRecord.java new file mode 100644 index 0000000000000..8a5edee3c6779 --- /dev/null +++ b/bundles/org.openhab.binding.fronius/src/main/java/org/openhab/binding/fronius/internal/api/dto/inverter/batterycontrol/WeekdaysRecord.java @@ -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) { +} diff --git a/bundles/org.openhab.binding.fronius/src/main/java/org/openhab/binding/fronius/internal/handler/FroniusSymoInverterHandler.java b/bundles/org.openhab.binding.fronius/src/main/java/org/openhab/binding/fronius/internal/handler/FroniusSymoInverterHandler.java index 803009756c931..1c80ded6e1563 100644 --- a/bundles/org.openhab.binding.fronius/src/main/java/org/openhab/binding/fronius/internal/handler/FroniusSymoInverterHandler.java +++ b/bundles/org.openhab.binding.fronius/src/main/java/org/openhab/binding/fronius/internal/handler/FroniusSymoInverterHandler.java @@ -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.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 @@ * @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 @@ protected void handleRefresh(FroniusBridgeConfiguration bridgeConfiguration) thr @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> 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) { + 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) { + 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 @@ protected State getValue(String channelId) { return null; } + InverterDeviceStatus deviceStatus; switch (fieldName) { case FroniusBindingConstants.INVERTER_DATA_CHANNEL_PAC: return getQuantityOrZero(inverterData.getPac(), Units.WATT); @@ -129,7 +219,7 @@ protected State getValue(String channelId) { // 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; } diff --git a/bundles/org.openhab.binding.fronius/src/main/resources/OH-INF/i18n/fronius.properties b/bundles/org.openhab.binding.fronius/src/main/resources/OH-INF/i18n/fronius.properties index a35ced3a73813..a5c1d08c80839 100644 --- a/bundles/org.openhab.binding.fronius/src/main/resources/OH-INF/i18n/fronius.properties +++ b/bundles/org.openhab.binding.fronius/src/main/resources/OH-INF/i18n/fronius.properties @@ -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 diff --git a/bundles/org.openhab.binding.fronius/src/main/resources/OH-INF/thing/bridge.xml b/bundles/org.openhab.binding.fronius/src/main/resources/OH-INF/thing/bridge.xml index 462889377f57a..143dd95d4ba39 100644 --- a/bundles/org.openhab.binding.fronius/src/main/resources/OH-INF/thing/bridge.xml +++ b/bundles/org.openhab.binding.fronius/src/main/resources/OH-INF/thing/bridge.xml @@ -13,6 +13,18 @@ The hostname or IP address of the Fronius gateway/device + + + The username to access the configuration of the Fronius gateway/device, required only for battery + control + customer + + + password + + The password to access the configuration of the Fronius gateway/device, required only for battery + control + Specifies the refresh interval in seconds.