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