2 * Copyright (c) 2010-2024 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
7 * This program and the accompanying materials are made available under the
8 * terms of the Eclipse Public License 2.0 which is available at
9 * http://www.eclipse.org/legal/epl-2.0
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.growatt.internal.handler;
15 import java.util.Collection;
16 import java.util.List;
18 import java.util.concurrent.ScheduledFuture;
19 import java.util.concurrent.TimeUnit;
20 import java.util.stream.Collectors;
22 import org.eclipse.jdt.annotation.NonNullByDefault;
23 import org.eclipse.jdt.annotation.Nullable;
24 import org.openhab.binding.growatt.internal.action.GrowattActions;
25 import org.openhab.binding.growatt.internal.cloud.GrowattApiException;
26 import org.openhab.binding.growatt.internal.cloud.GrowattCloud;
27 import org.openhab.binding.growatt.internal.config.GrowattInverterConfiguration;
28 import org.openhab.binding.growatt.internal.dto.GrottDevice;
29 import org.openhab.binding.growatt.internal.dto.GrottValues;
30 import org.openhab.binding.growatt.internal.dto.helper.GrottValuesHelper;
31 import org.openhab.core.library.types.QuantityType;
32 import org.openhab.core.thing.Bridge;
33 import org.openhab.core.thing.Channel;
34 import org.openhab.core.thing.ChannelUID;
35 import org.openhab.core.thing.Thing;
36 import org.openhab.core.thing.ThingStatus;
37 import org.openhab.core.thing.ThingStatusDetail;
38 import org.openhab.core.thing.binding.BaseThingHandler;
39 import org.openhab.core.thing.binding.ThingHandlerService;
40 import org.openhab.core.types.Command;
41 import org.slf4j.Logger;
42 import org.slf4j.LoggerFactory;
45 * The {@link GrowattInverterHandler} is a thing handler for Growatt inverters.
47 * @author Andrew Fiddian-Green - Initial contribution
50 public class GrowattInverterHandler extends BaseThingHandler {
52 // data-logger sends packets each 5 minutes; timeout means 2 packets missed
53 private static final int AWAITING_DATA_TIMEOUT_MINUTES = 11;
55 private final Logger logger = LoggerFactory.getLogger(GrowattInverterHandler.class);
57 private String deviceId = "unknown";
59 private @Nullable ScheduledFuture<?> awaitingDataTimeoutTask;
61 public GrowattInverterHandler(Thing thing) {
66 public void dispose() {
67 ScheduledFuture<?> task = awaitingDataTimeoutTask;
74 public Collection<Class<? extends ThingHandlerService>> getServices() {
75 return List.of(GrowattActions.class);
79 public void handleCommand(ChannelUID channelUID, Command command) {
80 // everything is read only so do nothing
84 public void initialize() {
85 GrowattInverterConfiguration config = getConfigAs(GrowattInverterConfiguration.class);
86 deviceId = config.deviceId;
87 thing.setProperty(GrowattInverterConfiguration.DEVICE_ID, deviceId);
88 updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.NONE, "@text/status.awaiting-data");
89 scheduleAwaitingDataTimeoutTask();
90 logger.debug("initialize() thing has {} channels", thing.getChannels().size());
93 private void scheduleAwaitingDataTimeoutTask() {
94 ScheduledFuture<?> task = awaitingDataTimeoutTask;
98 awaitingDataTimeoutTask = scheduler.schedule(() -> {
99 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
100 "@text/status.awaiting-data-timeout");
101 }, AWAITING_DATA_TIMEOUT_MINUTES, TimeUnit.MINUTES);
105 * 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 * @param inverters collection of GrottDevice objects.
111 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);
119 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR);
124 * Receives a GrottValues object containing state values for this thing. Process the respective values and update
125 * the channels accordingly.
127 * @param inverter a GrottDevice object containing the new status values.
129 public void updateInverterValues(GrottValues inverterValues) {
130 // get channel states
131 Map<String, QuantityType<?>> channelStates;
133 channelStates = GrottValuesHelper.getChannelStates(inverterValues);
134 } catch (NoSuchFieldException | SecurityException | IllegalAccessException | IllegalArgumentException e) {
135 logger.warn("updateInverterValues() unexpected exception:{}, message:{}", e.getClass().getName(),
140 // find unused channels
141 List<Channel> actualChannels = thing.getChannels();
142 List<Channel> unusedChannels = actualChannels.stream()
143 .filter(channel -> !channelStates.containsKey(channel.getUID().getId())).collect(Collectors.toList());
145 // remove unused channels
146 if (!unusedChannels.isEmpty()) {
147 updateThing(editThing().withoutChannels(unusedChannels).build());
148 logger.debug("updateInverterValues() channel count {} reduced by {} to {}", actualChannels.size(),
149 unusedChannels.size(), thing.getChannels().size());
152 List<String> thingChannelIds = thing.getChannels().stream().map(channel -> channel.getUID().getId())
153 .collect(Collectors.toList());
155 // update channel states
156 channelStates.forEach((channelId, state) -> {
157 if (thingChannelIds.contains(channelId)) {
158 updateState(channelId, state);
160 logger.warn("updateInverterValues() channel '{}' not found; try re-creating the thing", channelId);
165 private GrowattCloud getGrowattCloud() throws IllegalStateException {
166 Bridge bridge = getBridge();
167 if (bridge != null && (bridge.getHandler() instanceof GrowattBridgeHandler bridgeHandler)) {
168 return bridgeHandler.getGrowattCloud();
170 throw new IllegalStateException("Unable to get GrowattCloud from bridge handler");
174 * This method is called from a Rule Action to setup the battery charging program.
176 * @param programMode indicates if the program is Load first (0), Battery first (1), Grid first (2)
177 * @param powerLevel the rate of charging / discharging 0%..100%
178 * @param stopSOC the SOC at which to stop charging / discharging 0%..100%
179 * @param enableAcCharging allow the battery to be charged from AC power
180 * @param startTime the start time of the charging program; a time formatted string e.g. "12:34"
181 * @param stopTime the stop time of the charging program; a time formatted string e.g. "12:34"
182 * @param enableProgram charge / discharge program shall be enabled
184 public void setupBatteryProgram(Integer programMode, @Nullable Integer powerLevel, @Nullable Integer stopSOC,
185 @Nullable Boolean enableAcCharging, @Nullable String startTime, @Nullable String stopTime,
186 @Nullable Boolean enableProgram) {
188 getGrowattCloud().setupBatteryProgram(deviceId, programMode, powerLevel, stopSOC, enableAcCharging,
189 startTime, stopTime, enableProgram);
190 } catch (GrowattApiException e) {
191 logger.warn("setupBatteryProgram() error", e);