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.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()) {
96 updateStatus(ThingStatus.UNKNOWN);
97 scheduler.submit(() -> {
99 String readerThreadName = "OH-binding-" + getThing().getUID().getAsString();
101 IntesisBoxSocketApi intesisLocalApi = intesisBoxSocketApi = new IntesisBoxSocketApi(config.ipAddress,
102 config.port, readerThreadName);
103 intesisLocalApi.addIntesisBoxChangeListener(this);
105 intesisLocalApi.openConnection();
106 intesisLocalApi.sendId();
107 intesisLocalApi.sendLimitsQuery();
108 intesisLocalApi.sendAlive();
109 } catch (IOException e) {
110 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
113 updateStatus(ThingStatus.ONLINE);
115 pollingTask = scheduler.scheduleWithFixedDelay(this::polling, 3, config.pollingInterval, TimeUnit.SECONDS);
117 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "No IP address specified)");
122 public void dispose() {
123 final ScheduledFuture<?> pollingTask = this.pollingTask;
125 IntesisBoxSocketApi api = this.intesisBoxSocketApi;
127 if (pollingTask != null) {
128 pollingTask.cancel(true);
129 this.pollingTask = null;
132 api.closeConnection();
133 api.removeIntesisBoxChangeListener(this);
138 private synchronized void polling() {
139 IntesisBoxSocketApi api = this.intesisBoxSocketApi;
141 if (!api.isConnected()) {
143 api.openConnection();
144 } catch (IOException e) {
145 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
154 public void handleCommand(ChannelUID channelUID, Command command) {
155 IntesisBoxSocketApi api = this.intesisBoxSocketApi;
157 if (!api.isConnected()) {
158 logger.trace("Sending command failed, not connected");
161 if (command instanceof RefreshType) {
162 logger.trace("Refresh channel {}", channelUID.getId());
163 api.sendQuery(channelUID.getId());
168 String function = "";
169 switch (channelUID.getId()) {
170 case CHANNEL_TYPE_POWER:
171 if (command instanceof OnOffType) {
173 value = command == OnOffType.ON ? "ON" : "OFF";
176 case CHANNEL_TYPE_TARGETTEMP:
177 if (command instanceof QuantityType quantityCommand) {
178 QuantityType<?> celsiusTemperature = quantityCommand.toUnit(SIUnits.CELSIUS);
179 if (celsiusTemperature != null) {
180 double doubleValue = celsiusTemperature.doubleValue();
181 logger.trace("targetTemp double value = {}", doubleValue);
182 doubleValue = Math.max(minTemp, Math.min(maxTemp, doubleValue));
183 value = String.format("%.0f", doubleValue * 10);
184 function = "SETPTEMP";
185 logger.trace("targetTemp raw string = {}", value);
189 case CHANNEL_TYPE_MODE:
191 value = command.toString();
193 case CHANNEL_TYPE_FANSPEED:
195 value = command.toString();
197 case CHANNEL_TYPE_VANESUD:
199 value = command.toString();
201 case CHANNEL_TYPE_VANESLR:
203 value = command.toString();
206 if (!value.isEmpty() || function.isEmpty()) {
208 logger.trace("Sending command {} to function {}", value, function);
209 api.sendCommand(function, value);
211 logger.warn("Sending command failed, could not get API");
216 private void populateProperties(String[] value) {
217 properties.put(PROPERTY_VENDOR, "Intesis");
218 properties.put(PROPERTY_MODEL_ID, value[0]);
219 properties.put(PROPERTY_MAC_ADDRESS, value[1]);
220 properties.put("ipAddress", value[2]);
221 properties.put("protocol", value[3]);
222 properties.put(PROPERTY_FIRMWARE_VERSION, value[4]);
223 properties.put("hostname", value[6]);
224 updateProperties(properties);
225 hasProperties = true;
228 private void receivedUpdate(String function, String receivedValue) {
229 String value = receivedValue;
230 logger.trace("receivedUpdate(): {} {}", function, value);
233 updateState(CHANNEL_TYPE_POWER, OnOffType.from(value));
237 if ("32768".equals(value)) {
240 updateState(CHANNEL_TYPE_TARGETTEMP,
241 new QuantityType<Temperature>(Double.valueOf(value) / 10.0d, SIUnits.CELSIUS));
244 if (Double.valueOf(value).isNaN()) {
247 updateState(CHANNEL_TYPE_AMBIENTTEMP,
248 new QuantityType<Temperature>(Double.valueOf(value) / 10.0d, SIUnits.CELSIUS));
251 updateState(CHANNEL_TYPE_MODE, new StringType(value));
254 updateState(CHANNEL_TYPE_FANSPEED, new StringType(value));
257 updateState(CHANNEL_TYPE_VANESUD, new StringType(value));
260 updateState(CHANNEL_TYPE_VANESLR, new StringType(value));
263 updateState(CHANNEL_TYPE_ERRORCODE, new StringType(value));
266 updateState(CHANNEL_TYPE_ERRORSTATUS, new StringType(value));
267 if ("ERR".equals(value)) {
268 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
269 "device reported an error");
275 private void handleMessage(String data) {
276 logger.debug("handleMessage(): Message received - {}", data);
277 if ("ACK".equals(data) || "".equals(data)) {
280 if (data.startsWith(ID + ':')) {
281 String[] value = data.substring(3).split(",");
282 if (!hasProperties) {
283 populateProperties(value);
285 DecimalType signalStrength = mapSignalStrength(Integer.parseInt(value[5]));
286 updateState(CHANNEL_TYPE_RSSI, signalStrength);
289 IntesisBoxMessage message = IntesisBoxMessage.parse(data);
290 if (message != null) {
291 switch (message.getCommand()) {
293 logger.debug("handleMessage(): Limits received - {}", data);
294 String function = message.getFunction();
295 if ("SETPTEMP".equals(function)) {
296 List<Double> limits = message.getLimitsValue().stream().map(l -> Double.valueOf(l) / 10.0d)
297 .collect(Collectors.toList());
298 if (limits.size() == 2) {
299 minTemp = limits.get(0);
300 maxTemp = limits.get(1);
302 logger.trace("Property target temperatures {} added", message.getValue());
303 properties.put("targetTemperature limits", "[" + minTemp + "," + maxTemp + "]");
304 addChannel(CHANNEL_TYPE_TARGETTEMP, "Number:Temperature");
308 properties.put("supported modes", message.getValue());
309 limits.put(CHANNEL_TYPE_MODE, message.getLimitsValue());
310 addChannel(CHANNEL_TYPE_MODE, "String");
313 properties.put("supported fan levels", message.getValue());
314 limits.put(CHANNEL_TYPE_FANSPEED, message.getLimitsValue());
315 addChannel(CHANNEL_TYPE_FANSPEED, "String");
318 properties.put("supported vane up/down modes", message.getValue());
319 limits.put(CHANNEL_TYPE_VANESUD, message.getLimitsValue());
320 addChannel(CHANNEL_TYPE_VANESUD, "String");
323 properties.put("supported vane left/right modes", message.getValue());
324 limits.put(CHANNEL_TYPE_VANESLR, message.getLimitsValue());
325 addChannel(CHANNEL_TYPE_VANESLR, "String");
329 updateProperties(properties);
332 receivedUpdate(message.getFunction(), message.getValue());
338 public void addChannel(String channelId, String itemType) {
339 if (thing.getChannel(channelId) == null) {
340 logger.trace("Channel '{}' for UID to be added", channelId);
341 ThingBuilder thingBuilder = editThing();
342 final ChannelTypeUID channelTypeUID = new ChannelTypeUID(BINDING_ID, channelId);
343 Channel channel = ChannelBuilder.create(new ChannelUID(getThing().getUID(), channelId), itemType)
344 .withType(channelTypeUID).withKind(ChannelKind.STATE).build();
345 thingBuilder.withChannel(channel);
346 updateThing(thingBuilder.build());
348 if (limits.containsKey(channelId)) {
349 List<StateOption> options = new ArrayList<>();
350 for (String mode : limits.get(channelId)) {
352 new StateOption(mode, mode.substring(0, 1).toUpperCase() + mode.substring(1).toLowerCase()));
354 intesisStateDescriptionProvider.setStateOptions(new ChannelUID(getThing().getUID(), channelId), options);
359 public void messageReceived(String messageLine) {
360 logger.trace("messageReceived() : {}", messageLine);
361 handleMessage(messageLine);
365 public void connectionStatusChanged(ThingStatus status, @Nullable String message) {
366 if (message != null) {
367 this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, message);
369 this.updateStatus(status);
372 public static DecimalType mapSignalStrength(int dbm) {
376 } else if (dbm > -70) {
378 } else if (dbm > -80) {
380 } else if (dbm > -90) {
385 return new DecimalType(strength);