]> git.basschouten.com Git - openhab-addons.git/blob
da8e1ff7f9f190b43840e0304fa7bdbb89cf527c
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2021 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.intesis.internal.handler;
14
15 import static org.openhab.binding.intesis.internal.IntesisBindingConstants.*;
16 import static org.openhab.core.thing.Thing.*;
17
18 import java.util.ArrayList;
19 import java.util.Collection;
20 import java.util.HashMap;
21 import java.util.List;
22 import java.util.Map;
23 import java.util.concurrent.ScheduledFuture;
24 import java.util.concurrent.TimeUnit;
25 import java.util.function.Consumer;
26 import java.util.function.UnaryOperator;
27 import java.util.stream.Collectors;
28
29 import org.eclipse.jdt.annotation.NonNullByDefault;
30 import org.eclipse.jdt.annotation.Nullable;
31 import org.eclipse.jetty.client.HttpClient;
32 import org.openhab.binding.intesis.internal.IntesisDynamicStateDescriptionProvider;
33 import org.openhab.binding.intesis.internal.api.IntesisHomeHttpApi;
34 import org.openhab.binding.intesis.internal.config.IntesisHomeConfiguration;
35 import org.openhab.binding.intesis.internal.enums.IntesisHomeModeEnum;
36 import org.openhab.binding.intesis.internal.gson.IntesisHomeJSonDTO.Data;
37 import org.openhab.binding.intesis.internal.gson.IntesisHomeJSonDTO.Datapoints;
38 import org.openhab.binding.intesis.internal.gson.IntesisHomeJSonDTO.Descr;
39 import org.openhab.binding.intesis.internal.gson.IntesisHomeJSonDTO.Dp;
40 import org.openhab.binding.intesis.internal.gson.IntesisHomeJSonDTO.Dpval;
41 import org.openhab.binding.intesis.internal.gson.IntesisHomeJSonDTO.Id;
42 import org.openhab.binding.intesis.internal.gson.IntesisHomeJSonDTO.Info;
43 import org.openhab.binding.intesis.internal.gson.IntesisHomeJSonDTO.Response;
44 import org.openhab.core.library.types.OnOffType;
45 import org.openhab.core.library.types.QuantityType;
46 import org.openhab.core.library.types.StringType;
47 import org.openhab.core.library.unit.SIUnits;
48 import org.openhab.core.thing.Channel;
49 import org.openhab.core.thing.ChannelUID;
50 import org.openhab.core.thing.Thing;
51 import org.openhab.core.thing.ThingStatus;
52 import org.openhab.core.thing.ThingStatusDetail;
53 import org.openhab.core.thing.binding.BaseThingHandler;
54 import org.openhab.core.thing.binding.builder.ChannelBuilder;
55 import org.openhab.core.thing.binding.builder.ThingBuilder;
56 import org.openhab.core.thing.type.ChannelKind;
57 import org.openhab.core.thing.type.ChannelTypeUID;
58 import org.openhab.core.types.Command;
59 import org.openhab.core.types.RefreshType;
60 import org.openhab.core.types.State;
61 import org.openhab.core.types.StateOption;
62 import org.slf4j.Logger;
63 import org.slf4j.LoggerFactory;
64
65 import com.google.gson.Gson;
66 import com.google.gson.JsonSyntaxException;
67
68 /**
69  * The {@link IntesisHomeHandler} is responsible for handling commands, which are
70  * sent to one of the channels.
71  *
72  * @author Hans-Jörg Merk - Initial contribution
73  */
74 @NonNullByDefault
75 public class IntesisHomeHandler extends BaseThingHandler {
76
77     private final Logger logger = LoggerFactory.getLogger(IntesisHomeHandler.class);
78     private final IntesisHomeHttpApi api;
79
80     private final Map<String, String> properties = new HashMap<>();
81
82     private final IntesisDynamicStateDescriptionProvider intesisStateDescriptionProvider;
83
84     private final Gson gson = new Gson();
85
86     private IntesisHomeConfiguration config = new IntesisHomeConfiguration();
87
88     private @Nullable ScheduledFuture<?> refreshJob;
89
90     public IntesisHomeHandler(final Thing thing, final HttpClient httpClient,
91             IntesisDynamicStateDescriptionProvider intesisStateDescriptionProvider) {
92         super(thing);
93         this.api = new IntesisHomeHttpApi(config, httpClient);
94         this.intesisStateDescriptionProvider = intesisStateDescriptionProvider;
95     }
96
97     @Override
98     public void initialize() {
99         updateStatus(ThingStatus.UNKNOWN);
100         config = getConfigAs(IntesisHomeConfiguration.class);
101         if (config.ipAddress.isEmpty() && config.password.isEmpty()) {
102             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "IP-Address and password not set");
103             return;
104         } else if (config.ipAddress.isEmpty()) {
105             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "IP-Address not set");
106             return;
107         } else if (config.password.isEmpty()) {
108             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Password not set");
109             return;
110         } else {
111             // start background initialization:
112             scheduler.submit(() -> {
113                 populateProperties();
114                 // query available dataPoints and build dynamic channels
115                 postRequestInSession(sessionId -> "{\"command\":\"getavailabledatapoints\",\"data\":{\"sessionID\":\""
116                         + sessionId + "\"}}", this::handleDataPointsResponse);
117                 updateProperties(properties);
118             });
119         }
120     }
121
122     @Override
123     public void dispose() {
124         logger.debug("IntesisHomeHandler disposed.");
125         final ScheduledFuture<?> refreshJob = this.refreshJob;
126
127         if (refreshJob != null) {
128             refreshJob.cancel(true);
129             this.refreshJob = null;
130         }
131     }
132
133     @Override
134     public void handleCommand(ChannelUID channelUID, Command command) {
135         int uid = 0;
136         int value = 0;
137         String channelId = channelUID.getId();
138         if (command instanceof RefreshType) {
139             // Refresh command is not supported as the binding polls all values every 30 seconds
140         } else {
141             switch (channelId) {
142                 case CHANNEL_TYPE_POWER:
143                     uid = 1;
144                     value = command.equals(OnOffType.OFF) ? 0 : 1;
145                     break;
146                 case CHANNEL_TYPE_MODE:
147                     uid = 2;
148                     value = IntesisHomeModeEnum.valueOf(command.toString()).getMode();
149                     break;
150                 case CHANNEL_TYPE_FANSPEED:
151                     uid = 4;
152                     if (("AUTO").equals(command.toString())) {
153                         value = 0;
154                     } else {
155                         value = Integer.parseInt(command.toString());
156                     }
157                     break;
158                 case CHANNEL_TYPE_VANESUD:
159                 case CHANNEL_TYPE_VANESLR:
160                     switch (command.toString()) {
161                         case "AUTO":
162                             value = 0;
163                             break;
164                         case "1":
165                         case "2":
166                         case "3":
167                         case "4":
168                         case "5":
169                         case "6":
170                         case "7":
171                         case "8":
172                         case "9":
173                             value = Integer.parseInt(command.toString());
174                             break;
175                         case "SWING":
176                             value = 10;
177                             break;
178                         case "SWIRL":
179                             value = 11;
180                             break;
181                         case "WIDE":
182                             value = 12;
183                             break;
184                     }
185                     switch (channelId) {
186                         case CHANNEL_TYPE_VANESUD:
187                             uid = 5;
188                             break;
189                         case CHANNEL_TYPE_VANESLR:
190                             uid = 6;
191                             break;
192                     }
193                     break;
194                 case CHANNEL_TYPE_TARGETTEMP:
195                     uid = 9;
196                     if (command instanceof QuantityType) {
197                         QuantityType<?> newVal = (QuantityType<?>) command;
198                         newVal = newVal.toUnit(SIUnits.CELSIUS);
199                         if (newVal != null) {
200                             value = newVal.intValue() * 10;
201                         }
202                     }
203                     break;
204             }
205         }
206         if (uid != 0) {
207             final int uId = uid;
208             final int newValue = value;
209             scheduler.submit(() -> {
210                 postRequestInSession(
211                         sessionId -> "{\"command\":\"setdatapointvalue\",\"data\":{\"sessionID\":\"" + sessionId
212                                 + "\", \"uid\":" + uId + ",\"value\":" + newValue + "}}",
213                         r -> updateStatus(ThingStatus.ONLINE));
214             });
215         }
216     }
217
218     public @Nullable String login() {
219         // lambda's can't modify local variables, so we use an array here to get around the issue
220         String[] sessionId = new String[1];
221         postRequest(
222                 "{\"command\":\"login\",\"data\":{\"username\":\"Admin\",\"password\":\"" + config.password + "\"}}",
223                 resp -> {
224                     Data data = gson.fromJson(resp.data, Data.class);
225                     Id id = gson.fromJson(data.id, Id.class);
226                     sessionId[0] = id.sessionID.toString();
227                 });
228         if (sessionId[0] != null && !sessionId[0].isEmpty()) {
229             updateStatus(ThingStatus.ONLINE);
230             return sessionId[0];
231         } else {
232             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "SessionId not received");
233             return null;
234         }
235     }
236
237     public @Nullable String logout(String sessionId) {
238         String contentString = "{\"command\":\"logout\",\"data\":{\"sessionID\":\"" + sessionId + "\"}}";
239         String response = api.postRequest(config.ipAddress, contentString);
240         return response;
241     }
242
243     public void populateProperties() {
244         postRequest("{\"command\":\"getinfo\",\"data\":\"\"}", resp -> {
245             Data data = gson.fromJson(resp.data, Data.class);
246             Info info = gson.fromJson(data.info, Info.class);
247             properties.put(PROPERTY_VENDOR, "Intesis");
248             properties.put(PROPERTY_MODEL_ID, info.deviceModel);
249             properties.put(PROPERTY_SERIAL_NUMBER, info.sn);
250             properties.put(PROPERTY_FIRMWARE_VERSION, info.fwVersion);
251             properties.put(PROPERTY_MAC_ADDRESS, info.wlanSTAMAC);
252             updateStatus(ThingStatus.ONLINE);
253         });
254     }
255
256     public void addChannel(String channelId, String itemType, @Nullable final Collection<String> options) {
257         if (thing.getChannel(channelId) == null) {
258             logger.trace("Channel '{}' for UID to be added", channelId);
259             ThingBuilder thingBuilder = editThing();
260             final ChannelTypeUID channelTypeUID = new ChannelTypeUID(BINDING_ID, channelId);
261             Channel channel = ChannelBuilder.create(new ChannelUID(getThing().getUID(), channelId), itemType)
262                     .withType(channelTypeUID).withKind(ChannelKind.STATE).build();
263             thingBuilder.withChannel(channel);
264             updateThing(thingBuilder.build());
265         }
266         if (options != null) {
267             final List<StateOption> stateOptions = options.stream()
268                     .map(e -> new StateOption(e, e.substring(0, 1) + e.substring(1).toLowerCase()))
269                     .collect(Collectors.toList());
270             logger.trace("StateOptions : '{}'", stateOptions);
271             intesisStateDescriptionProvider.setStateOptions(new ChannelUID(getThing().getUID(), channelId),
272                     stateOptions);
273         }
274     }
275
276     private void postRequest(String request, Consumer<Response> handler) {
277         try {
278             logger.trace("request : '{}'", request);
279             String response = api.postRequest(config.ipAddress, request);
280             if (response != null) {
281                 Response resp = gson.fromJson(response, Response.class);
282                 boolean success = resp.success;
283                 if (success) {
284                     handler.accept(resp);
285                 } else {
286                     updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Request unsuccessful");
287                 }
288             } else {
289                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "No Response");
290             }
291         } catch (JsonSyntaxException e) {
292             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
293         }
294     }
295
296     private void postRequestInSession(UnaryOperator<String> requestFactory, Consumer<Response> handler) {
297         String sessionId = login();
298         if (sessionId != null) {
299             try {
300                 String request = requestFactory.apply(sessionId);
301                 postRequest(request, handler);
302             } finally {
303                 logout(sessionId);
304             }
305         }
306     }
307
308     private void handleDataPointsResponse(Response response) {
309         try {
310             Data data = gson.fromJson(response.data, Data.class);
311             Dp dp = gson.fromJson(data.dp, Dp.class);
312             Datapoints[] datapoints = gson.fromJson(dp.datapoints, Datapoints[].class);
313             for (Datapoints datapoint : datapoints) {
314                 Descr descr = gson.fromJson(datapoint.descr, Descr.class);
315                 String channelId = "";
316                 String itemType = "String";
317                 switch (datapoint.uid) {
318                     case 2:
319                         List<String> opModes = new ArrayList<>();
320                         for (String modString : descr.states) {
321                             switch (modString) {
322                                 case "0":
323                                     opModes.add("AUTO");
324                                     break;
325                                 case "1":
326                                     opModes.add("HEAT");
327                                     break;
328                                 case "2":
329                                     opModes.add("DRY");
330                                     break;
331                                 case "3":
332                                     opModes.add("FAN");
333                                     break;
334                                 case "4":
335                                     opModes.add("COOL");
336                                     break;
337                             }
338                         }
339                         properties.put("supported modes", opModes.toString());
340                         channelId = CHANNEL_TYPE_MODE;
341                         addChannel(channelId, itemType, opModes);
342                         break;
343                     case 4:
344                         List<String> fanLevels = new ArrayList<>();
345                         for (String fanString : descr.states) {
346                             if ("AUTO".contentEquals(fanString)) {
347                                 fanLevels.add("AUTO");
348                             } else {
349                                 fanLevels.add(fanString);
350                             }
351                         }
352                         properties.put("supported fan levels", fanLevels.toString());
353                         channelId = CHANNEL_TYPE_FANSPEED;
354                         addChannel(channelId, itemType, fanLevels);
355                         break;
356                     case 5:
357                     case 6:
358                         List<String> swingModes = new ArrayList<>();
359                         for (String swingString : descr.states) {
360                             if ("AUTO".contentEquals(swingString)) {
361                                 swingModes.add("AUTO");
362                             } else if ("10".contentEquals(swingString)) {
363                                 swingModes.add("SWING");
364                             } else if ("11".contentEquals(swingString)) {
365                                 swingModes.add("SWIRL");
366                             } else if ("12".contentEquals(swingString)) {
367                                 swingModes.add("WIDE");
368                             } else {
369                                 swingModes.add(swingString);
370                             }
371                         }
372                         switch (datapoint.uid) {
373                             case 5:
374                                 channelId = CHANNEL_TYPE_VANESUD;
375                                 properties.put("supported vane up/down modes", swingModes.toString());
376                                 addChannel(channelId, itemType, swingModes);
377                                 break;
378                             case 6:
379                                 channelId = CHANNEL_TYPE_VANESLR;
380                                 properties.put("supported vane left/right modes", swingModes.toString());
381                                 addChannel(channelId, itemType, swingModes);
382                                 break;
383                         }
384                         break;
385                     case 9:
386                         channelId = CHANNEL_TYPE_TARGETTEMP;
387                         itemType = "Number:Temperature";
388                         addChannel(channelId, itemType, null);
389                         break;
390                     case 10:
391                         channelId = CHANNEL_TYPE_AMBIENTTEMP;
392                         itemType = "Number:Temperature";
393                         addChannel(channelId, itemType, null);
394                         break;
395                     case 14:
396                         channelId = CHANNEL_TYPE_ERRORSTATUS;
397                         itemType = "Switch";
398                         addChannel(channelId, itemType, null);
399                         break;
400                     case 15:
401                         channelId = CHANNEL_TYPE_ERRORCODE;
402                         itemType = "String";
403                         addChannel(channelId, itemType, null);
404                         break;
405                     case 37:
406                         channelId = CHANNEL_TYPE_OUTDOORTEMP;
407                         itemType = "Number:Temperature";
408                         addChannel(channelId, itemType, null);
409                         break;
410                 }
411             }
412         } catch (JsonSyntaxException e) {
413             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
414         }
415         logger.trace("Start Refresh Job");
416         refreshJob = scheduler.scheduleWithFixedDelay(this::getAllUidValues, 0, INTESIS_REFRESH_INTERVAL_SEC,
417                 TimeUnit.SECONDS);
418     }
419
420     /**
421      * Update device status and all channels
422      */
423     private void getAllUidValues() {
424         postRequestInSession(sessionId -> "{\"command\":\"getdatapointvalue\",\"data\":{\"sessionID\":\"" + sessionId
425                 + "\", \"uid\":\"all\"}}", this::handleDataPointValues);
426     }
427
428     private void handleDataPointValues(Response response) {
429         try {
430             Data data = gson.fromJson(response.data, Data.class);
431             Dpval[] dpval = gson.fromJson(data.dpval, Dpval[].class);
432             for (Dpval element : dpval) {
433                 logger.trace("UID : {} ; value : {}", element.uid, element.value);
434                 switch (element.uid) {
435                     case 1:
436                         updateState(CHANNEL_TYPE_POWER,
437                                 String.valueOf(element.value).equals("0") ? OnOffType.OFF : OnOffType.ON);
438                         break;
439                     case 2:
440                         switch (element.value) {
441                             case 0:
442                                 updateState(CHANNEL_TYPE_MODE, StringType.valueOf("AUTO"));
443                                 break;
444                             case 1:
445                                 updateState(CHANNEL_TYPE_MODE, StringType.valueOf("HEAT"));
446                                 break;
447                             case 2:
448                                 updateState(CHANNEL_TYPE_MODE, StringType.valueOf("DRY"));
449                                 break;
450                             case 3:
451                                 updateState(CHANNEL_TYPE_MODE, StringType.valueOf("FAN"));
452                                 break;
453                             case 4:
454                                 updateState(CHANNEL_TYPE_MODE, StringType.valueOf("COOL"));
455                                 break;
456                         }
457                         break;
458                     case 4:
459                         if ((element.value) == 0) {
460                             updateState(CHANNEL_TYPE_FANSPEED, StringType.valueOf("AUTO"));
461                         } else {
462                             updateState(CHANNEL_TYPE_FANSPEED, StringType.valueOf(String.valueOf(element.value)));
463                         }
464                         break;
465                     case 5:
466                     case 6:
467                         State state;
468                         if ((element.value) == 0) {
469                             state = StringType.valueOf("AUTO");
470                         } else if ((element.value) == 10) {
471                             state = StringType.valueOf("SWING");
472                         } else if ((element.value) == 11) {
473                             state = StringType.valueOf("SWIRL");
474                         } else if ((element.value) == 12) {
475                             state = StringType.valueOf("WIDE");
476                         } else {
477                             state = StringType.valueOf(String.valueOf(element.value));
478                         }
479                         switch (element.uid) {
480                             case 5:
481                                 updateState(CHANNEL_TYPE_VANESUD, state);
482                                 break;
483                             case 6:
484                                 updateState(CHANNEL_TYPE_VANESLR, state);
485                                 break;
486                         }
487                         break;
488                     case 9:
489                         int unit = Math.round((element.value) / 10);
490                         State stateValue = QuantityType.valueOf(unit, SIUnits.CELSIUS);
491                         updateState(CHANNEL_TYPE_TARGETTEMP, stateValue);
492                         break;
493                     case 10:
494                         unit = Math.round((element.value) / 10);
495                         stateValue = QuantityType.valueOf(unit, SIUnits.CELSIUS);
496                         updateState(CHANNEL_TYPE_AMBIENTTEMP, stateValue);
497                         break;
498                     case 14:
499                         updateState(CHANNEL_TYPE_ERRORSTATUS,
500                                 String.valueOf(element.value).equals("0") ? OnOffType.OFF : OnOffType.ON);
501                         break;
502                     case 15:
503                         updateState(CHANNEL_TYPE_ERRORCODE, StringType.valueOf(String.valueOf(element.value)));
504                         break;
505                     case 37:
506                         unit = Math.round((element.value) / 10);
507                         stateValue = QuantityType.valueOf(unit, SIUnits.CELSIUS);
508                         updateState(CHANNEL_TYPE_OUTDOORTEMP, stateValue);
509                         break;
510                 }
511             }
512         } catch (JsonSyntaxException e) {
513             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
514         }
515     }
516 }