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