2 * Copyright (c) 2010-2022 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.binding.intesis.internal.api.IntesisBoxMessage.*;
17 import static org.openhab.core.thing.Thing.*;
19 import java.io.IOException;
20 import java.util.ArrayList;
21 import java.util.HashMap;
22 import java.util.List;
24 import java.util.concurrent.ScheduledFuture;
25 import java.util.concurrent.TimeUnit;
26 import java.util.stream.Collectors;
28 import javax.measure.quantity.Temperature;
30 import org.eclipse.jdt.annotation.NonNullByDefault;
31 import org.eclipse.jdt.annotation.Nullable;
32 import org.openhab.binding.intesis.internal.IntesisDynamicStateDescriptionProvider;
33 import org.openhab.binding.intesis.internal.api.IntesisBoxChangeListener;
34 import org.openhab.binding.intesis.internal.api.IntesisBoxMessage;
35 import org.openhab.binding.intesis.internal.api.IntesisBoxSocketApi;
36 import org.openhab.binding.intesis.internal.config.IntesisBoxConfiguration;
37 import org.openhab.core.library.types.DecimalType;
38 import org.openhab.core.library.types.OnOffType;
39 import org.openhab.core.library.types.QuantityType;
40 import org.openhab.core.library.types.StringType;
41 import org.openhab.core.library.unit.SIUnits;
42 import org.openhab.core.thing.Channel;
43 import org.openhab.core.thing.ChannelUID;
44 import org.openhab.core.thing.Thing;
45 import org.openhab.core.thing.ThingStatus;
46 import org.openhab.core.thing.ThingStatusDetail;
47 import org.openhab.core.thing.binding.BaseThingHandler;
48 import org.openhab.core.thing.binding.builder.ChannelBuilder;
49 import org.openhab.core.thing.binding.builder.ThingBuilder;
50 import org.openhab.core.thing.type.ChannelKind;
51 import org.openhab.core.thing.type.ChannelTypeUID;
52 import org.openhab.core.types.Command;
53 import org.openhab.core.types.RefreshType;
54 import org.openhab.core.types.StateOption;
55 import org.slf4j.Logger;
56 import org.slf4j.LoggerFactory;
59 * The {@link IntesisBoxHandler} is responsible for handling commands, which are
60 * sent to one of the channels.
62 * @author Cody Cutrer - Initial contribution
63 * @author Rocky Amatulli - additions to include id message handling, dynamic channel options based on limits.
64 * @author Hans-Jörg Merk - refactored for openHAB 3.0 compatibility
68 public class IntesisBoxHandler extends BaseThingHandler implements IntesisBoxChangeListener {
70 private final Logger logger = LoggerFactory.getLogger(IntesisBoxHandler.class);
71 private @Nullable IntesisBoxSocketApi intesisBoxSocketApi;
73 private final Map<String, String> properties = new HashMap<>();
74 private final Map<String, List<String>> limits = new HashMap<>();
76 private final IntesisDynamicStateDescriptionProvider intesisStateDescriptionProvider;
78 private IntesisBoxConfiguration config = new IntesisBoxConfiguration();
80 private double minTemp = 0.0, maxTemp = 0.0;
82 private boolean hasProperties = false;
84 private @Nullable ScheduledFuture<?> pollingTask;
86 public IntesisBoxHandler(Thing thing, IntesisDynamicStateDescriptionProvider intesisStateDescriptionProvider) {
88 this.intesisStateDescriptionProvider = intesisStateDescriptionProvider;
92 public void initialize() {
93 config = getConfigAs(IntesisBoxConfiguration.class);
95 if (!config.ipAddress.isEmpty()) {
97 updateStatus(ThingStatus.UNKNOWN);
98 scheduler.submit(() -> {
100 String readerThreadName = "OH-binding-" + getThing().getUID().getAsString();
102 IntesisBoxSocketApi intesisLocalApi = intesisBoxSocketApi = new IntesisBoxSocketApi(config.ipAddress,
103 config.port, readerThreadName);
104 intesisLocalApi.addIntesisBoxChangeListener(this);
106 intesisLocalApi.openConnection();
107 intesisLocalApi.sendId();
108 intesisLocalApi.sendLimitsQuery();
109 intesisLocalApi.sendAlive();
111 } catch (IOException e) {
112 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
115 updateStatus(ThingStatus.ONLINE);
117 pollingTask = scheduler.scheduleWithFixedDelay(this::polling, 3, 45, TimeUnit.SECONDS);
119 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "No IP address specified)");
124 public void dispose() {
125 final ScheduledFuture<?> pollingTask = this.pollingTask;
127 IntesisBoxSocketApi api = this.intesisBoxSocketApi;
129 if (pollingTask != null) {
130 pollingTask.cancel(true);
131 this.pollingTask = null;
134 api.closeConnection();
135 api.removeIntesisBoxChangeListener(this);
140 private synchronized void polling() {
141 IntesisBoxSocketApi api = this.intesisBoxSocketApi;
143 if (!api.isConnected()) {
145 api.openConnection();
146 } catch (IOException e) {
147 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
156 public void handleCommand(ChannelUID channelUID, Command command) {
157 IntesisBoxSocketApi api = this.intesisBoxSocketApi;
159 if (!api.isConnected()) {
160 logger.trace("Sending command failed, not connected");
163 if (command instanceof RefreshType) {
164 logger.trace("Refresh channel {}", channelUID.getId());
165 api.sendQuery(channelUID.getId());
170 String function = "";
171 switch (channelUID.getId()) {
172 case CHANNEL_TYPE_POWER:
173 if (command instanceof OnOffType) {
175 value = command == OnOffType.ON ? "ON" : "OFF";
178 case CHANNEL_TYPE_TARGETTEMP:
179 if (command instanceof QuantityType) {
180 QuantityType<?> celsiusTemperature = (QuantityType<?>) command;
181 celsiusTemperature = celsiusTemperature.toUnit(SIUnits.CELSIUS);
182 if (celsiusTemperature != null) {
183 double doubleValue = celsiusTemperature.doubleValue();
184 logger.trace("targetTemp double value = {}", doubleValue);
185 doubleValue = Math.max(minTemp, Math.min(maxTemp, doubleValue));
186 value = String.format("%.0f", doubleValue * 10);
187 function = "SETPTEMP";
188 logger.trace("targetTemp raw string = {}", value);
192 case CHANNEL_TYPE_MODE:
194 value = command.toString();
196 case CHANNEL_TYPE_FANSPEED:
198 value = command.toString();
200 case CHANNEL_TYPE_VANESUD:
202 value = command.toString();
204 case CHANNEL_TYPE_VANESLR:
206 value = command.toString();
209 if (!value.isEmpty() || function.isEmpty()) {
211 logger.trace("Sending command {} to function {}", value, function);
212 api.sendCommand(function, value);
214 logger.warn("Sending command failed, could not get API");
219 private void populateProperties(String[] value) {
220 properties.put(PROPERTY_VENDOR, "Intesis");
221 properties.put(PROPERTY_MODEL_ID, value[0]);
222 properties.put(PROPERTY_MAC_ADDRESS, value[1]);
223 properties.put("ipAddress", value[2]);
224 properties.put("protocol", value[3]);
225 properties.put(PROPERTY_FIRMWARE_VERSION, value[4]);
226 properties.put("hostname", value[6]);
227 updateProperties(properties);
228 hasProperties = true;
231 private void receivedUpdate(String function, String receivedValue) {
232 String value = receivedValue;
233 logger.trace("receivedUpdate(): {} {}", function, value);
236 updateState(CHANNEL_TYPE_POWER, OnOffType.from(value));
240 if (value.equals("32768")) {
243 updateState(CHANNEL_TYPE_TARGETTEMP,
244 new QuantityType<Temperature>(Double.valueOf(value) / 10.0d, SIUnits.CELSIUS));
247 if (Double.valueOf(value).isNaN()) {
250 updateState(CHANNEL_TYPE_AMBIENTTEMP,
251 new QuantityType<Temperature>(Double.valueOf(value) / 10.0d, SIUnits.CELSIUS));
254 updateState(CHANNEL_TYPE_MODE, new StringType(value));
257 updateState(CHANNEL_TYPE_FANSPEED, new StringType(value));
260 updateState(CHANNEL_TYPE_VANESUD, new StringType(value));
263 updateState(CHANNEL_TYPE_VANESLR, new StringType(value));
266 updateState(CHANNEL_TYPE_ERRORCODE, new StringType(value));
269 updateState(CHANNEL_TYPE_ERRORSTATUS, new StringType(value));
270 if ("ERR".equals(value)) {
271 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
272 "device reported an error");
278 private void handleMessage(String data) {
279 logger.debug("handleMessage(): Message received - {}", data);
280 if (data.equals("ACK") || data.equals("")) {
283 if (data.startsWith(ID + ':')) {
284 String[] value = data.substring(3).split(",");
285 if (!hasProperties) {
286 populateProperties(value);
288 DecimalType signalStrength = mapSignalStrength(Integer.parseInt(value[5]));
289 updateState(CHANNEL_TYPE_RSSI, signalStrength);
292 IntesisBoxMessage message = IntesisBoxMessage.parse(data);
293 if (message != null) {
294 switch (message.getCommand()) {
296 logger.debug("handleMessage(): Limits received - {}", data);
297 String function = message.getFunction();
298 if (function.equals("SETPTEMP")) {
299 List<Double> limits = message.getLimitsValue().stream().map(l -> Double.valueOf(l) / 10.0d)
300 .collect(Collectors.toList());
301 if (limits.size() == 2) {
302 minTemp = limits.get(0);
303 maxTemp = limits.get(1);
305 logger.trace("Property target temperatures {} added", message.getValue());
306 properties.put("targetTemperature limits", "[" + minTemp + "," + maxTemp + "]");
307 addChannel(CHANNEL_TYPE_TARGETTEMP, "Number:Temperature");
311 properties.put("supported modes", message.getValue());
312 limits.put(CHANNEL_TYPE_MODE, message.getLimitsValue());
313 addChannel(CHANNEL_TYPE_MODE, "String");
316 properties.put("supported fan levels", message.getValue());
317 limits.put(CHANNEL_TYPE_FANSPEED, message.getLimitsValue());
318 addChannel(CHANNEL_TYPE_FANSPEED, "String");
321 properties.put("supported vane up/down modes", message.getValue());
322 limits.put(CHANNEL_TYPE_VANESUD, message.getLimitsValue());
323 addChannel(CHANNEL_TYPE_VANESUD, "String");
326 properties.put("supported vane left/right modes", message.getValue());
327 limits.put(CHANNEL_TYPE_VANESLR, message.getLimitsValue());
328 addChannel(CHANNEL_TYPE_VANESLR, "String");
332 updateProperties(properties);
335 receivedUpdate(message.getFunction(), message.getValue());
341 public void addChannel(String channelId, String itemType) {
342 if (thing.getChannel(channelId) == null) {
343 logger.trace("Channel '{}' for UID to be added", channelId);
344 ThingBuilder thingBuilder = editThing();
345 final ChannelTypeUID channelTypeUID = new ChannelTypeUID(BINDING_ID, channelId);
346 Channel channel = ChannelBuilder.create(new ChannelUID(getThing().getUID(), channelId), itemType)
347 .withType(channelTypeUID).withKind(ChannelKind.STATE).build();
348 thingBuilder.withChannel(channel);
349 updateThing(thingBuilder.build());
351 if (limits.containsKey(channelId)) {
352 List<StateOption> options = new ArrayList<>();
353 for (String mode : limits.get(channelId)) {
355 new StateOption(mode, mode.substring(0, 1).toUpperCase() + mode.substring(1).toLowerCase()));
357 intesisStateDescriptionProvider.setStateOptions(new ChannelUID(getThing().getUID(), channelId), options);
362 public void messageReceived(String messageLine) {
363 logger.trace("messageReceived() : {}", messageLine);
364 handleMessage(messageLine);
368 public void connectionStatusChanged(ThingStatus status, @Nullable String message) {
369 if (message != null) {
370 this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, message);
372 this.updateStatus(status);
375 public static DecimalType mapSignalStrength(int dbm) {
379 } else if (dbm > -70) {
381 } else if (dbm > -80) {
383 } else if (dbm > -90) {
388 return new DecimalType(strength);