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