]> git.basschouten.com Git - openhab-addons.git/blob
ae27263f04ceb6a79e74483d3d7c065684ee5787
[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.sonnen.internal;
14
15 import static org.openhab.binding.sonnen.internal.SonnenBindingConstants.*;
16
17 import java.util.HashMap;
18 import java.util.Map;
19 import java.util.concurrent.ScheduledFuture;
20 import java.util.concurrent.TimeUnit;
21
22 import org.eclipse.jdt.annotation.NonNullByDefault;
23 import org.eclipse.jdt.annotation.Nullable;
24 import org.openhab.binding.sonnen.internal.communication.SonnenJSONCommunication;
25 import org.openhab.binding.sonnen.internal.communication.SonnenJsonDataDTO;
26 import org.openhab.binding.sonnen.internal.communication.SonnenJsonPowerMeterDataDTO;
27 import org.openhab.core.library.types.OnOffType;
28 import org.openhab.core.library.types.QuantityType;
29 import org.openhab.core.library.unit.Units;
30 import org.openhab.core.thing.Channel;
31 import org.openhab.core.thing.ChannelUID;
32 import org.openhab.core.thing.Thing;
33 import org.openhab.core.thing.ThingStatus;
34 import org.openhab.core.thing.ThingStatusDetail;
35 import org.openhab.core.thing.binding.BaseThingHandler;
36 import org.openhab.core.types.Command;
37 import org.openhab.core.types.RefreshType;
38 import org.openhab.core.types.State;
39 import org.openhab.core.types.UnDefType;
40 import org.slf4j.Logger;
41 import org.slf4j.LoggerFactory;
42
43 /**
44  * The {@link SonnenHandler} is responsible for handling commands, which are
45  * sent to one of the channels.
46  *
47  * @author Christian Feininger - Initial contribution
48  */
49 @NonNullByDefault
50 public class SonnenHandler extends BaseThingHandler {
51
52     private final Logger logger = LoggerFactory.getLogger(SonnenHandler.class);
53
54     private SonnenConfiguration config = new SonnenConfiguration();
55
56     private @Nullable ScheduledFuture<?> refreshJob;
57
58     private SonnenJSONCommunication serviceCommunication;
59
60     private boolean automaticRefreshing = false;
61
62     private boolean sonnenAPIV2 = false;
63
64     private int disconnectionCounter = 0;
65
66     private Map<String, Boolean> linkedChannels = new HashMap<>();
67
68     public SonnenHandler(Thing thing) {
69         super(thing);
70         serviceCommunication = new SonnenJSONCommunication();
71     }
72
73     @Override
74     public void initialize() {
75         logger.debug("Initializing sonnen handler for thing {}", getThing().getUID());
76         config = getConfigAs(SonnenConfiguration.class);
77         if (config.refreshInterval < 0 || config.refreshInterval > 1000) {
78             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
79                     "Parameter 'refresh Rate' msut be in the range 0-1000!");
80             return;
81         }
82         if (config.hostIP == null) {
83             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "IP Address must be configured!");
84             return;
85         }
86
87         if (!config.authToken.isEmpty()) {
88             sonnenAPIV2 = true;
89         }
90
91         serviceCommunication.setConfig(config);
92         updateStatus(ThingStatus.UNKNOWN);
93         scheduler.submit(() -> {
94             if (updateBatteryData()) {
95                 for (Channel channel : getThing().getChannels()) {
96                     if (isLinked(channel.getUID().getId())) {
97                         channelLinked(channel.getUID());
98                     }
99                 }
100             }
101         });
102     }
103
104     /**
105      * Calls the service to update the battery data
106      *
107      * @return true if the update succeeded, false otherwise
108      */
109     private boolean updateBatteryData() {
110         String error = "";
111         if (sonnenAPIV2) {
112             error = serviceCommunication.refreshBatteryConnectionAPICALLV2(arePowerMeterChannelsLinked());
113         } else {
114             error = serviceCommunication.refreshBatteryConnectionAPICALLV1();
115         }
116         if (error.isEmpty()) {
117             if (!ThingStatus.ONLINE.equals(getThing().getStatus())) {
118                 updateStatus(ThingStatus.ONLINE);
119                 disconnectionCounter = 0;
120             }
121         } else {
122             disconnectionCounter++;
123             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, error);
124             if (disconnectionCounter < 60) {
125                 return true;
126             }
127         }
128         return error.isEmpty();
129     }
130
131     private void verifyLinkedChannel(String channelID) {
132         if (isLinked(channelID) && !linkedChannels.containsKey(channelID)) {
133             linkedChannels.put(channelID, true);
134         }
135     }
136
137     @Override
138     public void dispose() {
139         stopAutomaticRefresh();
140         linkedChannels.clear();
141         automaticRefreshing = false;
142     }
143
144     private void stopAutomaticRefresh() {
145         ScheduledFuture<?> job = refreshJob;
146         if (job != null) {
147             job.cancel(true);
148         }
149         refreshJob = null;
150     }
151
152     /**
153      * Start the job refreshing the battery status
154      */
155     private void startAutomaticRefresh() {
156         ScheduledFuture<?> job = refreshJob;
157         if (job == null || job.isCancelled()) {
158             refreshJob = scheduler.scheduleWithFixedDelay(this::refreshChannels, 0, config.refreshInterval,
159                     TimeUnit.SECONDS);
160         }
161     }
162
163     private void refreshChannels() {
164         updateBatteryData();
165         for (Channel channel : getThing().getChannels()) {
166             updateChannel(channel.getUID().getId());
167         }
168     }
169
170     @Override
171     public void channelLinked(ChannelUID channelUID) {
172         if (!automaticRefreshing) {
173             logger.debug("Start automatic refreshing");
174             startAutomaticRefresh();
175             automaticRefreshing = true;
176         }
177         verifyLinkedChannel(channelUID.getId());
178         updateChannel(channelUID.getId());
179     }
180
181     @Override
182     public void channelUnlinked(ChannelUID channelUID) {
183         linkedChannels.remove(channelUID.getId());
184         if (linkedChannels.isEmpty()) {
185             automaticRefreshing = false;
186             stopAutomaticRefresh();
187             logger.debug("Stop automatic refreshing");
188         }
189     }
190
191     private void updateChannel(String channelId) {
192         if (isLinked(channelId)) {
193             State state = null;
194             SonnenJsonDataDTO data = serviceCommunication.getBatteryData();
195             // The sonnen API has two sub-channels, e.g. 4_1 and 4_2, one representing consumption and the
196             // other production. E.g. 4_1.kwh_imported represents the total production since the
197             // battery was installed.
198             SonnenJsonPowerMeterDataDTO[] dataPM = null;
199             if (arePowerMeterChannelsLinked()) {
200                 dataPM = serviceCommunication.getPowerMeterData();
201             }
202
203             if (dataPM != null && dataPM.length >= 2) {
204                 switch (channelId) {
205                     case CHANNELENERGYIMPORTEDSTATEPRODUCTION:
206                         state = new QuantityType<>(dataPM[0].getKwhImported(), Units.KILOWATT_HOUR);
207                         update(state, channelId);
208                         break;
209                     case CHANNELENERGYEXPORTEDSTATEPRODUCTION:
210                         state = new QuantityType<>(dataPM[0].getKwhExported(), Units.KILOWATT_HOUR);
211                         update(state, channelId);
212                         break;
213                     case CHANNELENERGYIMPORTEDSTATECONSUMPTION:
214                         state = new QuantityType<>(dataPM[1].getKwhImported(), Units.KILOWATT_HOUR);
215                         update(state, channelId);
216                         break;
217                     case CHANNELENERGYEXPORTEDSTATECONSUMPTION:
218                         state = new QuantityType<>(dataPM[1].getKwhExported(), Units.KILOWATT_HOUR);
219                         update(state, channelId);
220                         break;
221                 }
222             }
223
224             if (data != null) {
225                 switch (channelId) {
226                     case CHANNELBATTERYDISCHARGINGSTATE:
227                         update(OnOffType.from(data.isBatteryDischarging()), channelId);
228                         break;
229                     case CHANNELBATTERYCHARGINGSTATE:
230                         update(OnOffType.from(data.isBatteryCharging()), channelId);
231                         break;
232                     case CHANNELCONSUMPTION:
233                         state = new QuantityType<>(data.getConsumptionHouse(), Units.WATT);
234                         update(state, channelId);
235                         break;
236                     case CHANNELBATTERYDISCHARGING:
237                         state = new QuantityType<>(data.getbatteryCurrent() > 0 ? data.getbatteryCurrent() : 0,
238                                 Units.WATT);
239                         update(state, channelId);
240                         break;
241                     case CHANNELBATTERYCHARGING:
242                         state = new QuantityType<>(data.getbatteryCurrent() <= 0 ? (data.getbatteryCurrent() * -1) : 0,
243                                 Units.WATT);
244                         update(state, channelId);
245                         break;
246                     case CHANNELGRIDFEEDIN:
247                         state = new QuantityType<>(data.getGridValue() > 0 ? data.getGridValue() : 0, Units.WATT);
248                         update(state, channelId);
249                         break;
250                     case CHANNELGRIDCONSUMPTION:
251                         state = new QuantityType<>(data.getGridValue() <= 0 ? (data.getGridValue() * -1) : 0,
252                                 Units.WATT);
253                         update(state, channelId);
254                         break;
255                     case CHANNELSOLARPRODUCTION:
256                         state = new QuantityType<>(data.getSolarProduction(), Units.WATT);
257                         update(state, channelId);
258                         break;
259                     case CHANNELBATTERYLEVEL:
260                         state = new QuantityType<>(data.getBatteryChargingLevel(), Units.PERCENT);
261                         update(state, channelId);
262                         break;
263                     case CHANNELFLOWCONSUMPTIONBATTERYSTATE:
264                         update(OnOffType.from(data.isFlowConsumptionBattery()), channelId);
265                         break;
266                     case CHANNELFLOWCONSUMPTIONGRIDSTATE:
267                         update(OnOffType.from(data.isFlowConsumptionGrid()), channelId);
268                         break;
269                     case CHANNELFLOWCONSUMPTIONPRODUCTIONSTATE:
270                         update(OnOffType.from(data.isFlowConsumptionProduction()), channelId);
271                         break;
272                     case CHANNELFLOWGRIDBATTERYSTATE:
273                         update(OnOffType.from(data.isFlowGridBattery()), channelId);
274                         break;
275                     case CHANNELFLOWPRODUCTIONBATTERYSTATE:
276                         update(OnOffType.from(data.isFlowProductionBattery()), channelId);
277                         break;
278                     case CHANNELFLOWPRODUCTIONGRIDSTATE:
279                         update(OnOffType.from(data.isFlowProductionGrid()), channelId);
280                         break;
281                 }
282             }
283         } else {
284             update(null, channelId);
285         }
286     }
287
288     @SuppressWarnings("PMD.SimplifyBooleanReturns")
289     private boolean arePowerMeterChannelsLinked() {
290         if (isLinked(CHANNELENERGYIMPORTEDSTATEPRODUCTION)) {
291             return true;
292         } else if (isLinked(CHANNELENERGYEXPORTEDSTATEPRODUCTION)) {
293             return true;
294         } else if (isLinked(CHANNELENERGYIMPORTEDSTATECONSUMPTION)) {
295             return true;
296         } else if (isLinked(CHANNELENERGYEXPORTEDSTATECONSUMPTION)) {
297             return true;
298         } else {
299             return false;
300         }
301     }
302
303     /**
304      * Updates the State of the given channel
305      *
306      * @param state Given state
307      * @param channelId the refereed channelID
308      */
309     private void update(@Nullable State state, String channelId) {
310         logger.debug("Update channel {} with state {}", channelId, (state == null) ? "null" : state.toString());
311         updateState(channelId, state != null ? state : UnDefType.UNDEF);
312     }
313
314     @Override
315     public void handleCommand(ChannelUID channelUID, Command command) {
316         if (command == RefreshType.REFRESH) {
317             updateBatteryData();
318             updateChannel(channelUID.getId());
319         }
320     }
321 }