]> git.basschouten.com Git - openhab-addons.git/blob
163e8d6d1baddaaf1497592c1b33b72a9f2bb057
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2022 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
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
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.binding.intesis.internal.handler;
14
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.*;
18
19 import java.io.IOException;
20 import java.util.ArrayList;
21 import java.util.HashMap;
22 import java.util.List;
23 import java.util.Map;
24 import java.util.concurrent.ScheduledFuture;
25 import java.util.concurrent.TimeUnit;
26 import java.util.stream.Collectors;
27
28 import javax.measure.quantity.Temperature;
29
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;
57
58 /**
59  * The {@link IntesisBoxHandler} is responsible for handling commands, which are
60  * sent to one of the channels.
61  *
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
65  *
66  */
67 @NonNullByDefault
68 public class IntesisBoxHandler extends BaseThingHandler implements IntesisBoxChangeListener {
69
70     private final Logger logger = LoggerFactory.getLogger(IntesisBoxHandler.class);
71     private @Nullable IntesisBoxSocketApi intesisBoxSocketApi;
72
73     private final Map<String, String> properties = new HashMap<>();
74     private final Map<String, List<String>> limits = new HashMap<>();
75
76     private final IntesisDynamicStateDescriptionProvider intesisStateDescriptionProvider;
77
78     private IntesisBoxConfiguration config = new IntesisBoxConfiguration();
79
80     private double minTemp = 0.0, maxTemp = 0.0;
81
82     private boolean hasProperties = false;
83
84     private @Nullable ScheduledFuture<?> pollingTask;
85
86     public IntesisBoxHandler(Thing thing, IntesisDynamicStateDescriptionProvider intesisStateDescriptionProvider) {
87         super(thing);
88         this.intesisStateDescriptionProvider = intesisStateDescriptionProvider;
89     }
90
91     @Override
92     public void initialize() {
93         config = getConfigAs(IntesisBoxConfiguration.class);
94
95         if (!config.ipAddress.isEmpty()) {
96
97             updateStatus(ThingStatus.UNKNOWN);
98             scheduler.submit(() -> {
99
100                 String readerThreadName = "OH-binding-" + getThing().getUID().getAsString();
101
102                 IntesisBoxSocketApi intesisLocalApi = intesisBoxSocketApi = new IntesisBoxSocketApi(config.ipAddress,
103                         config.port, readerThreadName);
104                 intesisLocalApi.addIntesisBoxChangeListener(this);
105                 try {
106                     intesisLocalApi.openConnection();
107                     intesisLocalApi.sendId();
108                     intesisLocalApi.sendLimitsQuery();
109                     intesisLocalApi.sendAlive();
110
111                 } catch (IOException e) {
112                     updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
113                     return;
114                 }
115                 updateStatus(ThingStatus.ONLINE);
116             });
117             pollingTask = scheduler.scheduleWithFixedDelay(this::polling, 3, 45, TimeUnit.SECONDS);
118         } else {
119             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "No IP address specified)");
120         }
121     }
122
123     @Override
124     public void dispose() {
125         final ScheduledFuture<?> pollingTask = this.pollingTask;
126
127         IntesisBoxSocketApi api = this.intesisBoxSocketApi;
128
129         if (pollingTask != null) {
130             pollingTask.cancel(true);
131             this.pollingTask = null;
132         }
133         if (api != null) {
134             api.closeConnection();
135             api.removeIntesisBoxChangeListener(this);
136         }
137         super.dispose();
138     }
139
140     private synchronized void polling() {
141         IntesisBoxSocketApi api = this.intesisBoxSocketApi;
142         if (api != null) {
143             if (!api.isConnected()) {
144                 try {
145                     api.openConnection();
146                 } catch (IOException e) {
147                     updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
148                 }
149             }
150             api.sendAlive();
151             api.sendId();
152         }
153     }
154
155     @Override
156     public void handleCommand(ChannelUID channelUID, Command command) {
157         IntesisBoxSocketApi api = this.intesisBoxSocketApi;
158         if (api != null) {
159             if (!api.isConnected()) {
160                 logger.trace("Sending command failed, not connected");
161                 return;
162             }
163             if (command instanceof RefreshType) {
164                 logger.trace("Refresh channel {}", channelUID.getId());
165                 api.sendQuery(channelUID.getId());
166                 return;
167             }
168         }
169         String value = "";
170         String function = "";
171         switch (channelUID.getId()) {
172             case CHANNEL_TYPE_POWER:
173                 if (command instanceof OnOffType) {
174                     function = "ONOFF";
175                     value = command == OnOffType.ON ? "ON" : "OFF";
176                 }
177                 break;
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);
189                     }
190                 }
191                 break;
192             case CHANNEL_TYPE_MODE:
193                 function = "MODE";
194                 value = command.toString();
195                 break;
196             case CHANNEL_TYPE_FANSPEED:
197                 function = "FANSP";
198                 value = command.toString();
199                 break;
200             case CHANNEL_TYPE_VANESUD:
201                 function = "VANEUD";
202                 value = command.toString();
203                 break;
204             case CHANNEL_TYPE_VANESLR:
205                 function = "VANELR";
206                 value = command.toString();
207                 break;
208         }
209         if (!value.isEmpty() || function.isEmpty()) {
210             if (api != null) {
211                 logger.trace("Sending command {} to function {}", value, function);
212                 api.sendCommand(function, value);
213             } else {
214                 logger.warn("Sending command failed, could not get API");
215             }
216         }
217     }
218
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;
229     }
230
231     private void receivedUpdate(String function, String receivedValue) {
232         String value = receivedValue;
233         logger.trace("receivedUpdate(): {} {}", function, value);
234         switch (function) {
235             case "ONOFF":
236                 updateState(CHANNEL_TYPE_POWER, OnOffType.from(value));
237                 break;
238
239             case "SETPTEMP":
240                 if (value.equals("32768")) {
241                     value = "0";
242                 }
243                 updateState(CHANNEL_TYPE_TARGETTEMP,
244                         new QuantityType<Temperature>(Double.valueOf(value) / 10.0d, SIUnits.CELSIUS));
245                 break;
246             case "AMBTEMP":
247                 if (Double.valueOf(value).isNaN()) {
248                     value = "0";
249                 }
250                 updateState(CHANNEL_TYPE_AMBIENTTEMP,
251                         new QuantityType<Temperature>(Double.valueOf(value) / 10.0d, SIUnits.CELSIUS));
252                 break;
253             case "MODE":
254                 updateState(CHANNEL_TYPE_MODE, new StringType(value));
255                 break;
256             case "FANSP":
257                 updateState(CHANNEL_TYPE_FANSPEED, new StringType(value));
258                 break;
259             case "VANEUD":
260                 updateState(CHANNEL_TYPE_VANESUD, new StringType(value));
261                 break;
262             case "VANELR":
263                 updateState(CHANNEL_TYPE_VANESLR, new StringType(value));
264                 break;
265             case "ERRCODE":
266                 updateState(CHANNEL_TYPE_ERRORCODE, new StringType(value));
267                 break;
268             case "ERRSTATUS":
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");
273                 }
274                 break;
275         }
276     }
277
278     private void handleMessage(String data) {
279         logger.debug("handleMessage(): Message received - {}", data);
280         if (data.equals("ACK") || data.equals("")) {
281             return;
282         }
283         if (data.startsWith(ID + ':')) {
284             String[] value = data.substring(3).split(",");
285             if (!hasProperties) {
286                 populateProperties(value);
287             }
288             DecimalType signalStrength = mapSignalStrength(Integer.parseInt(value[5]));
289             updateState(CHANNEL_TYPE_RSSI, signalStrength);
290             return;
291         }
292         IntesisBoxMessage message = IntesisBoxMessage.parse(data);
293         if (message != null) {
294             switch (message.getCommand()) {
295                 case LIMITS:
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);
304                         }
305                         logger.trace("Property target temperatures {} added", message.getValue());
306                         properties.put("targetTemperature limits", "[" + minTemp + "," + maxTemp + "]");
307                         addChannel(CHANNEL_TYPE_TARGETTEMP, "Number:Temperature");
308                     } else {
309                         switch (function) {
310                             case "MODE":
311                                 properties.put("supported modes", message.getValue());
312                                 limits.put(CHANNEL_TYPE_MODE, message.getLimitsValue());
313                                 addChannel(CHANNEL_TYPE_MODE, "String");
314                                 break;
315                             case "FANSP":
316                                 properties.put("supported fan levels", message.getValue());
317                                 limits.put(CHANNEL_TYPE_FANSPEED, message.getLimitsValue());
318                                 addChannel(CHANNEL_TYPE_FANSPEED, "String");
319                                 break;
320                             case "VANEUD":
321                                 properties.put("supported vane up/down modes", message.getValue());
322                                 limits.put(CHANNEL_TYPE_VANESUD, message.getLimitsValue());
323                                 addChannel(CHANNEL_TYPE_VANESUD, "String");
324                                 break;
325                             case "VANELR":
326                                 properties.put("supported vane left/right modes", message.getValue());
327                                 limits.put(CHANNEL_TYPE_VANESLR, message.getLimitsValue());
328                                 addChannel(CHANNEL_TYPE_VANESLR, "String");
329                                 break;
330                         }
331                     }
332                     updateProperties(properties);
333                     break;
334                 case CHN:
335                     receivedUpdate(message.getFunction(), message.getValue());
336                     break;
337             }
338         }
339     }
340
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());
350         }
351         if (limits.containsKey(channelId)) {
352             List<StateOption> options = new ArrayList<>();
353             for (String mode : limits.get(channelId)) {
354                 options.add(
355                         new StateOption(mode, mode.substring(0, 1).toUpperCase() + mode.substring(1).toLowerCase()));
356             }
357             intesisStateDescriptionProvider.setStateOptions(new ChannelUID(getThing().getUID(), channelId), options);
358         }
359     }
360
361     @Override
362     public void messageReceived(String messageLine) {
363         logger.trace("messageReceived() : {}", messageLine);
364         handleMessage(messageLine);
365     }
366
367     @Override
368     public void connectionStatusChanged(ThingStatus status, @Nullable String message) {
369         if (message != null) {
370             this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, message);
371         }
372         this.updateStatus(status);
373     }
374
375     public static DecimalType mapSignalStrength(int dbm) {
376         int strength = -1;
377         if (dbm > -60) {
378             strength = 4;
379         } else if (dbm > -70) {
380             strength = 3;
381         } else if (dbm > -80) {
382             strength = 2;
383         } else if (dbm > -90) {
384             strength = 1;
385         } else {
386             strength = 0;
387         }
388         return new DecimalType(strength);
389     }
390 }