]> git.basschouten.com Git - openhab-addons.git/blob
eeb960461f5f43d67e4ae66c6e279e2e1068dae0
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2024 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
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
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.binding.growatt.internal.handler;
14
15 import java.util.Collection;
16 import java.util.List;
17 import java.util.Map;
18 import java.util.concurrent.ScheduledFuture;
19 import java.util.concurrent.TimeUnit;
20 import java.util.stream.Collectors;
21
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;
43
44 /**
45  * The {@link GrowattInverterHandler} is a thing handler for Growatt inverters.
46  *
47  * @author Andrew Fiddian-Green - Initial contribution
48  */
49 @NonNullByDefault
50 public class GrowattInverterHandler extends BaseThingHandler {
51
52     // data-logger sends packets each 5 minutes; timeout means 2 packets missed
53     private static final int AWAITING_DATA_TIMEOUT_MINUTES = 11;
54
55     private final Logger logger = LoggerFactory.getLogger(GrowattInverterHandler.class);
56
57     private String deviceId = "unknown";
58
59     private @Nullable ScheduledFuture<?> awaitingDataTimeoutTask;
60
61     public GrowattInverterHandler(Thing thing) {
62         super(thing);
63     }
64
65     @Override
66     public void dispose() {
67         ScheduledFuture<?> task = awaitingDataTimeoutTask;
68         if (task != null) {
69             task.cancel(true);
70         }
71     }
72
73     @Override
74     public Collection<Class<? extends ThingHandlerService>> getServices() {
75         return List.of(GrowattActions.class);
76     }
77
78     @Override
79     public void handleCommand(ChannelUID channelUID, Command command) {
80         // everything is read only so do nothing
81     }
82
83     @Override
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());
91     }
92
93     private void scheduleAwaitingDataTimeoutTask() {
94         ScheduledFuture<?> task = awaitingDataTimeoutTask;
95         if (task != null) {
96             task.cancel(true);
97         }
98         awaitingDataTimeoutTask = scheduler.schedule(() -> {
99             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
100                     "@text/status.awaiting-data-timeout");
101         }, AWAITING_DATA_TIMEOUT_MINUTES, TimeUnit.MINUTES);
102     }
103
104     /**
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.
108      *
109      * @param inverters collection of GrottDevice objects.
110      */
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);
118                 }, () -> {
119                     updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR);
120                 });
121     }
122
123     /**
124      * Receives a GrottValues object containing state values for this thing. Process the respective values and update
125      * the channels accordingly.
126      *
127      * @param inverter a GrottDevice object containing the new status values.
128      */
129     public void updateInverterValues(GrottValues inverterValues) {
130         // get channel states
131         Map<String, QuantityType<?>> channelStates;
132         try {
133             channelStates = GrottValuesHelper.getChannelStates(inverterValues);
134         } catch (NoSuchFieldException | SecurityException | IllegalAccessException | IllegalArgumentException e) {
135             logger.warn("updateInverterValues() unexpected exception:{}, message:{}", e.getClass().getName(),
136                     e.getMessage(), e);
137             return;
138         }
139
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());
144
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());
150         }
151
152         List<String> thingChannelIds = thing.getChannels().stream().map(channel -> channel.getUID().getId())
153                 .collect(Collectors.toList());
154
155         // update channel states
156         channelStates.forEach((channelId, state) -> {
157             if (thingChannelIds.contains(channelId)) {
158                 updateState(channelId, state);
159             } else {
160                 logger.warn("updateInverterValues() channel '{}' not found; try re-creating the thing", channelId);
161             }
162         });
163     }
164
165     private GrowattCloud getGrowattCloud() throws IllegalStateException {
166         Bridge bridge = getBridge();
167         if (bridge != null && (bridge.getHandler() instanceof GrowattBridgeHandler bridgeHandler)) {
168             return bridgeHandler.getGrowattCloud();
169         }
170         throw new IllegalStateException("Unable to get GrowattCloud from bridge handler");
171     }
172
173     /**
174      * This method is called from a Rule Action to setup the battery charging program.
175      *
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
183      */
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) {
187         try {
188             getGrowattCloud().setupBatteryProgram(deviceId, programMode, powerLevel, stopSOC, enableAcCharging,
189                     startTime, stopTime, enableProgram);
190         } catch (GrowattApiException e) {
191             logger.warn("setupBatteryProgram() error", e);
192         }
193     }
194 }