Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[growatt] Enhance support for SPF inverters #17795

Merged
merged 10 commits into from
Nov 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 ==============
*/
andrewfg marked this conversation as resolved.
Show resolved Hide resolved

// 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 ==============
*/
andrewfg marked this conversation as resolved.
Show resolved Hide resolved

// 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