2 * Copyright (c) 2010-2023 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) {
198 QuantityType<?> newVal = (QuantityType<?>) command;
199 newVal = newVal.toUnit(SIUnits.CELSIUS);
200 if (newVal != null) {
201 value = newVal.intValue() * 10;
209 final int newValue = value;
210 scheduler.submit(() -> {
211 postRequestInSession(
212 sessionId -> "{\"command\":\"setdatapointvalue\",\"data\":{\"sessionID\":\"" + sessionId
213 + "\", \"uid\":" + uId + ",\"value\":" + newValue + "}}",
214 r -> updateStatus(ThingStatus.ONLINE));
219 public @Nullable String login() {
220 // lambda's can't modify local variables, so we use an array here to get around the issue
221 String[] sessionId = new String[1];
223 "{\"command\":\"login\",\"data\":{\"username\":\"Admin\",\"password\":\"" + config.password + "\"}}",
225 Data data = gson.fromJson(resp.data, Data.class);
227 Id id = gson.fromJson(data.id, Id.class);
229 sessionId[0] = id.sessionID.toString();
233 if (sessionId[0] != null && !sessionId[0].isEmpty()) {
234 updateStatus(ThingStatus.ONLINE);
237 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "SessionId not received");
242 public @Nullable String logout(String sessionId) {
243 String contentString = "{\"command\":\"logout\",\"data\":{\"sessionID\":\"" + sessionId + "\"}}";
244 String response = api.postRequest(config.ipAddress, contentString);
248 public void populateProperties() {
249 postRequest("{\"command\":\"getinfo\",\"data\":\"\"}", resp -> {
250 Data data = gson.fromJson(resp.data, Data.class);
252 Info info = gson.fromJson(data.info, Info.class);
254 properties.put(PROPERTY_VENDOR, "Intesis");
255 properties.put(PROPERTY_MODEL_ID, info.deviceModel);
256 properties.put(PROPERTY_SERIAL_NUMBER, info.sn);
257 properties.put(PROPERTY_FIRMWARE_VERSION, info.fwVersion);
258 properties.put(PROPERTY_MAC_ADDRESS, info.wlanSTAMAC);
259 updateStatus(ThingStatus.ONLINE);
265 public void getWiFiSignal() {
266 postRequest("{\"command\":\"getinfo\",\"data\":\"\"}", resp -> {
267 Data data = gson.fromJson(resp.data, Data.class);
269 Info info = gson.fromJson(data.info, Info.class);
271 String rssi = info.rssi;
272 int dbm = Integer.valueOf(rssi);
276 } else if (dbm > -70) {
278 } else if (dbm > -80) {
280 } else if (dbm > -90) {
285 DecimalType signalStrength = new DecimalType(strength);
286 updateState(CHANNEL_TYPE_RSSI, signalStrength);
293 public void addChannel(String channelId, String itemType, @Nullable final Collection<String> options) {
294 if (thing.getChannel(channelId) == null) {
295 logger.trace("Channel '{}' for UID to be added", channelId);
296 ThingBuilder thingBuilder = editThing();
297 final ChannelTypeUID channelTypeUID = new ChannelTypeUID(BINDING_ID, channelId);
298 Channel channel = ChannelBuilder.create(new ChannelUID(getThing().getUID(), channelId), itemType)
299 .withType(channelTypeUID).withKind(ChannelKind.STATE).build();
300 thingBuilder.withChannel(channel);
301 updateThing(thingBuilder.build());
303 if (options != null) {
304 final List<StateOption> stateOptions = options.stream()
305 .map(e -> new StateOption(e, e.substring(0, 1) + e.substring(1).toLowerCase()))
306 .collect(Collectors.toList());
307 logger.trace("StateOptions : '{}'", stateOptions);
308 intesisStateDescriptionProvider.setStateOptions(new ChannelUID(getThing().getUID(), channelId),
313 private void postRequest(String request, Consumer<Response> handler) {
315 logger.trace("request : '{}'", request);
316 String response = api.postRequest(config.ipAddress, request);
317 if (response != null) {
318 Response resp = gson.fromJson(response, Response.class);
320 boolean success = resp.success;
322 handler.accept(resp);
324 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
325 "Request unsuccessful");
329 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "No Response");
331 } catch (JsonSyntaxException e) {
332 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
336 private void postRequestInSession(UnaryOperator<String> requestFactory, Consumer<Response> handler) {
337 String sessionId = login();
338 if (sessionId != null) {
340 String request = requestFactory.apply(sessionId);
341 postRequest(request, handler);
348 private void handleDataPointsResponse(Response response) {
350 Data data = gson.fromJson(response.data, Data.class);
352 Dp dp = gson.fromJson(data.dp, Dp.class);
354 Datapoints[] datapoints = gson.fromJson(dp.datapoints, Datapoints[].class);
355 if (datapoints != null) {
356 for (Datapoints datapoint : datapoints) {
357 Descr descr = gson.fromJson(datapoint.descr, Descr.class);
358 String channelId = "";
359 String itemType = "String";
360 switch (datapoint.uid) {
363 List<String> opModes = new ArrayList<>();
364 for (String modString : descr.states) {
382 properties.put("supported modes", opModes.toString());
383 channelId = CHANNEL_TYPE_MODE;
384 addChannel(channelId, itemType, opModes);
390 List<String> fanLevels = new ArrayList<>();
391 for (String fanString : descr.states) {
392 if ("AUTO".contentEquals(fanString)) {
393 fanLevels.add("AUTO");
395 fanLevels.add(fanString);
398 properties.put("supported fan levels", fanLevels.toString());
399 channelId = CHANNEL_TYPE_FANSPEED;
400 addChannel(channelId, itemType, fanLevels);
405 List<String> swingModes = new ArrayList<>();
407 for (String swingString : descr.states) {
408 if ("AUTO".contentEquals(swingString)) {
409 swingModes.add("AUTO");
410 } else if ("10".contentEquals(swingString)) {
411 swingModes.add("SWING");
412 } else if ("11".contentEquals(swingString)) {
413 swingModes.add("SWIRL");
414 } else if ("12".contentEquals(swingString)) {
415 swingModes.add("WIDE");
417 swingModes.add(swingString);
422 switch (datapoint.uid) {
424 channelId = CHANNEL_TYPE_VANESUD;
425 properties.put("supported vane up/down modes", swingModes.toString());
426 addChannel(channelId, itemType, swingModes);
429 channelId = CHANNEL_TYPE_VANESLR;
430 properties.put("supported vane left/right modes", swingModes.toString());
431 addChannel(channelId, itemType, swingModes);
436 channelId = CHANNEL_TYPE_TARGETTEMP;
437 itemType = "Number:Temperature";
438 addChannel(channelId, itemType, null);
441 channelId = CHANNEL_TYPE_AMBIENTTEMP;
442 itemType = "Number:Temperature";
443 addChannel(channelId, itemType, null);
446 channelId = CHANNEL_TYPE_ERRORSTATUS;
448 addChannel(channelId, itemType, null);
451 channelId = CHANNEL_TYPE_ERRORCODE;
453 addChannel(channelId, itemType, null);
456 channelId = CHANNEL_TYPE_OUTDOORTEMP;
457 itemType = "Number:Temperature";
458 addChannel(channelId, itemType, null);
465 } catch (JsonSyntaxException e) {
466 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
468 logger.trace("Start Refresh Job");
469 refreshJob = scheduler.scheduleWithFixedDelay(this::getAllUidValues, 0, config.pollingInterval,
474 * Update device status and all channels
476 private void getAllUidValues() {
477 postRequestInSession(sessionId -> "{\"command\":\"getdatapointvalue\",\"data\":{\"sessionID\":\"" + sessionId
478 + "\", \"uid\":\"all\"}}", this::handleDataPointValues);
482 private void handleDataPointValues(Response response) {
484 Data data = gson.fromJson(response.data, Data.class);
486 Dpval[] dpval = gson.fromJson(data.dpval, Dpval[].class);
488 for (Dpval element : dpval) {
489 logger.trace("UID : {} ; value : {}", element.uid, element.value);
490 switch (element.uid) {
492 updateState(CHANNEL_TYPE_POWER,
493 String.valueOf(element.value).equals("0") ? OnOffType.OFF : OnOffType.ON);
496 switch (element.value) {
498 updateState(CHANNEL_TYPE_MODE, StringType.valueOf("AUTO"));
501 updateState(CHANNEL_TYPE_MODE, StringType.valueOf("HEAT"));
504 updateState(CHANNEL_TYPE_MODE, StringType.valueOf("DRY"));
507 updateState(CHANNEL_TYPE_MODE, StringType.valueOf("FAN"));
510 updateState(CHANNEL_TYPE_MODE, StringType.valueOf("COOL"));
515 if ((element.value) == 0) {
516 updateState(CHANNEL_TYPE_FANSPEED, StringType.valueOf("AUTO"));
518 updateState(CHANNEL_TYPE_FANSPEED,
519 StringType.valueOf(String.valueOf(element.value)));
525 if ((element.value) == 0) {
526 state = StringType.valueOf("AUTO");
527 } else if ((element.value) == 10) {
528 state = StringType.valueOf("SWING");
529 } else if ((element.value) == 11) {
530 state = StringType.valueOf("SWIRL");
531 } else if ((element.value) == 12) {
532 state = StringType.valueOf("WIDE");
534 state = StringType.valueOf(String.valueOf(element.value));
536 switch (element.uid) {
538 updateState(CHANNEL_TYPE_VANESUD, state);
541 updateState(CHANNEL_TYPE_VANESLR, state);
546 int unit = Math.round((element.value) / 10);
547 State stateValue = QuantityType.valueOf(unit, SIUnits.CELSIUS);
548 updateState(CHANNEL_TYPE_TARGETTEMP, stateValue);
551 unit = Math.round((element.value) / 10);
552 stateValue = QuantityType.valueOf(unit, SIUnits.CELSIUS);
553 updateState(CHANNEL_TYPE_AMBIENTTEMP, stateValue);
556 updateState(CHANNEL_TYPE_ERRORSTATUS,
557 String.valueOf(element.value).equals("0") ? OnOffType.OFF : OnOffType.ON);
560 updateState(CHANNEL_TYPE_ERRORCODE, StringType.valueOf(String.valueOf(element.value)));
563 unit = Math.round((element.value) / 10);
564 stateValue = QuantityType.valueOf(unit, SIUnits.CELSIUS);
565 updateState(CHANNEL_TYPE_OUTDOORTEMP, stateValue);
571 } catch (JsonSyntaxException e) {
572 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());