]> git.basschouten.com Git - openhab-addons.git/blob
74758e50c757d27a2c90ade0af3c03e0b54e63b7
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2023 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             updateStatus(ThingStatus.UNKNOWN);
97             scheduler.submit(() -> {
98
99                 String readerThreadName = "OH-binding-" + getThing().getUID().getAsString();
100
101                 IntesisBoxSocketApi intesisLocalApi = intesisBoxSocketApi = new IntesisBoxSocketApi(config.ipAddress,
102                         config.port, readerThreadName);
103                 intesisLocalApi.addIntesisBoxChangeListener(this);
104                 try {
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());
111                     return;
112                 }
113                 updateStatus(ThingStatus.ONLINE);
114             });
115             pollingTask = scheduler.scheduleWithFixedDelay(this::polling, 3, config.pollingInterval, TimeUnit.SECONDS);
116         } else {
117             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "No IP address specified)");
118         }
119     }
120
121     @Override
122     public void dispose() {
123         final ScheduledFuture<?> pollingTask = this.pollingTask;
124
125         IntesisBoxSocketApi api = this.intesisBoxSocketApi;
126
127         if (pollingTask != null) {
128             pollingTask.cancel(true);
129             this.pollingTask = null;
130         }
131         if (api != null) {
132             api.closeConnection();
133             api.removeIntesisBoxChangeListener(this);
134         }
135         super.dispose();
136     }
137
138     private synchronized void polling() {
139         IntesisBoxSocketApi api = this.intesisBoxSocketApi;
140         if (api != null) {
141             if (!api.isConnected()) {
142                 try {
143                     api.openConnection();
144                 } catch (IOException e) {
145                     updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
146                 }
147             }
148             api.sendAlive();
149             api.sendId();
150         }
151     }
152
153     @Override
154     public void handleCommand(ChannelUID channelUID, Command command) {
155         IntesisBoxSocketApi api = this.intesisBoxSocketApi;
156         if (api != null) {
157             if (!api.isConnected()) {
158                 logger.trace("Sending command failed, not connected");
159                 return;
160             }
161             if (command instanceof RefreshType) {
162                 logger.trace("Refresh channel {}", channelUID.getId());
163                 api.sendQuery(channelUID.getId());
164                 return;
165             }
166         }
167         String value = "";
168         String function = "";
169         switch (channelUID.getId()) {
170             case CHANNEL_TYPE_POWER:
171                 if (command instanceof OnOffType) {
172                     function = "ONOFF";
173                     value = command == OnOffType.ON ? "ON" : "OFF";
174                 }
175                 break;
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);
186                     }
187                 }
188                 break;
189             case CHANNEL_TYPE_MODE:
190                 function = "MODE";
191                 value = command.toString();
192                 break;
193             case CHANNEL_TYPE_FANSPEED:
194                 function = "FANSP";
195                 value = command.toString();
196                 break;
197             case CHANNEL_TYPE_VANESUD:
198                 function = "VANEUD";
199                 value = command.toString();
200                 break;
201             case CHANNEL_TYPE_VANESLR:
202                 function = "VANELR";
203                 value = command.toString();
204                 break;
205         }
206         if (!value.isEmpty() || function.isEmpty()) {
207             if (api != null) {
208                 logger.trace("Sending command {} to function {}", value, function);
209                 api.sendCommand(function, value);
210             } else {
211                 logger.warn("Sending command failed, could not get API");
212             }
213         }
214     }
215
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;
226     }
227
228     private void receivedUpdate(String function, String receivedValue) {
229         String value = receivedValue;
230         logger.trace("receivedUpdate(): {} {}", function, value);
231         switch (function) {
232             case "ONOFF":
233                 updateState(CHANNEL_TYPE_POWER, OnOffType.from(value));
234                 break;
235
236             case "SETPTEMP":
237                 if ("32768".equals(value)) {
238                     value = "0";
239                 }
240                 updateState(CHANNEL_TYPE_TARGETTEMP,
241                         new QuantityType<Temperature>(Double.valueOf(value) / 10.0d, SIUnits.CELSIUS));
242                 break;
243             case "AMBTEMP":
244                 if (Double.valueOf(value).isNaN()) {
245                     value = "0";
246                 }
247                 updateState(CHANNEL_TYPE_AMBIENTTEMP,
248                         new QuantityType<Temperature>(Double.valueOf(value) / 10.0d, SIUnits.CELSIUS));
249                 break;
250             case "MODE":
251                 updateState(CHANNEL_TYPE_MODE, new StringType(value));
252                 break;
253             case "FANSP":
254                 updateState(CHANNEL_TYPE_FANSPEED, new StringType(value));
255                 break;
256             case "VANEUD":
257                 updateState(CHANNEL_TYPE_VANESUD, new StringType(value));
258                 break;
259             case "VANELR":
260                 updateState(CHANNEL_TYPE_VANESLR, new StringType(value));
261                 break;
262             case "ERRCODE":
263                 updateState(CHANNEL_TYPE_ERRORCODE, new StringType(value));
264                 break;
265             case "ERRSTATUS":
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");
270                 }
271                 break;
272         }
273     }
274
275     private void handleMessage(String data) {
276         logger.debug("handleMessage(): Message received - {}", data);
277         if ("ACK".equals(data) || "".equals(data)) {
278             return;
279         }
280         if (data.startsWith(ID + ':')) {
281             String[] value = data.substring(3).split(",");
282             if (!hasProperties) {
283                 populateProperties(value);
284             }
285             DecimalType signalStrength = mapSignalStrength(Integer.parseInt(value[5]));
286             updateState(CHANNEL_TYPE_RSSI, signalStrength);
287             return;
288         }
289         IntesisBoxMessage message = IntesisBoxMessage.parse(data);
290         if (message != null) {
291             switch (message.getCommand()) {
292                 case LIMITS:
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);
301                         }
302                         logger.trace("Property target temperatures {} added", message.getValue());
303                         properties.put("targetTemperature limits", "[" + minTemp + "," + maxTemp + "]");
304                         addChannel(CHANNEL_TYPE_TARGETTEMP, "Number:Temperature");
305                     } else {
306                         switch (function) {
307                             case "MODE":
308                                 properties.put("supported modes", message.getValue());
309                                 limits.put(CHANNEL_TYPE_MODE, message.getLimitsValue());
310                                 addChannel(CHANNEL_TYPE_MODE, "String");
311                                 break;
312                             case "FANSP":
313                                 properties.put("supported fan levels", message.getValue());
314                                 limits.put(CHANNEL_TYPE_FANSPEED, message.getLimitsValue());
315                                 addChannel(CHANNEL_TYPE_FANSPEED, "String");
316                                 break;
317                             case "VANEUD":
318                                 properties.put("supported vane up/down modes", message.getValue());
319                                 limits.put(CHANNEL_TYPE_VANESUD, message.getLimitsValue());
320                                 addChannel(CHANNEL_TYPE_VANESUD, "String");
321                                 break;
322                             case "VANELR":
323                                 properties.put("supported vane left/right modes", message.getValue());
324                                 limits.put(CHANNEL_TYPE_VANESLR, message.getLimitsValue());
325                                 addChannel(CHANNEL_TYPE_VANESLR, "String");
326                                 break;
327                         }
328                     }
329                     updateProperties(properties);
330                     break;
331                 case CHN:
332                     receivedUpdate(message.getFunction(), message.getValue());
333                     break;
334             }
335         }
336     }
337
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());
347         }
348         if (limits.containsKey(channelId)) {
349             List<StateOption> options = new ArrayList<>();
350             for (String mode : limits.get(channelId)) {
351                 options.add(
352                         new StateOption(mode, mode.substring(0, 1).toUpperCase() + mode.substring(1).toLowerCase()));
353             }
354             intesisStateDescriptionProvider.setStateOptions(new ChannelUID(getThing().getUID(), channelId), options);
355         }
356     }
357
358     @Override
359     public void messageReceived(String messageLine) {
360         logger.trace("messageReceived() : {}", messageLine);
361         handleMessage(messageLine);
362     }
363
364     @Override
365     public void connectionStatusChanged(ThingStatus status, @Nullable String message) {
366         if (message != null) {
367             this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, message);
368         }
369         this.updateStatus(status);
370     }
371
372     public static DecimalType mapSignalStrength(int dbm) {
373         int strength = -1;
374         if (dbm > -60) {
375             strength = 4;
376         } else if (dbm > -70) {
377             strength = 3;
378         } else if (dbm > -80) {
379             strength = 2;
380         } else if (dbm > -90) {
381             strength = 1;
382         } else {
383             strength = 0;
384         }
385         return new DecimalType(strength);
386     }
387 }