]> git.basschouten.com Git - openhab-addons.git/blob
8de6e1af2d800d0928f7a08f4cdc4e2b927b56e2
[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) {
178                     QuantityType<?> celsiusTemperature = (QuantityType<?>) command;
179                     celsiusTemperature = celsiusTemperature.toUnit(SIUnits.CELSIUS);
180                     if (celsiusTemperature != null) {
181                         double doubleValue = celsiusTemperature.doubleValue();
182                         logger.trace("targetTemp double value = {}", doubleValue);
183                         doubleValue = Math.max(minTemp, Math.min(maxTemp, doubleValue));
184                         value = String.format("%.0f", doubleValue * 10);
185                         function = "SETPTEMP";
186                         logger.trace("targetTemp raw string = {}", value);
187                     }
188                 }
189                 break;
190             case CHANNEL_TYPE_MODE:
191                 function = "MODE";
192                 value = command.toString();
193                 break;
194             case CHANNEL_TYPE_FANSPEED:
195                 function = "FANSP";
196                 value = command.toString();
197                 break;
198             case CHANNEL_TYPE_VANESUD:
199                 function = "VANEUD";
200                 value = command.toString();
201                 break;
202             case CHANNEL_TYPE_VANESLR:
203                 function = "VANELR";
204                 value = command.toString();
205                 break;
206         }
207         if (!value.isEmpty() || function.isEmpty()) {
208             if (api != null) {
209                 logger.trace("Sending command {} to function {}", value, function);
210                 api.sendCommand(function, value);
211             } else {
212                 logger.warn("Sending command failed, could not get API");
213             }
214         }
215     }
216
217     private void populateProperties(String[] value) {
218         properties.put(PROPERTY_VENDOR, "Intesis");
219         properties.put(PROPERTY_MODEL_ID, value[0]);
220         properties.put(PROPERTY_MAC_ADDRESS, value[1]);
221         properties.put("ipAddress", value[2]);
222         properties.put("protocol", value[3]);
223         properties.put(PROPERTY_FIRMWARE_VERSION, value[4]);
224         properties.put("hostname", value[6]);
225         updateProperties(properties);
226         hasProperties = true;
227     }
228
229     private void receivedUpdate(String function, String receivedValue) {
230         String value = receivedValue;
231         logger.trace("receivedUpdate(): {} {}", function, value);
232         switch (function) {
233             case "ONOFF":
234                 updateState(CHANNEL_TYPE_POWER, OnOffType.from(value));
235                 break;
236
237             case "SETPTEMP":
238                 if ("32768".equals(value)) {
239                     value = "0";
240                 }
241                 updateState(CHANNEL_TYPE_TARGETTEMP,
242                         new QuantityType<Temperature>(Double.valueOf(value) / 10.0d, SIUnits.CELSIUS));
243                 break;
244             case "AMBTEMP":
245                 if (Double.valueOf(value).isNaN()) {
246                     value = "0";
247                 }
248                 updateState(CHANNEL_TYPE_AMBIENTTEMP,
249                         new QuantityType<Temperature>(Double.valueOf(value) / 10.0d, SIUnits.CELSIUS));
250                 break;
251             case "MODE":
252                 updateState(CHANNEL_TYPE_MODE, new StringType(value));
253                 break;
254             case "FANSP":
255                 updateState(CHANNEL_TYPE_FANSPEED, new StringType(value));
256                 break;
257             case "VANEUD":
258                 updateState(CHANNEL_TYPE_VANESUD, new StringType(value));
259                 break;
260             case "VANELR":
261                 updateState(CHANNEL_TYPE_VANESLR, new StringType(value));
262                 break;
263             case "ERRCODE":
264                 updateState(CHANNEL_TYPE_ERRORCODE, new StringType(value));
265                 break;
266             case "ERRSTATUS":
267                 updateState(CHANNEL_TYPE_ERRORSTATUS, new StringType(value));
268                 if ("ERR".equals(value)) {
269                     updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
270                             "device reported an error");
271                 }
272                 break;
273         }
274     }
275
276     private void handleMessage(String data) {
277         logger.debug("handleMessage(): Message received - {}", data);
278         if ("ACK".equals(data) || "".equals(data)) {
279             return;
280         }
281         if (data.startsWith(ID + ':')) {
282             String[] value = data.substring(3).split(",");
283             if (!hasProperties) {
284                 populateProperties(value);
285             }
286             DecimalType signalStrength = mapSignalStrength(Integer.parseInt(value[5]));
287             updateState(CHANNEL_TYPE_RSSI, signalStrength);
288             return;
289         }
290         IntesisBoxMessage message = IntesisBoxMessage.parse(data);
291         if (message != null) {
292             switch (message.getCommand()) {
293                 case LIMITS:
294                     logger.debug("handleMessage(): Limits received - {}", data);
295                     String function = message.getFunction();
296                     if ("SETPTEMP".equals(function)) {
297                         List<Double> limits = message.getLimitsValue().stream().map(l -> Double.valueOf(l) / 10.0d)
298                                 .collect(Collectors.toList());
299                         if (limits.size() == 2) {
300                             minTemp = limits.get(0);
301                             maxTemp = limits.get(1);
302                         }
303                         logger.trace("Property target temperatures {} added", message.getValue());
304                         properties.put("targetTemperature limits", "[" + minTemp + "," + maxTemp + "]");
305                         addChannel(CHANNEL_TYPE_TARGETTEMP, "Number:Temperature");
306                     } else {
307                         switch (function) {
308                             case "MODE":
309                                 properties.put("supported modes", message.getValue());
310                                 limits.put(CHANNEL_TYPE_MODE, message.getLimitsValue());
311                                 addChannel(CHANNEL_TYPE_MODE, "String");
312                                 break;
313                             case "FANSP":
314                                 properties.put("supported fan levels", message.getValue());
315                                 limits.put(CHANNEL_TYPE_FANSPEED, message.getLimitsValue());
316                                 addChannel(CHANNEL_TYPE_FANSPEED, "String");
317                                 break;
318                             case "VANEUD":
319                                 properties.put("supported vane up/down modes", message.getValue());
320                                 limits.put(CHANNEL_TYPE_VANESUD, message.getLimitsValue());
321                                 addChannel(CHANNEL_TYPE_VANESUD, "String");
322                                 break;
323                             case "VANELR":
324                                 properties.put("supported vane left/right modes", message.getValue());
325                                 limits.put(CHANNEL_TYPE_VANESLR, message.getLimitsValue());
326                                 addChannel(CHANNEL_TYPE_VANESLR, "String");
327                                 break;
328                         }
329                     }
330                     updateProperties(properties);
331                     break;
332                 case CHN:
333                     receivedUpdate(message.getFunction(), message.getValue());
334                     break;
335             }
336         }
337     }
338
339     public void addChannel(String channelId, String itemType) {
340         if (thing.getChannel(channelId) == null) {
341             logger.trace("Channel '{}' for UID to be added", channelId);
342             ThingBuilder thingBuilder = editThing();
343             final ChannelTypeUID channelTypeUID = new ChannelTypeUID(BINDING_ID, channelId);
344             Channel channel = ChannelBuilder.create(new ChannelUID(getThing().getUID(), channelId), itemType)
345                     .withType(channelTypeUID).withKind(ChannelKind.STATE).build();
346             thingBuilder.withChannel(channel);
347             updateThing(thingBuilder.build());
348         }
349         if (limits.containsKey(channelId)) {
350             List<StateOption> options = new ArrayList<>();
351             for (String mode : limits.get(channelId)) {
352                 options.add(
353                         new StateOption(mode, mode.substring(0, 1).toUpperCase() + mode.substring(1).toLowerCase()));
354             }
355             intesisStateDescriptionProvider.setStateOptions(new ChannelUID(getThing().getUID(), channelId), options);
356         }
357     }
358
359     @Override
360     public void messageReceived(String messageLine) {
361         logger.trace("messageReceived() : {}", messageLine);
362         handleMessage(messageLine);
363     }
364
365     @Override
366     public void connectionStatusChanged(ThingStatus status, @Nullable String message) {
367         if (message != null) {
368             this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, message);
369         }
370         this.updateStatus(status);
371     }
372
373     public static DecimalType mapSignalStrength(int dbm) {
374         int strength = -1;
375         if (dbm > -60) {
376             strength = 4;
377         } else if (dbm > -70) {
378             strength = 3;
379         } else if (dbm > -80) {
380             strength = 2;
381         } else if (dbm > -90) {
382             strength = 1;
383         } else {
384             strength = 0;
385         }
386         return new DecimalType(strength);
387     }
388 }