Skip to content

Commit 81e488d

Browse files
authored
[growatt] Enhance support for SPF inverters (openhab#17795)
* [growatt] tweak channel aliases; add missing channels Signed-off-by: AndrewFG <[email protected]>
1 parent 3ae0203 commit 81e488d

File tree

12 files changed

+390
-115
lines changed

12 files changed

+390
-115
lines changed

bundles/org.openhab.binding.growatt/README.md

+99-90
Large diffs are not rendered by default.

bundles/org.openhab.binding.growatt/src/main/java/org/openhab/binding/growatt/internal/GrowattBindingConstants.java

+2
Original file line numberDiff line numberDiff line change
@@ -28,4 +28,6 @@ public class GrowattBindingConstants {
2828

2929
public static final ThingTypeUID THING_TYPE_BRIDGE = new ThingTypeUID(BINDING_ID, "bridge");
3030
public static final ThingTypeUID THING_TYPE_INVERTER = new ThingTypeUID(BINDING_ID, "inverter");
31+
32+
public static final String CHANNEL_INVERTER_CLOCK_OFFSET = "inverter-clock-offset";
3133
}

bundles/org.openhab.binding.growatt/src/main/java/org/openhab/binding/growatt/internal/GrowattChannels.java

+21-1
Original file line numberDiff line numberDiff line change
@@ -186,7 +186,27 @@ public UoM(Unit<?> units, float divisor) {
186186
// reactive 'power' resp. 'energy'
187187
new AbstractMap.SimpleEntry<String, UoM>("rac", new UoM(Units.VAR, 10)),
188188
new AbstractMap.SimpleEntry<String, UoM>("erac-today", new UoM(Units.KILOVAR_HOUR, 10)),
189-
new AbstractMap.SimpleEntry<String, UoM>("erac-total", new UoM(Units.KILOVAR_HOUR, 10))
189+
new AbstractMap.SimpleEntry<String, UoM>("erac-total", new UoM(Units.KILOVAR_HOUR, 10)),
190+
191+
/*
192+
* ============== CHANNELS ADDED IN PR #17795 ==============
193+
*/
194+
195+
// battery instantaneous measurements
196+
new AbstractMap.SimpleEntry<String, UoM>("battery-voltage2", new UoM(Units.VOLT, 100)),
197+
new AbstractMap.SimpleEntry<String, UoM>("charge-va", new UoM(Units.VOLT_AMPERE, 10)),
198+
new AbstractMap.SimpleEntry<String, UoM>("battery-discharge-va", new UoM(Units.VOLT_AMPERE, 10)),
199+
new AbstractMap.SimpleEntry<String, UoM>("battery-discharge-watt", new UoM(Units.WATT, 10)),
200+
201+
// battery energy
202+
new AbstractMap.SimpleEntry<String, UoM>("battery-discharge-energy-today",
203+
new UoM(Units.KILOWATT_HOUR, 10)),
204+
new AbstractMap.SimpleEntry<String, UoM>("battery-discharge-energy-total",
205+
new UoM(Units.KILOWATT_HOUR, 10)),
206+
207+
// inverter
208+
new AbstractMap.SimpleEntry<String, UoM>("inverter-current", new UoM(Units.AMPERE, 10)),
209+
new AbstractMap.SimpleEntry<String, UoM>("inverter-fan-speed", new UoM(Units.PERCENT, 1))
190210
//
191211
);
192212

bundles/org.openhab.binding.growatt/src/main/java/org/openhab/binding/growatt/internal/dto/GrottDevice.java

+26
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,11 @@
1313
package org.openhab.binding.growatt.internal.dto;
1414

1515
import java.lang.reflect.Type;
16+
import java.time.DateTimeException;
17+
import java.time.Instant;
18+
import java.time.LocalDateTime;
19+
import java.time.ZoneId;
20+
import java.time.ZonedDateTime;
1621
import java.util.ArrayList;
1722

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

3641
private @Nullable @SerializedName("device") String deviceId;
42+
private @Nullable @SerializedName("time") String timeStamp;
3743
private @Nullable GrottValues values;
3844

3945
public String getDeviceId() {
@@ -44,4 +50,24 @@ public String getDeviceId() {
4450
public @Nullable GrottValues getValues() {
4551
return values;
4652
}
53+
54+
/**
55+
* Return the time stamp of the data DTO sent by the inverter data-logger.
56+
* <p>
57+
* Note: the inverter provides a time stamp formatted as a {@link LocalDateTime} without any time zone information,
58+
* so we convert it to an {@link Instant} based on the OH system time zone. i.e. we are forced to assume the
59+
* inverter and the OH PC are both physically in the same time zone.
60+
*
61+
* @return the time stamp {@link Instant}
62+
*/
63+
public @Nullable Instant getTimeStamp() {
64+
String timeStamp = this.timeStamp;
65+
if (timeStamp != null) {
66+
try {
67+
return ZonedDateTime.of(LocalDateTime.parse(timeStamp), ZoneId.systemDefault()).toInstant();
68+
} catch (DateTimeException e) {
69+
}
70+
}
71+
return null;
72+
}
4773
}

bundles/org.openhab.binding.growatt/src/main/java/org/openhab/binding/growatt/internal/dto/GrottValues.java

+29-11
Original file line numberDiff line numberDiff line change
@@ -67,24 +67,24 @@ public static String getFieldName(String channelId) {
6767
public @Nullable @SerializedName(value = "Vac_TR", alternate = { "vactr", "L3-1_voltage" }) Integer grid_voltage_tr;
6868

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

74-
public @Nullable @SerializedName(value = "pvgridpower", alternate = { "op_watt", "AC_InWatt" }) Integer inverter_power_r;
74+
public @Nullable @SerializedName(value = "pvgridpower", alternate = { "op_watt" }) Integer inverter_power_r;
7575
public @Nullable @SerializedName(value = "pvgridpower2") Integer inverter_power_s;
7676
public @Nullable @SerializedName(value = "pvgridpower3") Integer inverter_power_t;
7777

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

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

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

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

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

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

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

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

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

187+
/*
188+
* ============== CHANNELS ADDED IN PR #17795 ==============
189+
*/
190+
191+
// battery instantaneous measurements
192+
public @Nullable @SerializedName(value = "bat_Volt") Integer battery_voltage2;
193+
public @Nullable @SerializedName(value = "acchr_VA") Integer charge_va;
194+
public @Nullable @SerializedName(value = "BatDischarVA") Integer battery_discharge_va;
195+
public @Nullable @SerializedName(value = "BatDischarWatt", alternate = { "BatWatt" }) Integer battery_discharge_watt;
196+
197+
// battery energy
198+
public @Nullable @SerializedName(value = "ebatDischarToday") Integer battery_discharge_energy_today;
199+
public @Nullable @SerializedName(value = "ebatDischarTotal") Integer battery_discharge_energy_total;
200+
201+
// inverter
202+
public @Nullable @SerializedName(value = "Inv_Curr") Integer inverter_current;
203+
public @Nullable @SerializedName(value = "invfanspeed") Integer inverter_fan_speed;
204+
187205
// @formatter:on
188206
}

bundles/org.openhab.binding.growatt/src/main/java/org/openhab/binding/growatt/internal/handler/GrowattInverterHandler.java

+24-12
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,17 @@
1212
*/
1313
package org.openhab.binding.growatt.internal.handler;
1414

15+
import java.time.Duration;
16+
import java.time.Instant;
1517
import java.util.Collection;
1618
import java.util.List;
1719
import java.util.Map;
1820
import java.util.concurrent.ScheduledFuture;
1921
import java.util.concurrent.TimeUnit;
20-
import java.util.stream.Collectors;
2122

2223
import org.eclipse.jdt.annotation.NonNullByDefault;
2324
import org.eclipse.jdt.annotation.Nullable;
25+
import org.openhab.binding.growatt.internal.GrowattBindingConstants;
2426
import org.openhab.binding.growatt.internal.action.GrowattActions;
2527
import org.openhab.binding.growatt.internal.cloud.GrowattApiException;
2628
import org.openhab.binding.growatt.internal.cloud.GrowattCloud;
@@ -29,6 +31,7 @@
2931
import org.openhab.binding.growatt.internal.dto.GrottValues;
3032
import org.openhab.binding.growatt.internal.dto.helper.GrottValuesHelper;
3133
import org.openhab.core.library.types.QuantityType;
34+
import org.openhab.core.library.unit.Units;
3235
import org.openhab.core.thing.Bridge;
3336
import org.openhab.core.thing.Channel;
3437
import org.openhab.core.thing.ChannelUID;
@@ -103,18 +106,25 @@ private void scheduleAwaitingDataTimeoutTask() {
103106

104107
/**
105108
* Receives a collection of GrottDevice inverter objects containing potential data for this thing. If the collection
106-
* contains an entry matching the things's deviceId, and it contains GrottValues, then process it further. Otherwise
107-
* go offline with a configuration error.
109+
* contains an entry matching the things's deviceId, and it contains GrottValues and a time-stamp, then process it
110+
* further. Otherwise go offline with a configuration or communication error.
108111
*
109112
* @param inverters collection of GrottDevice objects.
110113
*/
111114
public void updateInverters(Collection<GrottDevice> inverters) {
112-
inverters.stream().filter(inverter -> deviceId.equals(inverter.getDeviceId()))
113-
.map(inverter -> inverter.getValues()).filter(values -> values != null).findAny()
114-
.ifPresentOrElse(values -> {
115-
updateStatus(ThingStatus.ONLINE);
116-
scheduleAwaitingDataTimeoutTask();
117-
updateInverterValues(values);
115+
inverters.stream().filter(inverter -> deviceId.equals(inverter.getDeviceId())).findAny()
116+
.ifPresentOrElse(thisInverter -> {
117+
GrottValues grottValues = thisInverter.getValues();
118+
Instant dtoTimeStamp = thisInverter.getTimeStamp();
119+
if (grottValues != null && dtoTimeStamp != null) {
120+
updateStatus(ThingStatus.ONLINE);
121+
updateInverterValues(grottValues);
122+
updateState(GrowattBindingConstants.CHANNEL_INVERTER_CLOCK_OFFSET, QuantityType
123+
.valueOf(Duration.between(Instant.now(), dtoTimeStamp).toSeconds(), Units.SECOND));
124+
scheduleAwaitingDataTimeoutTask();
125+
} else {
126+
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
127+
}
118128
}, () -> {
119129
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR);
120130
});
@@ -140,7 +150,10 @@ public void updateInverterValues(GrottValues inverterValues) {
140150
// find unused channels
141151
List<Channel> actualChannels = thing.getChannels();
142152
List<Channel> unusedChannels = actualChannels.stream()
143-
.filter(channel -> !channelStates.containsKey(channel.getUID().getId())).collect(Collectors.toList());
153+
.filter(channel -> !channelStates.containsKey(channel.getUID().getId()))
154+
.filter(channel -> !GrowattBindingConstants.CHANNEL_INVERTER_CLOCK_OFFSET
155+
.equals(channel.getUID().getId()))
156+
.toList();
144157

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

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

155167
// update channel states
156168
channelStates.forEach((channelId, state) -> {

bundles/org.openhab.binding.growatt/src/main/resources/OH-INF/i18n/growatt.properties

+18
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,14 @@ thing-type.growatt.bridge.label = Growatt Bridge
99
thing-type.growatt.bridge.description = Bridge Thing for Growatt Binding
1010
thing-type.growatt.inverter.label = Growatt Inverter
1111
thing-type.growatt.inverter.description = Inverter Thing for Growatt Binding
12+
thing-type.growatt.inverter.channel.battery-discharge-energy-today.label = Discharge Energy Today
13+
thing-type.growatt.inverter.channel.battery-discharge-energy-today.description = Battery discharge energy today.
14+
thing-type.growatt.inverter.channel.battery-discharge-energy-total.label = Discharge Energy Total
15+
thing-type.growatt.inverter.channel.battery-discharge-energy-total.description = Total battery discharge energy.
16+
thing-type.growatt.inverter.channel.battery-discharge-va.label = Battery discharge VA
17+
thing-type.growatt.inverter.channel.battery-discharge-va.description = Discharging reactive power.
18+
thing-type.growatt.inverter.channel.battery-discharge-watt.label = Battery discharge power
19+
thing-type.growatt.inverter.channel.battery-discharge-watt.description = Battery discharging power.
1220
thing-type.growatt.inverter.channel.battery-display.label = Battery Display
1321
thing-type.growatt.inverter.channel.battery-display.description = Battery display voltage.
1422
thing-type.growatt.inverter.channel.battery-soc.label = Battery Charge
@@ -19,10 +27,14 @@ thing-type.growatt.inverter.channel.battery-type.label = Battery Type
1927
thing-type.growatt.inverter.channel.battery-type.description = Type code of the battery.
2028
thing-type.growatt.inverter.channel.battery-voltage.label = Battery Voltage
2129
thing-type.growatt.inverter.channel.battery-voltage.description = Battery voltage.
30+
thing-type.growatt.inverter.channel.battery-voltage2.label = Battery Voltage #2
31+
thing-type.growatt.inverter.channel.battery-voltage2.description = Battery voltage.
2232
thing-type.growatt.inverter.channel.charge-current.label = Charge Current
2333
thing-type.growatt.inverter.channel.charge-current.description = Charge current to battery.
2434
thing-type.growatt.inverter.channel.charge-power.label = Charge Power
2535
thing-type.growatt.inverter.channel.charge-power.description = Charge power to battery.
36+
thing-type.growatt.inverter.channel.charge-va.label = Charge VA
37+
thing-type.growatt.inverter.channel.charge-va.description = Charging reactive power.
2638
thing-type.growatt.inverter.channel.constant-power-ok.label = Constant Power OK
2739
thing-type.growatt.inverter.channel.constant-power-ok.description = Constant power OK code.
2840
thing-type.growatt.inverter.channel.discharge-energy-today.label = Battery Energy Today
@@ -83,6 +95,8 @@ thing-type.growatt.inverter.channel.inverter-charge-energy-today.label = Battery
8395
thing-type.growatt.inverter.channel.inverter-charge-energy-today.description = Energy from inverter to charge battery today.
8496
thing-type.growatt.inverter.channel.inverter-charge-energy-total.label = Battery Inverter Energy Total
8597
thing-type.growatt.inverter.channel.inverter-charge-energy-total.description = Total energy from inverter to charge battery.
98+
thing-type.growatt.inverter.channel.inverter-current.label = Inverter Current
99+
thing-type.growatt.inverter.channel.inverter-current.description = Inverter current.
86100
thing-type.growatt.inverter.channel.inverter-current-r.label = Inverter Current (#R)
87101
thing-type.growatt.inverter.channel.inverter-current-r.description = AC current from inverter (phase #R).
88102
thing-type.growatt.inverter.channel.inverter-current-s.label = Inverter Current #S
@@ -93,6 +107,8 @@ thing-type.growatt.inverter.channel.inverter-energy-today.label = Inverter Energ
93107
thing-type.growatt.inverter.channel.inverter-energy-today.description = Inverter output energy produced today.
94108
thing-type.growatt.inverter.channel.inverter-energy-total.label = Inverter Energy Total
95109
thing-type.growatt.inverter.channel.inverter-energy-total.description = Total inverter output energy produced.
110+
thing-type.growatt.inverter.channel.inverter-fan-speed.label = Inverter Fan
111+
thing-type.growatt.inverter.channel.inverter-fan-speed.description = Inverter fan speed.
96112
thing-type.growatt.inverter.channel.inverter-power.label = Inverter Power
97113
thing-type.growatt.inverter.channel.inverter-power.description = AC power the inverter (total).
98114
thing-type.growatt.inverter.channel.inverter-power-r.label = Inverter Power (#R)
@@ -209,6 +225,8 @@ channel-type.growatt.advanced-fault-code.label = Fault Code
209225
channel-type.growatt.advanced-outdoor-temperature.label = Outdoor Temperature
210226
channel-type.growatt.advanced-percent.label = Percentage
211227
channel-type.growatt.advanced-status-code.label = Status Code
228+
channel-type.growatt.advanced-time.label = Inverter Clock Offset
229+
channel-type.growatt.advanced-time.description = Time offset of inverter clock vs. OH system clock.
212230
channel-type.growatt.advanced-work-time.label = Work Time
213231
channel-type.growatt.system-status-code.label = Status Code
214232

0 commit comments

Comments
 (0)