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