]> git.basschouten.com Git - openhab-addons.git/blob
b9b48ac2fad96bc151e1bee2343b7ddca4a3eb97
[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.konnected.internal.handler;
14
15 import static org.openhab.binding.konnected.internal.KonnectedBindingConstants.*;
16
17 import java.util.Map;
18 import java.util.Map.Entry;
19 import java.util.concurrent.TimeUnit;
20
21 import org.openhab.binding.konnected.internal.KonnectedConfiguration;
22 import org.openhab.binding.konnected.internal.KonnectedHTTPUtils;
23 import org.openhab.binding.konnected.internal.KonnectedHttpRetryExceeded;
24 import org.openhab.binding.konnected.internal.ZoneConfiguration;
25 import org.openhab.binding.konnected.internal.gson.KonnectedModuleGson;
26 import org.openhab.binding.konnected.internal.gson.KonnectedModulePayload;
27 import org.openhab.core.config.core.Configuration;
28 import org.openhab.core.config.core.validation.ConfigValidationException;
29 import org.openhab.core.library.types.OnOffType;
30 import org.openhab.core.library.types.QuantityType;
31 import org.openhab.core.library.unit.SIUnits;
32 import org.openhab.core.library.unit.Units;
33 import org.openhab.core.thing.Channel;
34 import org.openhab.core.thing.ChannelUID;
35 import org.openhab.core.thing.Thing;
36 import org.openhab.core.thing.ThingStatus;
37 import org.openhab.core.thing.ThingStatusDetail;
38 import org.openhab.core.thing.binding.BaseThingHandler;
39 import org.openhab.core.types.Command;
40 import org.openhab.core.types.RefreshType;
41 import org.slf4j.Logger;
42 import org.slf4j.LoggerFactory;
43
44 import com.google.gson.Gson;
45 import com.google.gson.GsonBuilder;
46
47 /**
48  * The {@link KonnectedHandler} is responsible for handling commands, which are
49  * sent to one of the channels.
50  *
51  * @author Zachary Christiansen - Initial contribution
52  */
53 public class KonnectedHandler extends BaseThingHandler {
54     private final Logger logger = LoggerFactory.getLogger(KonnectedHandler.class);
55     private KonnectedConfiguration config;
56     private final KonnectedHTTPUtils http = new KonnectedHTTPUtils(30);
57     private String callbackUrl;
58     private String baseUrl;
59     private final Gson gson = new GsonBuilder().create();
60     private int retryCount;
61     private final String thingID;
62     public String authToken;
63
64     /**
65      * This is the constructor of the Konnected Handler.
66      *
67      * @param thing the instance of the Konnected thing
68      * @param webHookServlet the instance of the callback servlet that is running for communication with the Konnected
69      *            Module
70      * @param hostAddress the webaddress of the openHAB server instance obtained by the runtime
71      * @param port the port on which the openHAB instance is running that was obtained by the runtime.
72      */
73     public KonnectedHandler(Thing thing, String callbackUrl) {
74         super(thing);
75         this.callbackUrl = callbackUrl;
76         logger.debug("The auto discovered callback URL is: {}", this.callbackUrl);
77         retryCount = 2;
78         thingID = getThing().getThingTypeUID().getId();
79         authToken = getThing().getUID().getAsString();
80     }
81
82     @Override
83     public void handleCommand(ChannelUID channelUID, Command command) {
84         // get the zone number in integer form
85         Channel channel = this.getThing().getChannel(channelUID.getId());
86         String channelType = channel.getChannelTypeUID().getAsString();
87         ZoneConfiguration zoneConfig = channel.getConfiguration().as(ZoneConfiguration.class);
88         String zone = zoneConfig.zone;
89         logger.debug("The channelUID is: {} and the zone is : {}", channelUID.getAsString(), zone);
90         // if the command is OnOfftype
91         if (command instanceof OnOffType) {
92             if (channelType.contains(CHANNEL_SWITCH)) {
93                 logger.debug("A command was sent to a sensor type so we are ignoring the command");
94             } else {
95                 int sendCommand = (OnOffType.OFF.compareTo((OnOffType) command));
96                 logger.debug("The command being sent to zone {} for channel:{}  is {}", zone, channelUID.getAsString(),
97                         sendCommand);
98                 sendActuatorCommand(sendCommand, zone, channelUID);
99             }
100         } else if (command instanceof RefreshType) {
101             // check to see if handler has been initialized before attempting to get state of pin, else wait one minute
102             if (this.isInitialized()) {
103                 getSwitchState(zone, channelUID);
104             } else {
105                 scheduler.schedule(() -> {
106                     handleCommand(channelUID, command);
107                 }, 1, TimeUnit.MINUTES);
108             }
109         }
110     }
111
112     /**
113      * Process a {@link WebHookEvent} that has been received by the Servlet from a Konnected module with respect to a
114      * sensor event or status update request
115      *
116      * @param event the {@link KonnectedModuleGson} event that contains the state and pin information to be processed
117      */
118     public void handleWebHookEvent(KonnectedModuleGson event) {
119         // if we receive a command update the thing status to being online
120         updateStatus(ThingStatus.ONLINE);
121         // get the zone number based off of the index location of the pin value
122         // String sentZone = Integer.toString(Arrays.asList(PIN_TO_ZONE).indexOf(event.getPin()));
123         String zone = event.getZone(thingID);
124         // check that the zone number is in one of the channelUID definitions
125         logger.debug("Looping Through all channels on thing: {} to find a match for {}", thing.getUID().getAsString(),
126                 zone);
127         getThing().getChannels().forEach(channel -> {
128             ChannelUID channelId = channel.getUID();
129             ZoneConfiguration zoneConfig = channel.getConfiguration().as(ZoneConfiguration.class);
130             // if the string zone that was sent equals the last digit of the channelId found process it as the
131             // channelId else do nothing
132             if (zone.equalsIgnoreCase(zoneConfig.zone)) {
133                 logger.debug(
134                         "The configrued zone of channelID: {}  was a match for the zone sent by the alarm panel: {} on thing: {}",
135                         channelId, zone, this.getThing().getUID().getId());
136                 String channelType = channel.getChannelTypeUID().getAsString();
137                 logger.debug("The channeltypeID is: {}", channelType);
138                 // check if the itemType has been defined for the zone received
139                 // check the itemType of the Zone, if Contact, send the State if Temp send Temp, etc.
140                 if (channelType.contains(CHANNEL_SWITCH) || channelType.contains(CHANNEL_ACTUATOR)) {
141                     Integer state = event.getState();
142                     logger.debug("The event state is: {}", state);
143                     if (state != null) {
144                         OnOffType onOffType = state == zoneConfig.onValue ? OnOffType.ON : OnOffType.OFF;
145                         updateState(channelId, onOffType);
146                     }
147                 } else if (channelType.contains(CHANNEL_HUMIDITY)) {
148                     // if the state is of type number then this means it is the humidity channel of the dht22
149                     updateState(channelId, new QuantityType<>(Double.parseDouble(event.getHumi()), Units.PERCENT));
150                 } else if (channelType.contains(CHANNEL_TEMPERATURE)) {
151                     if (zoneConfig.dht22) {
152                         updateState(channelId,
153                                 new QuantityType<>(Double.parseDouble(event.getTemp()), SIUnits.CELSIUS));
154                     } else {
155                         // need to check to make sure right dsb1820 address
156                         logger.debug("The address of the DSB1820 sensor received from module {} is: {}",
157                                 this.thing.getUID(), event.getAddr());
158
159                         if (event.getAddr().equalsIgnoreCase(zoneConfig.ds18b20Address)) {
160                             updateState(channelId,
161                                     new QuantityType<>(Double.parseDouble(event.getTemp()), SIUnits.CELSIUS));
162                         } else {
163                             logger.debug("The address of {} does not match {} not updating this channel",
164                                     event.getAddr(), zoneConfig.ds18b20Address);
165                         }
166                     }
167                 }
168             } else {
169                 logger.trace(
170                         "The zone number sent by the alarm panel: {} was not a match the configured zone for channelId: {} for thing {}",
171                         zone, channelId, getThing().getThingTypeUID());
172             }
173         });
174     }
175
176     private void checkConfiguration() throws ConfigValidationException {
177         logger.debug("Checking configuration on thing {}", this.getThing().getUID().getAsString());
178         Configuration testConfig = this.getConfig();
179         String testRetryCount = testConfig.get(RETRY_COUNT).toString();
180         String testRequestTimeout = testConfig.get(REQUEST_TIMEOUT).toString();
181         baseUrl = testConfig.get(BASE_URL).toString();
182         String configuredCallbackUrl = (String) getThing().getConfiguration().get(CALLBACK_URL);
183         if (configuredCallbackUrl != null) {
184             callbackUrl = configuredCallbackUrl;
185         } else {
186             getThing().getConfiguration().put(CALLBACK_URL, callbackUrl);
187         }
188         logger.debug("The RequestTimeout Parameter is Configured as: {}", testRequestTimeout);
189         logger.debug("The Retry Count Parameter is Configured as: {}", testRetryCount);
190         logger.debug("Base URL is Configured as: {}", baseUrl);
191         logger.debug("The callback URL is: {}", callbackUrl);
192         try {
193             this.retryCount = Integer.parseInt(testRetryCount);
194         } catch (NumberFormatException e) {
195             logger.debug(
196                     "Please check your configuration of the Retry Count as it is not an Integer. It is configured as: {}, will contintue to configure the binding with the default of 2",
197                     testRetryCount);
198             this.retryCount = 2;
199         }
200         try {
201             this.http.setRequestTimeout(Integer.parseInt(testRequestTimeout));
202         } catch (NumberFormatException e) {
203             logger.debug(
204                     "Please check your configuration of the Request Timeout as it is not an Integer. It is configured as: {}, will contintue to configure the binding with the default of 30",
205                     testRequestTimeout);
206         }
207
208         if ((callbackUrl == null)) {
209             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Unable to obtain callback URL");
210         }
211
212         else {
213             this.config = getConfigAs(KonnectedConfiguration.class);
214         }
215     }
216
217     @Override
218     public void handleConfigurationUpdate(Map<String, Object> configurationParameters)
219             throws ConfigValidationException {
220         this.validateConfigurationParameters(configurationParameters);
221         for (Entry<String, Object> configurationParameter : configurationParameters.entrySet()) {
222             Object value = configurationParameter.getValue();
223             logger.debug("Controller Configuration update {} to {}", configurationParameter.getKey(), value);
224
225             if (value == null) {
226                 continue;
227             }
228             // this is a nonstandard implementation to to address the configuration of the konnected alarm panel (as
229             // opposed to the handler) until
230             // https://github.com/eclipse/smarthome/issues/3484 has been implemented in the framework
231             String[] cfg = configurationParameter.getKey().split("_");
232             if ("controller".equals(cfg[0])) {
233                 if (cfg[1].equals("softreset") && value instanceof Boolean && (Boolean) value) {
234                     scheduler.execute(() -> {
235                         try {
236                             http.doGet(baseUrl + "/settings?restart=true", null, retryCount);
237                         } catch (KonnectedHttpRetryExceeded e) {
238                             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
239                         }
240                     });
241                 } else if (cfg[1].equals("removewifi") && value instanceof Boolean && (Boolean) value) {
242                     scheduler.execute(() -> {
243                         try {
244                             http.doGet(baseUrl + "/settings?restore=true", null, retryCount);
245                         } catch (KonnectedHttpRetryExceeded e) {
246                             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
247                         }
248                     });
249                 } else if (cfg[1].equals("sendConfig") && value instanceof Boolean && (Boolean) value) {
250                     scheduler.execute(() -> {
251                         try {
252                             String response = updateKonnectedModule();
253                             logger.trace("The response from the konnected module with thingID {} was {}",
254                                     getThing().getUID(), response);
255                             if (response == null) {
256                                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
257                                         "Unable to communicate with Konnected Module.");
258                             } else {
259                                 updateStatus(ThingStatus.ONLINE);
260                             }
261                         } catch (KonnectedHttpRetryExceeded e) {
262                             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
263                         }
264                     });
265                 }
266             }
267         }
268
269         super.handleConfigurationUpdate(configurationParameters);
270         try
271
272         {
273             String response = updateKonnectedModule();
274             logger.trace("The response from the konnected module with thingID {} was {}", getThing().getUID(),
275                     response);
276             if (response == null) {
277                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
278                         "Unable to communicate with Konnected Module confirm settings.");
279             } else {
280                 updateStatus(ThingStatus.ONLINE);
281             }
282         } catch (KonnectedHttpRetryExceeded e) {
283             logger.trace("The number of retries was exceeeded during the HandleConfigurationUpdate(): {}",
284                     e.getMessage());
285             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
286         }
287     }
288
289     @Override
290     public void initialize() {
291         updateStatus(ThingStatus.UNKNOWN);
292         try {
293             checkConfiguration();
294         } catch (ConfigValidationException e) {
295             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getMessage());
296         }
297         scheduler.execute(() -> {
298             try {
299                 String response = updateKonnectedModule();
300                 if (response == null) {
301                     updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
302                             "Unable to communicate with Konnected Module confirm settings or readd thing.");
303                 } else {
304                     updateStatus(ThingStatus.ONLINE);
305                 }
306             } catch (KonnectedHttpRetryExceeded e) {
307                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
308             }
309         });
310     }
311
312     @Override
313     public void dispose() {
314         logger.debug("Running dispose()");
315         super.dispose();
316     }
317
318     /**
319      * This method constructs the payload that will be sent
320      * to the Konnected module via the put request
321      * it adds the appropriate sensors and actuators to the {@link KonnectedModulePayload}
322      * as well as the location of the callback {@link KonnectedJTTPServlet}
323      * and auth_token which can be used for validation
324      *
325      * @return a json settings payload which can be sent to the Konnected Module based on the Thing
326      */
327     private String constructSettingsPayload() {
328         logger.debug("The Auth_Token is: {}", authToken);
329         KonnectedModulePayload payload = new KonnectedModulePayload(authToken, callbackUrl);
330         payload.setBlink(config.blink);
331         payload.setDiscovery(config.discovery);
332         this.getThing().getChannels().forEach(channel -> {
333             // ChannelUID channelId = channel.getUID();
334             if (isLinked(channel.getUID())) {
335                 // adds linked channels to list based on last value of Channel ID
336                 // which is set to a number
337                 // get the zone number in integer form
338                 ZoneConfiguration zoneConfig = channel.getConfiguration().as(ZoneConfiguration.class);
339                 // if the pin is an actuator add to actuator string
340                 // else add to sensor string
341                 // This is determined based off of the accepted item type, contact types are sensors
342                 // switch types are actuators
343                 String channelType = channel.getChannelTypeUID().getAsString();
344                 logger.debug("The channeltypeID is: {}", channelType);
345                 KonnectedModuleGson module = new KonnectedModuleGson();
346                 module.setZone(thingID, zoneConfig.zone);
347                 if (channelType.contains(CHANNEL_SWITCH)) {
348                     payload.addSensor(module);
349                     logger.trace("Channel {} will be configured on the konnected alarm panel as a switch", channel);
350                 } else if (channelType.contains(CHANNEL_ACTUATOR)) {
351                     payload.addActuators(module);
352                     logger.trace("Channel {} will be configured on the konnected alarm panel as an actuator", channel);
353                 } else if (channelType.contains(CHANNEL_HUMIDITY)) {
354                     // the humidity channels do not need to be added because the supported sensor (dht22) is added under
355                     // the temp sensor
356                     logger.trace("Channel {} is a humidity channel.", channel);
357                 } else if (channelType.contains(CHANNEL_TEMPERATURE)) {
358                     logger.trace("Channel {} will be configured on the konnected alarm panel as a temperature sensor",
359                             channel);
360                     module.setPollInterval(zoneConfig.pollInterval);
361                     logger.trace("The Temperature Sensor Type is: {} ", zoneConfig.dht22);
362                     if (zoneConfig.dht22) {
363                         // add it as a dht22 module
364                         payload.addDht22(module);
365                         logger.trace(
366                                 "Channel {} will be configured on the konnected alarm panel as a DHT22 temperature sensor",
367                                 channel);
368                     } else {
369                         // add to payload as a DS18B20 module if the parameter is false
370                         payload.addDs18b20(module);
371                         logger.trace(
372                                 "Channel {} will be configured on the konnected alarm panel as a DS18B20 temperature sensor",
373                                 channel);
374                     }
375                 } else {
376                     logger.debug("Channel {} is of type {} which is not supported by the konnected binding", channel,
377                             channelType);
378                 }
379             } else {
380                 logger.debug("The Channel {} is not linked to an item", channel.getUID());
381             }
382         });
383         // Create Json to Send to Konnected Module
384
385         String payloadString = gson.toJson(payload);
386         logger.debug("The payload is: {}", payloadString);
387         return payloadString;
388     }
389
390     /*
391      * Prepares and sends the {@link KonnectedModulePayload} via the {@link KonnectedHttpUtils}
392      *
393      * @return response obtained from sending the settings payload to Konnected module defined by the thing
394      *
395      * @throws KonnectedHttpRetryExceeded if unable to communicate with the Konnected module defined by the Thing
396      */
397     private String updateKonnectedModule() throws KonnectedHttpRetryExceeded {
398         String payload = constructSettingsPayload();
399         String response = http.doPut(baseUrl + "/settings", payload, retryCount);
400         logger.debug("The response of the put request was: {}", response);
401         return response;
402     }
403
404     /**
405      * Sends a command to the module via {@link KonnectedHTTPUtils}
406      *
407      * @param scommand the string command, either 0 or 1 to send to the actutor pin on the Konnected module
408      * @param zone the zone to send the command to on the Konnected Module
409      */
410     private void sendActuatorCommand(Integer scommand, String zone, ChannelUID channelId) {
411         try {
412             Channel channel = getThing().getChannel(channelId.getId());
413             if (channel != null) {
414                 logger.debug("getasstring: {} getID: {} getGroupId: {} toString:{}", channelId.getAsString(),
415                         channelId.getId(), channelId.getGroupId(), channelId);
416                 ZoneConfiguration zoneConfig = channel.getConfiguration().as(ZoneConfiguration.class);
417                 KonnectedModuleGson payload = new KonnectedModuleGson();
418                 payload.setState(scommand);
419
420                 payload.setZone(thingID, zone);
421
422                 // check to see if this is an On Command type, if so add the momentary, pause, times to the payload if
423                 // they exist on the configuration.
424                 if (scommand == zoneConfig.onValue) {
425                     payload.setTimes(zoneConfig.times);
426                     logger.debug("The times configuration was set to: {} for channelID: {}.", zoneConfig.times,
427                             channelId);
428                     payload.setMomentary(zoneConfig.momentary);
429                     logger.debug("The momentary configuration set to: {} channelID: {}.", zoneConfig.momentary,
430                             channelId);
431                     payload.setPause(zoneConfig.pause);
432                     logger.debug("The pause configuration was set to: {} for channelID: {}.", zoneConfig.pause,
433                             channelId);
434                 }
435                 String payloadString = gson.toJson(payload);
436                 logger.debug("The command payload  is: {}", payloadString);
437                 String path = "";
438                 switch (this.thingID) {
439                     case PRO_MODULE:
440                         path = "/zone";
441                         break;
442                     case WIFI_MODULE:
443                         path = "/device";
444                         break;
445                 }
446                 http.doPut(baseUrl + path, payloadString, retryCount);
447             } else {
448                 logger.debug("The channel {} returned null for channelId.getID(): {}", channelId, channelId.getId());
449             }
450         } catch (KonnectedHttpRetryExceeded e) {
451             logger.debug("Attempting to set the state of the actuator on thing {} failed: {}",
452                     this.thing.getUID().getId(), e.getMessage());
453             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
454                     "Unable to communicate with Konnected Alarm Panel confirm settings, and that module is online.");
455         }
456     }
457
458     private void getSwitchState(String zone, ChannelUID channelId) {
459         Channel channel = getThing().getChannel(channelId.getId());
460         if (!(channel == null)) {
461             logger.debug("getasstring: {} getID: {} getGroupId: {} toString:{}", channelId.getAsString(),
462                     channelId.getId(), channelId.getGroupId(), channelId);
463             KonnectedModuleGson payload = new KonnectedModuleGson();
464             payload.setZone(thingID, zone);
465             String payloadString = gson.toJson(payload);
466             logger.debug("The command payload  is: {}", payloadString);
467             try {
468                 sendSetSwitchState(thingID, payloadString);
469             } catch (KonnectedHttpRetryExceeded e) {
470                 // try to get the state of the device one more time 30 seconds later. This way it can be confirmed if
471                 // the device was simply in a reboot loop when device state was attempted the first time
472                 scheduler.schedule(() -> {
473                     try {
474                         sendSetSwitchState(thingID, payloadString);
475                     } catch (KonnectedHttpRetryExceeded ex) {
476                         updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
477                                 "Unable to communicate with Konnected Alarm Panel confirm settings, and that module is online.");
478                         logger.debug("Attempting to get the state of the zone on thing {} failed for channel: {} : {}",
479                                 this.thing.getUID().getId(), channelId.getAsString(), ex.getMessage());
480                     }
481                 }, 2, TimeUnit.MINUTES);
482             }
483         } else {
484             logger.debug("The channel {} returned null for channelId.getID(): {}", channelId, channelId.getId());
485         }
486     }
487
488     private void sendSetSwitchState(String thingId, String payloadString) throws KonnectedHttpRetryExceeded {
489         String path = thingId.equals(WIFI_MODULE) ? "/device" : "/zone";
490         String response = http.doGet(baseUrl + path, payloadString, retryCount);
491         KonnectedModuleGson[] events = gson.fromJson(response, KonnectedModuleGson[].class);
492         for (KonnectedModuleGson event : events) {
493             this.handleWebHookEvent(event);
494         }
495     }
496 }