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.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;
67 import com.google.gson.Gson;
68 import com.google.gson.JsonSyntaxException;
71 * The {@link IntesisHomeHandler} is responsible for handling commands, which are
72 * sent to one of the channels.
74 * @author Hans-Jörg Merk - Initial contribution
77 public class IntesisHomeHandler extends BaseThingHandler {
79 private final Logger logger = LoggerFactory.getLogger(IntesisHomeHandler.class);
80 private final IntesisHomeHttpApi api;
82 private final Map<String, String> properties = new HashMap<>();
84 private final IntesisDynamicStateDescriptionProvider intesisStateDescriptionProvider;
86 private final Gson gson = new Gson();
88 private IntesisHomeConfiguration config = new IntesisHomeConfiguration();
90 private String sessionId = "";
92 private @Nullable ScheduledFuture<?> refreshJob;
94 public IntesisHomeHandler(final Thing thing, final HttpClient httpClient,
95 IntesisDynamicStateDescriptionProvider intesisStateDescriptionProvider) {
97 this.api = new IntesisHomeHttpApi(config, httpClient);
98 this.intesisStateDescriptionProvider = intesisStateDescriptionProvider;
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");
108 } else if (config.ipAddress.isEmpty()) {
109 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "IP-Address not set");
111 } else if (config.password.isEmpty()) {
112 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Password not set");
115 logger.trace("trying to log in - current session ID: {}", sessionId);
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);
130 public void dispose() {
131 logger.debug("IntesisHomeHandler disposed.");
132 final ScheduledFuture<?> refreshJob = this.refreshJob;
134 if (refreshJob != null) {
135 refreshJob.cancel(true);
136 this.refreshJob = null;
143 public void handleCommand(ChannelUID channelUID, Command command) {
146 String channelId = channelUID.getId();
147 if (command instanceof RefreshType) {
151 case CHANNEL_TYPE_POWER:
153 value = command.equals(OnOffType.OFF) ? 0 : 1;
155 case CHANNEL_TYPE_MODE:
157 value = IntesisHomeModeEnum.valueOf(command.toString()).getMode();
159 case CHANNEL_TYPE_FANSPEED:
161 if (("AUTO").equals(command.toString())) {
164 value = Integer.parseInt(command.toString());
167 case CHANNEL_TYPE_VANESUD:
168 case CHANNEL_TYPE_VANESLR:
169 switch (command.toString()) {
182 value = Integer.parseInt(command.toString());
195 case CHANNEL_TYPE_VANESUD:
198 case CHANNEL_TYPE_VANESLR:
203 case CHANNEL_TYPE_TARGETTEMP:
205 if (command instanceof QuantityType newVal) {
206 newVal = newVal.toUnit(SIUnits.CELSIUS);
207 if (newVal != null) {
208 value = newVal.intValue() * 10;
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));
226 public @Nullable String login() {
228 "{\"command\":\"login\",\"data\":{\"username\":\"Admin\",\"password\":\"" + config.password + "\"}}",
230 Data data = gson.fromJson(resp.data, Data.class);
231 ResponseError error = gson.fromJson(resp.error, ResponseError.class);
233 logger.debug("Login - Error: {}", error);
236 Id id = gson.fromJson(data.id, Id.class);
238 sessionId = id.sessionID.toString();
242 logger.trace("Login - received session ID: {}", sessionId);
243 if (sessionId != null && !sessionId.isEmpty()) {
244 updateStatus(ThingStatus.ONLINE);
246 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "SessionId not received");
252 public @Nullable String logout(String sessionId) {
253 String contentString = "{\"command\":\"logout\",\"data\":{\"sessionID\":\"" + sessionId + "\"}}";
254 return api.postRequest(config.ipAddress, contentString);
257 public void populateProperties() {
258 postRequest("{\"command\":\"getinfo\",\"data\":\"\"}", resp -> {
259 Data data = gson.fromJson(resp.data, Data.class);
261 Info info = gson.fromJson(data.info, Info.class);
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);
274 public void getWiFiSignal() {
275 postRequest("{\"command\":\"getinfo\",\"data\":\"\"}", resp -> {
276 Data data = gson.fromJson(resp.data, Data.class);
278 Info info = gson.fromJson(data.info, Info.class);
280 String rssi = info.rssi;
281 int dbm = Integer.valueOf(rssi);
285 } else if (dbm > -70) {
287 } else if (dbm > -80) {
289 } else if (dbm > -90) {
294 DecimalType signalStrength = new DecimalType(strength);
295 updateState(CHANNEL_TYPE_RSSI, signalStrength);
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());
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),
322 private void postRequest(String request, Consumer<Response> handler) {
323 postRequest(request, handler, false);
326 private void postRequest(String request, Consumer<Response> handler, boolean retry) {
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);
334 handler.accept(resp);
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) {
344 "postRequest: trying to log in and retry request - respErrorCode: {} / respErrorMessage: {} / retry {}",
345 respError.code, respError.message, retry);
347 postRequest(request, handler, true);
354 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "No Response");
356 } catch (JsonSyntaxException e) {
357 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
361 private void postRequestInSession(UnaryOperator<String> requestFactory, Consumer<Response> handler) {
362 if (sessionId != null) {
364 String request = requestFactory.apply(sessionId);
365 postRequest(request, handler);
371 private void handleDataPointsResponse(Response response) {
373 Data data = gson.fromJson(response.data, Data.class);
375 Dp dp = gson.fromJson(data.dp, Dp.class);
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) {
386 List<String> opModes = new ArrayList<>();
387 for (String modString : descr.states) {
405 properties.put("supported modes", opModes.toString());
406 channelId = CHANNEL_TYPE_MODE;
407 addChannel(channelId, itemType, opModes);
413 List<String> fanLevels = new ArrayList<>();
414 for (String fanString : descr.states) {
415 if ("AUTO".contentEquals(fanString)) {
416 fanLevels.add("AUTO");
418 fanLevels.add(fanString);
421 properties.put("supported fan levels", fanLevels.toString());
422 channelId = CHANNEL_TYPE_FANSPEED;
423 addChannel(channelId, itemType, fanLevels);
428 List<String> swingModes = new ArrayList<>();
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");
440 swingModes.add(swingString);
445 switch (datapoint.uid) {
447 channelId = CHANNEL_TYPE_VANESUD;
448 properties.put("supported vane up/down modes", swingModes.toString());
449 addChannel(channelId, itemType, swingModes);
452 channelId = CHANNEL_TYPE_VANESLR;
453 properties.put("supported vane left/right modes", swingModes.toString());
454 addChannel(channelId, itemType, swingModes);
459 channelId = CHANNEL_TYPE_TARGETTEMP;
460 itemType = "Number:Temperature";
461 addChannel(channelId, itemType, null);
464 channelId = CHANNEL_TYPE_AMBIENTTEMP;
465 itemType = "Number:Temperature";
466 addChannel(channelId, itemType, null);
469 channelId = CHANNEL_TYPE_ERRORSTATUS;
471 addChannel(channelId, itemType, null);
474 channelId = CHANNEL_TYPE_ERRORCODE;
476 addChannel(channelId, itemType, null);
479 channelId = CHANNEL_TYPE_OUTDOORTEMP;
480 itemType = "Number:Temperature";
481 addChannel(channelId, itemType, null);
488 } catch (JsonSyntaxException e) {
489 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
491 logger.trace("Start Refresh Job");
492 refreshJob = scheduler.scheduleWithFixedDelay(this::getAllUidValues, 0, config.pollingInterval,
497 * Update device status and all channels
499 private void getAllUidValues() {
500 postRequestInSession(sessionId -> "{\"command\":\"getdatapointvalue\",\"data\":{\"sessionID\":\"" + sessionId
501 + "\", \"uid\":\"all\"}}", this::handleDataPointValues);
505 private void handleDataPointValues(Response response) {
507 Data data = gson.fromJson(response.data, Data.class);
509 Dpval[] dpval = gson.fromJson(data.dpval, Dpval[].class);
511 for (Dpval element : dpval) {
512 logger.trace("UID : {} ; value : {}", element.uid, element.value);
513 switch (element.uid) {
515 updateState(CHANNEL_TYPE_POWER,
516 OnOffType.from(!"0".equals(String.valueOf(element.value))));
519 switch (element.value) {
521 updateState(CHANNEL_TYPE_MODE, StringType.valueOf("AUTO"));
524 updateState(CHANNEL_TYPE_MODE, StringType.valueOf("HEAT"));
527 updateState(CHANNEL_TYPE_MODE, StringType.valueOf("DRY"));
530 updateState(CHANNEL_TYPE_MODE, StringType.valueOf("FAN"));
533 updateState(CHANNEL_TYPE_MODE, StringType.valueOf("COOL"));
538 if ((element.value) == 0) {
539 updateState(CHANNEL_TYPE_FANSPEED, StringType.valueOf("AUTO"));
541 updateState(CHANNEL_TYPE_FANSPEED,
542 StringType.valueOf(String.valueOf(element.value)));
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");
557 state = StringType.valueOf(String.valueOf(element.value));
559 switch (element.uid) {
561 updateState(CHANNEL_TYPE_VANESUD, state);
564 updateState(CHANNEL_TYPE_VANESLR, state);
569 int unit = Math.round((element.value) / 10);
570 State stateValue = QuantityType.valueOf(unit, SIUnits.CELSIUS);
571 updateState(CHANNEL_TYPE_TARGETTEMP, stateValue);
574 unit = Math.round((element.value) / 10);
575 stateValue = QuantityType.valueOf(unit, SIUnits.CELSIUS);
576 updateState(CHANNEL_TYPE_AMBIENTTEMP, stateValue);
579 updateState(CHANNEL_TYPE_ERRORSTATUS,
580 OnOffType.from(!"0".equals(String.valueOf(element.value))));
583 updateState(CHANNEL_TYPE_ERRORCODE, StringType.valueOf(String.valueOf(element.value)));
586 unit = Math.round((element.value) / 10);
587 stateValue = QuantityType.valueOf(unit, SIUnits.CELSIUS);
588 updateState(CHANNEL_TYPE_OUTDOORTEMP, stateValue);
594 } catch (JsonSyntaxException e) {
595 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());