Skip to content

Commit

Permalink
[growatt] Enhance support for SPF inverters (openhab#17795)
Browse files Browse the repository at this point in the history
* [growatt] tweak channel aliases; add missing channels

Signed-off-by: AndrewFG <[email protected]>
  • Loading branch information
andrewfg authored Nov 26, 2024
1 parent 3ae0203 commit 81e488d
Show file tree
Hide file tree
Showing 12 changed files with 390 additions and 115 deletions.
189 changes: 99 additions & 90 deletions bundles/org.openhab.binding.growatt/README.md

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,6 @@ public class GrowattBindingConstants {

public static final ThingTypeUID THING_TYPE_BRIDGE = new ThingTypeUID(BINDING_ID, "bridge");
public static final ThingTypeUID THING_TYPE_INVERTER = new ThingTypeUID(BINDING_ID, "inverter");

public static final String CHANNEL_INVERTER_CLOCK_OFFSET = "inverter-clock-offset";
}
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,27 @@ public UoM(Unit<?> units, float divisor) {
// reactive 'power' resp. 'energy'
new AbstractMap.SimpleEntry<String, UoM>("rac", new UoM(Units.VAR, 10)),
new AbstractMap.SimpleEntry<String, UoM>("erac-today", new UoM(Units.KILOVAR_HOUR, 10)),
new AbstractMap.SimpleEntry<String, UoM>("erac-total", new UoM(Units.KILOVAR_HOUR, 10))
new AbstractMap.SimpleEntry<String, UoM>("erac-total", new UoM(Units.KILOVAR_HOUR, 10)),

/*
* ============== CHANNELS ADDED IN PR #17795 ==============
*/

// battery instantaneous measurements
new AbstractMap.SimpleEntry<String, UoM>("battery-voltage2", new UoM(Units.VOLT, 100)),
new AbstractMap.SimpleEntry<String, UoM>("charge-va", new UoM(Units.VOLT_AMPERE, 10)),
new AbstractMap.SimpleEntry<String, UoM>("battery-discharge-va", new UoM(Units.VOLT_AMPERE, 10)),
new AbstractMap.SimpleEntry<String, UoM>("battery-discharge-watt", new UoM(Units.WATT, 10)),

// battery energy
new AbstractMap.SimpleEntry<String, UoM>("battery-discharge-energy-today",
new UoM(Units.KILOWATT_HOUR, 10)),
new AbstractMap.SimpleEntry<String, UoM>("battery-discharge-energy-total",
new UoM(Units.KILOWATT_HOUR, 10)),

// inverter
new AbstractMap.SimpleEntry<String, UoM>("inverter-current", new UoM(Units.AMPERE, 10)),
new AbstractMap.SimpleEntry<String, UoM>("inverter-fan-speed", new UoM(Units.PERCENT, 1))
//
);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@
package org.openhab.binding.growatt.internal.dto;

import java.lang.reflect.Type;
import java.time.DateTimeException;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.util.ArrayList;

import org.eclipse.jdt.annotation.NonNullByDefault;
Expand All @@ -34,6 +39,7 @@ public class GrottDevice {
// @formatter:on

private @Nullable @SerializedName("device") String deviceId;
private @Nullable @SerializedName("time") String timeStamp;
private @Nullable GrottValues values;

public String getDeviceId() {
Expand All @@ -44,4 +50,24 @@ public String getDeviceId() {
public @Nullable GrottValues getValues() {
return values;
}

/**
* Return the time stamp of the data DTO sent by the inverter data-logger.
* <p>
* Note: the inverter provides a time stamp formatted as a {@link LocalDateTime} without any time zone information,
* so we convert it to an {@link Instant} based on the OH system time zone. i.e. we are forced to assume the
* inverter and the OH PC are both physically in the same time zone.
*
* @return the time stamp {@link Instant}
*/
public @Nullable Instant getTimeStamp() {
String timeStamp = this.timeStamp;
if (timeStamp != null) {
try {
return ZonedDateTime.of(LocalDateTime.parse(timeStamp), ZoneId.systemDefault()).toInstant();
} catch (DateTimeException e) {
}
}
return null;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -67,24 +67,24 @@ public static String getFieldName(String channelId) {
public @Nullable @SerializedName(value = "Vac_TR", alternate = { "vactr", "L3-1_voltage" }) Integer grid_voltage_tr;

// solar AC mains power
public @Nullable @SerializedName(value = "pvgridcurrent", alternate = { "OP_Curr", "Inv_Curr", "Current_l1" }) Integer inverter_current_r;
public @Nullable @SerializedName(value = "pvgridcurrent", alternate = { "OP_Curr", "Current_l1" }) Integer inverter_current_r;
public @Nullable @SerializedName(value = "pvgridcurrent2", alternate = { "Current_l2" }) Integer inverter_current_s;
public @Nullable @SerializedName(value = "pvgridcurrent3", alternate = { "Current_l3" }) Integer inverter_current_t;

public @Nullable @SerializedName(value = "pvgridpower", alternate = { "op_watt", "AC_InWatt" }) Integer inverter_power_r;
public @Nullable @SerializedName(value = "pvgridpower", alternate = { "op_watt" }) Integer inverter_power_r;
public @Nullable @SerializedName(value = "pvgridpower2") Integer inverter_power_s;
public @Nullable @SerializedName(value = "pvgridpower3") Integer inverter_power_t;

// apparent power VA
public @Nullable @SerializedName(value = "op_va", alternate = { "AC_InVA" }) Integer inverter_va;
public @Nullable @SerializedName(value = "op_va") Integer inverter_va;

// battery discharge / charge power
public @Nullable @SerializedName(value = "p1charge1", alternate = { "acchr_watt", "BatWatt", "bdc1_pchr" }) Integer charge_power;
public @Nullable @SerializedName(value = "pdischarge1", alternate = { "ACDischarWatt", "BatDischarWatt", "bdc1_pdischr" }) Integer discharge_power;
public @Nullable @SerializedName(value = "p1charge1", alternate = { "acchr_watt", "bdc1_pchr" }) Integer charge_power;
public @Nullable @SerializedName(value = "pdischarge1", alternate = { "ACDischarWatt", "bdc1_pdischr" }) Integer discharge_power;

// miscellaneous battery
public @Nullable @SerializedName(value = "ACCharCurr") Integer charge_current;
public @Nullable @SerializedName(value = "ACDischarVA", alternate = { "BatDischarVA", "acchar_VA" }) Integer discharge_va;
public @Nullable @SerializedName(value = "ACDischarVA") Integer discharge_va;

// power exported to utility company
public @Nullable @SerializedName(value = "pactogridtot", alternate = { "ptogridtotal" }) Integer export_power;
Expand All @@ -93,7 +93,7 @@ public static String getFieldName(String channelId) {
public @Nullable @SerializedName(value = "pactogridt") Integer export_power_t;

// power imported from utility company
public @Nullable @SerializedName(value = "pactousertot", alternate = { "ptousertotal", "pos_rev_act_power" }) Integer import_power;
public @Nullable @SerializedName(value = "pactousertot", alternate = { "ptousertotal", "AC_InWatt", "pos_rev_act_power" }) Integer import_power;
public @Nullable @SerializedName(value = "pactouserr", alternate = { "act_power_l1" }) Integer import_power_r;
public @Nullable @SerializedName(value = "pactousers", alternate = { "act_power_l2" }) Integer import_power_s;
public @Nullable @SerializedName(value = "pactousert", alternate = { "act_power_l3" }) Integer import_power_t;
Expand Down Expand Up @@ -138,8 +138,8 @@ public static String getFieldName(String channelId) {
public @Nullable @SerializedName(value = "eharge1_tot", alternate = { "echrtotal" }) Integer inverter_charge_energy_total;

// discharging energy
public @Nullable @SerializedName(value = "edischarge1_tod", alternate = { "eacDischarToday", "ebatDischarToday", "edischrtoday" }) Integer discharge_energy_today;
public @Nullable @SerializedName(value = "edischarge1_tot", alternate = { "eacDischarTotal", "ebatDischarTotal", "edischrtotal" }) Integer discharge_energy_total;
public @Nullable @SerializedName(value = "edischarge1_tod", alternate = { "eacDischarToday", "edischrtoday" }) Integer discharge_energy_today;
public @Nullable @SerializedName(value = "edischarge1_tot", alternate = { "eacDischarTotal", "edischrtotal" }) Integer discharge_energy_total;

// inverter up time
public @Nullable @SerializedName(value = "totworktime") Integer total_work_time;
Expand All @@ -159,7 +159,7 @@ public static String getFieldName(String channelId) {
// battery data
public @Nullable @SerializedName(value = "batterytype") Integer battery_type;
public @Nullable @SerializedName(value = "batttemp", alternate = { "bdc1_tempa" }) Integer battery_temperature;
public @Nullable @SerializedName(value = "vbat", alternate = { "uwBatVolt_DSP", "bat_Volt", "bms_batteryvolt" }) Integer battery_voltage;
public @Nullable @SerializedName(value = "vbat", alternate = { "uwBatVolt_DSP", "bms_batteryvolt" }) Integer battery_voltage;
public @Nullable @SerializedName(value = "bat_dsp") Integer battery_display;
public @Nullable @SerializedName(value = "SOC", alternate = { "batterySOC", "batterySoc", "bms_soc" }) Integer battery_soc;

Expand All @@ -180,9 +180,27 @@ public static String getFieldName(String channelId) {
public @Nullable @SerializedName(value = "loadpercent") Integer load_percent;

// reactive 'power' resp. 'energy'
public @Nullable @SerializedName(value = "rac", alternate = { "react_power" }) Integer rac;
public @Nullable @SerializedName(value = "rac", alternate = { "react_power", "AC_InVA" }) Integer rac;
public @Nullable @SerializedName(value = "eractoday", alternate = { "react_energy_kvar" }) Integer erac_today;
public @Nullable @SerializedName(value = "eractotal") Integer erac_total;

/*
* ============== CHANNELS ADDED IN PR #17795 ==============
*/

// battery instantaneous measurements
public @Nullable @SerializedName(value = "bat_Volt") Integer battery_voltage2;
public @Nullable @SerializedName(value = "acchr_VA") Integer charge_va;
public @Nullable @SerializedName(value = "BatDischarVA") Integer battery_discharge_va;
public @Nullable @SerializedName(value = "BatDischarWatt", alternate = { "BatWatt" }) Integer battery_discharge_watt;

// battery energy
public @Nullable @SerializedName(value = "ebatDischarToday") Integer battery_discharge_energy_today;
public @Nullable @SerializedName(value = "ebatDischarTotal") Integer battery_discharge_energy_total;

// inverter
public @Nullable @SerializedName(value = "Inv_Curr") Integer inverter_current;
public @Nullable @SerializedName(value = "invfanspeed") Integer inverter_fan_speed;

// @formatter:on
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,17 @@
*/
package org.openhab.binding.growatt.internal.handler;

import java.time.Duration;
import java.time.Instant;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.growatt.internal.GrowattBindingConstants;
import org.openhab.binding.growatt.internal.action.GrowattActions;
import org.openhab.binding.growatt.internal.cloud.GrowattApiException;
import org.openhab.binding.growatt.internal.cloud.GrowattCloud;
Expand All @@ -29,6 +31,7 @@
import org.openhab.binding.growatt.internal.dto.GrottValues;
import org.openhab.binding.growatt.internal.dto.helper.GrottValuesHelper;
import org.openhab.core.library.types.QuantityType;
import org.openhab.core.library.unit.Units;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.Channel;
import org.openhab.core.thing.ChannelUID;
Expand Down Expand Up @@ -103,18 +106,25 @@ private void scheduleAwaitingDataTimeoutTask() {

/**
* Receives a collection of GrottDevice inverter objects containing potential data for this thing. If the collection
* contains an entry matching the things's deviceId, and it contains GrottValues, then process it further. Otherwise
* go offline with a configuration error.
* contains an entry matching the things's deviceId, and it contains GrottValues and a time-stamp, then process it
* further. Otherwise go offline with a configuration or communication error.
*
* @param inverters collection of GrottDevice objects.
*/
public void updateInverters(Collection<GrottDevice> inverters) {
inverters.stream().filter(inverter -> deviceId.equals(inverter.getDeviceId()))
.map(inverter -> inverter.getValues()).filter(values -> values != null).findAny()
.ifPresentOrElse(values -> {
updateStatus(ThingStatus.ONLINE);
scheduleAwaitingDataTimeoutTask();
updateInverterValues(values);
inverters.stream().filter(inverter -> deviceId.equals(inverter.getDeviceId())).findAny()
.ifPresentOrElse(thisInverter -> {
GrottValues grottValues = thisInverter.getValues();
Instant dtoTimeStamp = thisInverter.getTimeStamp();
if (grottValues != null && dtoTimeStamp != null) {
updateStatus(ThingStatus.ONLINE);
updateInverterValues(grottValues);
updateState(GrowattBindingConstants.CHANNEL_INVERTER_CLOCK_OFFSET, QuantityType
.valueOf(Duration.between(Instant.now(), dtoTimeStamp).toSeconds(), Units.SECOND));
scheduleAwaitingDataTimeoutTask();
} else {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
}
}, () -> {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR);
});
Expand All @@ -140,7 +150,10 @@ public void updateInverterValues(GrottValues inverterValues) {
// find unused channels
List<Channel> actualChannels = thing.getChannels();
List<Channel> unusedChannels = actualChannels.stream()
.filter(channel -> !channelStates.containsKey(channel.getUID().getId())).collect(Collectors.toList());
.filter(channel -> !channelStates.containsKey(channel.getUID().getId()))
.filter(channel -> !GrowattBindingConstants.CHANNEL_INVERTER_CLOCK_OFFSET
.equals(channel.getUID().getId()))
.toList();

// remove unused channels
if (!unusedChannels.isEmpty()) {
Expand All @@ -149,8 +162,7 @@ public void updateInverterValues(GrottValues inverterValues) {
unusedChannels.size(), thing.getChannels().size());
}

List<String> thingChannelIds = thing.getChannels().stream().map(channel -> channel.getUID().getId())
.collect(Collectors.toList());
List<String> thingChannelIds = thing.getChannels().stream().map(channel -> channel.getUID().getId()).toList();

// update channel states
channelStates.forEach((channelId, state) -> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,14 @@ thing-type.growatt.bridge.label = Growatt Bridge
thing-type.growatt.bridge.description = Bridge Thing for Growatt Binding
thing-type.growatt.inverter.label = Growatt Inverter
thing-type.growatt.inverter.description = Inverter Thing for Growatt Binding
thing-type.growatt.inverter.channel.battery-discharge-energy-today.label = Discharge Energy Today
thing-type.growatt.inverter.channel.battery-discharge-energy-today.description = Battery discharge energy today.
thing-type.growatt.inverter.channel.battery-discharge-energy-total.label = Discharge Energy Total
thing-type.growatt.inverter.channel.battery-discharge-energy-total.description = Total battery discharge energy.
thing-type.growatt.inverter.channel.battery-discharge-va.label = Battery discharge VA
thing-type.growatt.inverter.channel.battery-discharge-va.description = Discharging reactive power.
thing-type.growatt.inverter.channel.battery-discharge-watt.label = Battery discharge power
thing-type.growatt.inverter.channel.battery-discharge-watt.description = Battery discharging power.
thing-type.growatt.inverter.channel.battery-display.label = Battery Display
thing-type.growatt.inverter.channel.battery-display.description = Battery display voltage.
thing-type.growatt.inverter.channel.battery-soc.label = Battery Charge
Expand All @@ -19,10 +27,14 @@ thing-type.growatt.inverter.channel.battery-type.label = Battery Type
thing-type.growatt.inverter.channel.battery-type.description = Type code of the battery.
thing-type.growatt.inverter.channel.battery-voltage.label = Battery Voltage
thing-type.growatt.inverter.channel.battery-voltage.description = Battery voltage.
thing-type.growatt.inverter.channel.battery-voltage2.label = Battery Voltage #2
thing-type.growatt.inverter.channel.battery-voltage2.description = Battery voltage.
thing-type.growatt.inverter.channel.charge-current.label = Charge Current
thing-type.growatt.inverter.channel.charge-current.description = Charge current to battery.
thing-type.growatt.inverter.channel.charge-power.label = Charge Power
thing-type.growatt.inverter.channel.charge-power.description = Charge power to battery.
thing-type.growatt.inverter.channel.charge-va.label = Charge VA
thing-type.growatt.inverter.channel.charge-va.description = Charging reactive power.
thing-type.growatt.inverter.channel.constant-power-ok.label = Constant Power OK
thing-type.growatt.inverter.channel.constant-power-ok.description = Constant power OK code.
thing-type.growatt.inverter.channel.discharge-energy-today.label = Battery Energy Today
Expand Down Expand Up @@ -83,6 +95,8 @@ thing-type.growatt.inverter.channel.inverter-charge-energy-today.label = Battery
thing-type.growatt.inverter.channel.inverter-charge-energy-today.description = Energy from inverter to charge battery today.
thing-type.growatt.inverter.channel.inverter-charge-energy-total.label = Battery Inverter Energy Total
thing-type.growatt.inverter.channel.inverter-charge-energy-total.description = Total energy from inverter to charge battery.
thing-type.growatt.inverter.channel.inverter-current.label = Inverter Current
thing-type.growatt.inverter.channel.inverter-current.description = Inverter current.
thing-type.growatt.inverter.channel.inverter-current-r.label = Inverter Current (#R)
thing-type.growatt.inverter.channel.inverter-current-r.description = AC current from inverter (phase #R).
thing-type.growatt.inverter.channel.inverter-current-s.label = Inverter Current #S
Expand All @@ -93,6 +107,8 @@ thing-type.growatt.inverter.channel.inverter-energy-today.label = Inverter Energ
thing-type.growatt.inverter.channel.inverter-energy-today.description = Inverter output energy produced today.
thing-type.growatt.inverter.channel.inverter-energy-total.label = Inverter Energy Total
thing-type.growatt.inverter.channel.inverter-energy-total.description = Total inverter output energy produced.
thing-type.growatt.inverter.channel.inverter-fan-speed.label = Inverter Fan
thing-type.growatt.inverter.channel.inverter-fan-speed.description = Inverter fan speed.
thing-type.growatt.inverter.channel.inverter-power.label = Inverter Power
thing-type.growatt.inverter.channel.inverter-power.description = AC power the inverter (total).
thing-type.growatt.inverter.channel.inverter-power-r.label = Inverter Power (#R)
Expand Down Expand Up @@ -209,6 +225,8 @@ channel-type.growatt.advanced-fault-code.label = Fault Code
channel-type.growatt.advanced-outdoor-temperature.label = Outdoor Temperature
channel-type.growatt.advanced-percent.label = Percentage
channel-type.growatt.advanced-status-code.label = Status Code
channel-type.growatt.advanced-time.label = Inverter Clock Offset
channel-type.growatt.advanced-time.description = Time offset of inverter clock vs. OH system clock.
channel-type.growatt.advanced-work-time.label = Work Time
channel-type.growatt.system-status-code.label = Status Code

Expand Down
Loading

0 comments on commit 81e488d

Please sign in to comment.