2 * Copyright (c) 2010-2024 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
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
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.intesis.internal.handler;
15 import static org.openhab.binding.intesis.internal.IntesisBindingConstants.*;
16 import static org.openhab.core.thing.Thing.*;
18 import java.util.ArrayList;
19 import java.util.Collection;
20 import java.util.HashMap;
21 import java.util.List;
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;
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;
66 import com.google.gson.Gson;
67 import com.google.gson.JsonSyntaxException;
70 * The {@link IntesisHomeHandler} is responsible for handling commands, which are
71 * sent to one of the channels.
73 * @author Hans-Jörg Merk - Initial contribution
76 public class IntesisHomeHandler extends BaseThingHandler {
78 private final Logger logger = LoggerFactory.getLogger(IntesisHomeHandler.class);
79 private final IntesisHomeHttpApi api;
81 private final Map<String, String> properties = new HashMap<>();
83 private final IntesisDynamicStateDescriptionProvider intesisStateDescriptionProvider;
85 private final Gson gson = new Gson();
87 private IntesisHomeConfiguration config = new IntesisHomeConfiguration();
89 private @Nullable ScheduledFuture<?> refreshJob;
91 public IntesisHomeHandler(final Thing thing, final HttpClient httpClient,
92 IntesisDynamicStateDescriptionProvider intesisStateDescriptionProvider) {
94 this.api = new IntesisHomeHttpApi(config, httpClient);
95 this.intesisStateDescriptionProvider = intesisStateDescriptionProvider;
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");
105 } else if (config.ipAddress.isEmpty()) {
106 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "IP-Address not set");
108 } else if (config.password.isEmpty()) {
109 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Password not set");
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);
124 public void dispose() {
125 logger.debug("IntesisHomeHandler disposed.");
126 final ScheduledFuture<?> refreshJob = this.refreshJob;
128 if (refreshJob != null) {
129 refreshJob.cancel(true);
130 this.refreshJob = null;
135 public void handleCommand(ChannelUID channelUID, Command command) {
138 String channelId = channelUID.getId();
139 if (command instanceof RefreshType) {
143 case CHANNEL_TYPE_POWER:
145 value = command.equals(OnOffType.OFF) ? 0 : 1;
147 case CHANNEL_TYPE_MODE:
149 value = IntesisHomeModeEnum.valueOf(command.toString()).getMode();
151 case CHANNEL_TYPE_FANSPEED:
153 if (("AUTO").equals(command.toString())) {
156 value = Integer.parseInt(command.toString());
159 case CHANNEL_TYPE_VANESUD:
160 case CHANNEL_TYPE_VANESLR:
161 switch (command.toString()) {
174 value = Integer.parseInt(command.toString());
187 case CHANNEL_TYPE_VANESUD:
190 case CHANNEL_TYPE_VANESLR:
195 case CHANNEL_TYPE_TARGETTEMP:
197 if (command instanceof QuantityType newVal) {
198 newVal = newVal.toUnit(SIUnits.CELSIUS);
199 if (newVal != null) {
200 value = newVal.intValue() * 10;
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));
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];
222 "{\"command\":\"login\",\"data\":{\"username\":\"Admin\",\"password\":\"" + config.password + "\"}}",
224 Data data = gson.fromJson(resp.data, Data.class);
226 Id id = gson.fromJson(data.id, Id.class);
228 sessionId[0] = id.sessionID;
232 if (sessionId[0] != null && !sessionId[0].isEmpty()) {
233 updateStatus(ThingStatus.ONLINE);
236 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "SessionId not received");
241 public @Nullable String logout(String sessionId) {
242 String contentString = "{\"command\":\"logout\",\"data\":{\"sessionID\":\"" + sessionId + "\"}}";
243 return api.postRequest(config.ipAddress, contentString);
246 public void populateProperties() {
247 postRequest("{\"command\":\"getinfo\",\"data\":\"\"}", resp -> {
248 Data data = gson.fromJson(resp.data, Data.class);
250 Info info = gson.fromJson(data.info, Info.class);
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);
263 public void getWiFiSignal() {
264 postRequest("{\"command\":\"getinfo\",\"data\":\"\"}", resp -> {
265 Data data = gson.fromJson(resp.data, Data.class);
267 Info info = gson.fromJson(data.info, Info.class);
269 String rssi = info.rssi;
270 int dbm = Integer.valueOf(rssi);
274 } else if (dbm > -70) {
276 } else if (dbm > -80) {
278 } else if (dbm > -90) {
283 DecimalType signalStrength = new DecimalType(strength);
284 updateState(CHANNEL_TYPE_RSSI, signalStrength);
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());
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),
311 private void postRequest(String request, Consumer<Response> handler) {
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);
318 boolean success = resp.success;
320 handler.accept(resp);
322 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
323 "Request unsuccessful");
327 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "No Response");
329 } catch (JsonSyntaxException e) {
330 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
334 private void postRequestInSession(UnaryOperator<String> requestFactory, Consumer<Response> handler) {
335 String sessionId = login();
336 if (sessionId != null) {
338 String request = requestFactory.apply(sessionId);
339 postRequest(request, handler);
346 private void handleDataPointsResponse(Response response) {
348 Data data = gson.fromJson(response.data, Data.class);
350 Dp dp = gson.fromJson(data.dp, Dp.class);
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) {
361 List<String> opModes = new ArrayList<>();
362 for (String modString : descr.states) {
380 properties.put("supported modes", opModes.toString());
381 channelId = CHANNEL_TYPE_MODE;
382 addChannel(channelId, itemType, opModes);
388 List<String> fanLevels = new ArrayList<>();
389 for (String fanString : descr.states) {
390 if ("AUTO".contentEquals(fanString)) {
391 fanLevels.add("AUTO");
393 fanLevels.add(fanString);
396 properties.put("supported fan levels", fanLevels.toString());
397 channelId = CHANNEL_TYPE_FANSPEED;
398 addChannel(channelId, itemType, fanLevels);
403 List<String> swingModes = new ArrayList<>();
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");
415 swingModes.add(swingString);
420 switch (datapoint.uid) {
422 channelId = CHANNEL_TYPE_VANESUD;
423 properties.put("supported vane up/down modes", swingModes.toString());
424 addChannel(channelId, itemType, swingModes);
427 channelId = CHANNEL_TYPE_VANESLR;
428 properties.put("supported vane left/right modes", swingModes.toString());
429 addChannel(channelId, itemType, swingModes);
434 channelId = CHANNEL_TYPE_TARGETTEMP;
435 itemType = "Number:Temperature";
436 addChannel(channelId, itemType, null);
439 channelId = CHANNEL_TYPE_AMBIENTTEMP;
440 itemType = "Number:Temperature";
441 addChannel(channelId, itemType, null);
444 channelId = CHANNEL_TYPE_ERRORSTATUS;
446 addChannel(channelId, itemType, null);
449 channelId = CHANNEL_TYPE_ERRORCODE;
451 addChannel(channelId, itemType, null);
454 channelId = CHANNEL_TYPE_OUTDOORTEMP;
455 itemType = "Number:Temperature";
456 addChannel(channelId, itemType, null);
463 } catch (JsonSyntaxException e) {
464 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
466 logger.trace("Start Refresh Job");
467 refreshJob = scheduler.scheduleWithFixedDelay(this::getAllUidValues, 0, config.pollingInterval,
472 * Update device status and all channels
474 private void getAllUidValues() {
475 postRequestInSession(sessionId -> "{\"command\":\"getdatapointvalue\",\"data\":{\"sessionID\":\"" + sessionId
476 + "\", \"uid\":\"all\"}}", this::handleDataPointValues);
480 private void handleDataPointValues(Response response) {
482 Data data = gson.fromJson(response.data, Data.class);
484 Dpval[] dpval = gson.fromJson(data.dpval, Dpval[].class);
486 for (Dpval element : dpval) {
487 logger.trace("UID : {} ; value : {}", element.uid, element.value);
488 switch (element.uid) {
490 updateState(CHANNEL_TYPE_POWER,
491 OnOffType.from(!"0".equals(String.valueOf(element.value))));
494 switch (element.value) {
496 updateState(CHANNEL_TYPE_MODE, StringType.valueOf("AUTO"));
499 updateState(CHANNEL_TYPE_MODE, StringType.valueOf("HEAT"));
502 updateState(CHANNEL_TYPE_MODE, StringType.valueOf("DRY"));
505 updateState(CHANNEL_TYPE_MODE, StringType.valueOf("FAN"));
508 updateState(CHANNEL_TYPE_MODE, StringType.valueOf("COOL"));
513 if ((element.value) == 0) {
514 updateState(CHANNEL_TYPE_FANSPEED, StringType.valueOf("AUTO"));
516 updateState(CHANNEL_TYPE_FANSPEED,
517 StringType.valueOf(String.valueOf(element.value)));
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");
532 state = StringType.valueOf(String.valueOf(element.value));
534 switch (element.uid) {
536 updateState(CHANNEL_TYPE_VANESUD, state);
539 updateState(CHANNEL_TYPE_VANESLR, state);
544 int unit = Math.round((element.value) / 10);
545 State stateValue = QuantityType.valueOf(unit, SIUnits.CELSIUS);
546 updateState(CHANNEL_TYPE_TARGETTEMP, stateValue);
549 unit = Math.round((element.value) / 10);
550 stateValue = QuantityType.valueOf(unit, SIUnits.CELSIUS);
551 updateState(CHANNEL_TYPE_AMBIENTTEMP, stateValue);
554 updateState(CHANNEL_TYPE_ERRORSTATUS,
555 OnOffType.from(!"0".equals(String.valueOf(element.value))));
558 updateState(CHANNEL_TYPE_ERRORCODE, StringType.valueOf(String.valueOf(element.value)));
561 unit = Math.round((element.value) / 10);
562 stateValue = QuantityType.valueOf(unit, SIUnits.CELSIUS);
563 updateState(CHANNEL_TYPE_OUTDOORTEMP, stateValue);
569 } catch (JsonSyntaxException e) {
570 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());