2 * Copyright (c) 2010-2021 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.OnOffType;
45 import org.openhab.core.library.types.QuantityType;
46 import org.openhab.core.library.types.StringType;
47 import org.openhab.core.library.unit.SIUnits;
48 import org.openhab.core.thing.Channel;
49 import org.openhab.core.thing.ChannelUID;
50 import org.openhab.core.thing.Thing;
51 import org.openhab.core.thing.ThingStatus;
52 import org.openhab.core.thing.ThingStatusDetail;
53 import org.openhab.core.thing.binding.BaseThingHandler;
54 import org.openhab.core.thing.binding.builder.ChannelBuilder;
55 import org.openhab.core.thing.binding.builder.ThingBuilder;
56 import org.openhab.core.thing.type.ChannelKind;
57 import org.openhab.core.thing.type.ChannelTypeUID;
58 import org.openhab.core.types.Command;
59 import org.openhab.core.types.RefreshType;
60 import org.openhab.core.types.State;
61 import org.openhab.core.types.StateOption;
62 import org.slf4j.Logger;
63 import org.slf4j.LoggerFactory;
65 import com.google.gson.Gson;
66 import com.google.gson.JsonSyntaxException;
69 * The {@link IntesisHomeHandler} is responsible for handling commands, which are
70 * sent to one of the channels.
72 * @author Hans-Jörg Merk - Initial contribution
75 public class IntesisHomeHandler extends BaseThingHandler {
77 private final Logger logger = LoggerFactory.getLogger(IntesisHomeHandler.class);
78 private final IntesisHomeHttpApi api;
80 private final Map<String, String> properties = new HashMap<>();
82 private final IntesisDynamicStateDescriptionProvider intesisStateDescriptionProvider;
84 private final Gson gson = new Gson();
86 private IntesisHomeConfiguration config = new IntesisHomeConfiguration();
88 private @Nullable ScheduledFuture<?> refreshJob;
90 public IntesisHomeHandler(final Thing thing, final HttpClient httpClient,
91 IntesisDynamicStateDescriptionProvider intesisStateDescriptionProvider) {
93 this.api = new IntesisHomeHttpApi(config, httpClient);
94 this.intesisStateDescriptionProvider = intesisStateDescriptionProvider;
98 public void initialize() {
99 updateStatus(ThingStatus.UNKNOWN);
100 config = getConfigAs(IntesisHomeConfiguration.class);
101 if (config.ipAddress.isEmpty() && config.password.isEmpty()) {
102 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "IP-Address and password not set");
104 } else if (config.ipAddress.isEmpty()) {
105 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "IP-Address not set");
107 } else if (config.password.isEmpty()) {
108 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Password not set");
111 // start background initialization:
112 scheduler.submit(() -> {
113 populateProperties();
114 // query available dataPoints and build dynamic channels
115 postRequestInSession(sessionId -> "{\"command\":\"getavailabledatapoints\",\"data\":{\"sessionID\":\""
116 + sessionId + "\"}}", this::handleDataPointsResponse);
117 updateProperties(properties);
123 public void dispose() {
124 logger.debug("IntesisHomeHandler disposed.");
125 final ScheduledFuture<?> refreshJob = this.refreshJob;
127 if (refreshJob != null) {
128 refreshJob.cancel(true);
129 this.refreshJob = null;
134 public void handleCommand(ChannelUID channelUID, Command command) {
137 String channelId = channelUID.getId();
138 if (command instanceof RefreshType) {
139 // Refresh command is not supported as the binding polls all values every 30 seconds
142 case CHANNEL_TYPE_POWER:
144 value = command.equals(OnOffType.OFF) ? 0 : 1;
146 case CHANNEL_TYPE_MODE:
148 value = IntesisHomeModeEnum.valueOf(command.toString()).getMode();
150 case CHANNEL_TYPE_FANSPEED:
152 if (("AUTO").equals(command.toString())) {
155 value = Integer.parseInt(command.toString());
158 case CHANNEL_TYPE_VANESUD:
159 case CHANNEL_TYPE_VANESLR:
160 switch (command.toString()) {
173 value = Integer.parseInt(command.toString());
186 case CHANNEL_TYPE_VANESUD:
189 case CHANNEL_TYPE_VANESLR:
194 case CHANNEL_TYPE_TARGETTEMP:
196 if (command instanceof QuantityType) {
197 QuantityType<?> newVal = (QuantityType<?>) command;
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);
225 Id id = gson.fromJson(data.id, Id.class);
226 sessionId[0] = id.sessionID.toString();
228 if (sessionId[0] != null && !sessionId[0].isEmpty()) {
229 updateStatus(ThingStatus.ONLINE);
232 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "SessionId not received");
237 public @Nullable String logout(String sessionId) {
238 String contentString = "{\"command\":\"logout\",\"data\":{\"sessionID\":\"" + sessionId + "\"}}";
239 String response = api.postRequest(config.ipAddress, contentString);
243 public void populateProperties() {
244 postRequest("{\"command\":\"getinfo\",\"data\":\"\"}", resp -> {
245 Data data = gson.fromJson(resp.data, Data.class);
246 Info info = gson.fromJson(data.info, Info.class);
247 properties.put(PROPERTY_VENDOR, "Intesis");
248 properties.put(PROPERTY_MODEL_ID, info.deviceModel);
249 properties.put(PROPERTY_SERIAL_NUMBER, info.sn);
250 properties.put(PROPERTY_FIRMWARE_VERSION, info.fwVersion);
251 properties.put(PROPERTY_MAC_ADDRESS, info.wlanSTAMAC);
252 updateStatus(ThingStatus.ONLINE);
256 public void addChannel(String channelId, String itemType, @Nullable final Collection<String> options) {
257 if (thing.getChannel(channelId) == null) {
258 logger.trace("Channel '{}' for UID to be added", channelId);
259 ThingBuilder thingBuilder = editThing();
260 final ChannelTypeUID channelTypeUID = new ChannelTypeUID(BINDING_ID, channelId);
261 Channel channel = ChannelBuilder.create(new ChannelUID(getThing().getUID(), channelId), itemType)
262 .withType(channelTypeUID).withKind(ChannelKind.STATE).build();
263 thingBuilder.withChannel(channel);
264 updateThing(thingBuilder.build());
266 if (options != null) {
267 final List<StateOption> stateOptions = options.stream()
268 .map(e -> new StateOption(e, e.substring(0, 1) + e.substring(1).toLowerCase()))
269 .collect(Collectors.toList());
270 logger.trace("StateOptions : '{}'", stateOptions);
271 intesisStateDescriptionProvider.setStateOptions(new ChannelUID(getThing().getUID(), channelId),
276 private void postRequest(String request, Consumer<Response> handler) {
278 logger.trace("request : '{}'", request);
279 String response = api.postRequest(config.ipAddress, request);
280 if (response != null) {
281 Response resp = gson.fromJson(response, Response.class);
282 boolean success = resp.success;
284 handler.accept(resp);
286 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Request unsuccessful");
289 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "No Response");
291 } catch (JsonSyntaxException e) {
292 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
296 private void postRequestInSession(UnaryOperator<String> requestFactory, Consumer<Response> handler) {
297 String sessionId = login();
298 if (sessionId != null) {
300 String request = requestFactory.apply(sessionId);
301 postRequest(request, handler);
308 private void handleDataPointsResponse(Response response) {
310 Data data = gson.fromJson(response.data, Data.class);
311 Dp dp = gson.fromJson(data.dp, Dp.class);
312 Datapoints[] datapoints = gson.fromJson(dp.datapoints, Datapoints[].class);
313 for (Datapoints datapoint : datapoints) {
314 Descr descr = gson.fromJson(datapoint.descr, Descr.class);
315 String channelId = "";
316 String itemType = "String";
317 switch (datapoint.uid) {
319 List<String> opModes = new ArrayList<>();
320 for (String modString : descr.states) {
339 properties.put("supported modes", opModes.toString());
340 channelId = CHANNEL_TYPE_MODE;
341 addChannel(channelId, itemType, opModes);
344 List<String> fanLevels = new ArrayList<>();
345 for (String fanString : descr.states) {
346 if ("AUTO".contentEquals(fanString)) {
347 fanLevels.add("AUTO");
349 fanLevels.add(fanString);
352 properties.put("supported fan levels", fanLevels.toString());
353 channelId = CHANNEL_TYPE_FANSPEED;
354 addChannel(channelId, itemType, fanLevels);
358 List<String> swingModes = new ArrayList<>();
359 for (String swingString : descr.states) {
360 if ("AUTO".contentEquals(swingString)) {
361 swingModes.add("AUTO");
362 } else if ("10".contentEquals(swingString)) {
363 swingModes.add("SWING");
364 } else if ("11".contentEquals(swingString)) {
365 swingModes.add("SWIRL");
366 } else if ("12".contentEquals(swingString)) {
367 swingModes.add("WIDE");
369 swingModes.add(swingString);
372 switch (datapoint.uid) {
374 channelId = CHANNEL_TYPE_VANESUD;
375 properties.put("supported vane up/down modes", swingModes.toString());
376 addChannel(channelId, itemType, swingModes);
379 channelId = CHANNEL_TYPE_VANESLR;
380 properties.put("supported vane left/right modes", swingModes.toString());
381 addChannel(channelId, itemType, swingModes);
386 channelId = CHANNEL_TYPE_TARGETTEMP;
387 itemType = "Number:Temperature";
388 addChannel(channelId, itemType, null);
391 channelId = CHANNEL_TYPE_AMBIENTTEMP;
392 itemType = "Number:Temperature";
393 addChannel(channelId, itemType, null);
396 channelId = CHANNEL_TYPE_ERRORSTATUS;
398 addChannel(channelId, itemType, null);
401 channelId = CHANNEL_TYPE_ERRORCODE;
403 addChannel(channelId, itemType, null);
406 channelId = CHANNEL_TYPE_OUTDOORTEMP;
407 itemType = "Number:Temperature";
408 addChannel(channelId, itemType, null);
412 } catch (JsonSyntaxException e) {
413 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
415 logger.trace("Start Refresh Job");
416 refreshJob = scheduler.scheduleWithFixedDelay(this::getAllUidValues, 0, INTESIS_REFRESH_INTERVAL_SEC,
421 * Update device status and all channels
423 private void getAllUidValues() {
424 postRequestInSession(sessionId -> "{\"command\":\"getdatapointvalue\",\"data\":{\"sessionID\":\"" + sessionId
425 + "\", \"uid\":\"all\"}}", this::handleDataPointValues);
428 private void handleDataPointValues(Response response) {
430 Data data = gson.fromJson(response.data, Data.class);
431 Dpval[] dpval = gson.fromJson(data.dpval, Dpval[].class);
432 for (Dpval element : dpval) {
433 logger.trace("UID : {} ; value : {}", element.uid, element.value);
434 switch (element.uid) {
436 updateState(CHANNEL_TYPE_POWER,
437 String.valueOf(element.value).equals("0") ? OnOffType.OFF : OnOffType.ON);
440 switch (element.value) {
442 updateState(CHANNEL_TYPE_MODE, StringType.valueOf("AUTO"));
445 updateState(CHANNEL_TYPE_MODE, StringType.valueOf("HEAT"));
448 updateState(CHANNEL_TYPE_MODE, StringType.valueOf("DRY"));
451 updateState(CHANNEL_TYPE_MODE, StringType.valueOf("FAN"));
454 updateState(CHANNEL_TYPE_MODE, StringType.valueOf("COOL"));
459 if ((element.value) == 0) {
460 updateState(CHANNEL_TYPE_FANSPEED, StringType.valueOf("AUTO"));
462 updateState(CHANNEL_TYPE_FANSPEED, StringType.valueOf(String.valueOf(element.value)));
468 if ((element.value) == 0) {
469 state = StringType.valueOf("AUTO");
470 } else if ((element.value) == 10) {
471 state = StringType.valueOf("SWING");
472 } else if ((element.value) == 11) {
473 state = StringType.valueOf("SWIRL");
474 } else if ((element.value) == 12) {
475 state = StringType.valueOf("WIDE");
477 state = StringType.valueOf(String.valueOf(element.value));
479 switch (element.uid) {
481 updateState(CHANNEL_TYPE_VANESUD, state);
484 updateState(CHANNEL_TYPE_VANESLR, state);
489 int unit = Math.round((element.value) / 10);
490 State stateValue = QuantityType.valueOf(unit, SIUnits.CELSIUS);
491 updateState(CHANNEL_TYPE_TARGETTEMP, stateValue);
494 unit = Math.round((element.value) / 10);
495 stateValue = QuantityType.valueOf(unit, SIUnits.CELSIUS);
496 updateState(CHANNEL_TYPE_AMBIENTTEMP, stateValue);
499 updateState(CHANNEL_TYPE_ERRORSTATUS,
500 String.valueOf(element.value).equals("0") ? OnOffType.OFF : OnOffType.ON);
503 updateState(CHANNEL_TYPE_ERRORCODE, StringType.valueOf(String.valueOf(element.value)));
506 unit = Math.round((element.value) / 10);
507 stateValue = QuantityType.valueOf(unit, SIUnits.CELSIUS);
508 updateState(CHANNEL_TYPE_OUTDOORTEMP, stateValue);
512 } catch (JsonSyntaxException e) {
513 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());