2 * Copyright (c) 2010-2020 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
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
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.konnected.internal.handler;
15 import static org.openhab.binding.konnected.internal.KonnectedBindingConstants.*;
17 import java.math.BigDecimal;
18 import java.util.Arrays;
20 import java.util.Map.Entry;
21 import java.util.concurrent.TimeUnit;
23 import org.openhab.binding.konnected.internal.KonnectedConfiguration;
24 import org.openhab.binding.konnected.internal.KonnectedHTTPUtils;
25 import org.openhab.binding.konnected.internal.KonnectedHttpRetryExceeded;
26 import org.openhab.binding.konnected.internal.gson.KonnectedModuleGson;
27 import org.openhab.binding.konnected.internal.gson.KonnectedModulePayload;
28 import org.openhab.core.config.core.Configuration;
29 import org.openhab.core.config.core.validation.ConfigValidationException;
30 import org.openhab.core.library.types.OnOffType;
31 import org.openhab.core.library.types.QuantityType;
32 import org.openhab.core.library.unit.SIUnits;
33 import org.openhab.core.library.unit.SmartHomeUnits;
34 import org.openhab.core.thing.Channel;
35 import org.openhab.core.thing.ChannelUID;
36 import org.openhab.core.thing.Thing;
37 import org.openhab.core.thing.ThingStatus;
38 import org.openhab.core.thing.ThingStatusDetail;
39 import org.openhab.core.thing.binding.BaseThingHandler;
40 import org.openhab.core.types.Command;
41 import org.openhab.core.types.RefreshType;
42 import org.slf4j.Logger;
43 import org.slf4j.LoggerFactory;
45 import com.google.gson.Gson;
46 import com.google.gson.GsonBuilder;
49 * The {@link KonnectedHandler} is responsible for handling commands, which are
50 * sent to one of the channels.
52 * @author Zachary Christiansen - Initial contribution
54 public class KonnectedHandler extends BaseThingHandler {
55 private final Logger logger = LoggerFactory.getLogger(KonnectedHandler.class);
56 private KonnectedConfiguration config;
58 private final String konnectedServletPath;
59 private final KonnectedHTTPUtils http = new KonnectedHTTPUtils(30);
60 private String callbackIpAddress = null;
61 private String moduleIpAddress;
62 private Gson gson = new GsonBuilder().create();
63 private int retryCount;
66 * This is the constructor of the Konnected Handler.
68 * @param thing the instance of the Konnected thing
69 * @param webHookServlet the instance of the callback servlet that is running for communication with the Konnected
71 * @param hostAddress the webaddress of the openHAB server instance obtained by the runtime
72 * @param port the port on which the openHAB instance is running that was obtained by the runtime.
74 public KonnectedHandler(Thing thing, String path, String hostAddress, String port) {
77 this.konnectedServletPath = path;
78 callbackIpAddress = hostAddress + ":" + port;
79 logger.debug("The callback ip address is: {}", callbackIpAddress);
84 public void handleCommand(ChannelUID channelUID, Command command) {
85 // get the zone number in integer form
86 Channel channel = this.getThing().getChannel(channelUID.getId());
87 String channelType = channel.getChannelTypeUID().getAsString();
88 String zoneNumber = (String) channel.getConfiguration().get(CHANNEL_ZONE);
89 Integer zone = Integer.parseInt(zoneNumber);
90 logger.debug("The channelUID is: {} and the zone is : {}", channelUID.getAsString(), zone);
91 // convert the zone to the pin based on value at index of zone
92 Integer pin = Arrays.asList(PIN_TO_ZONE).get(zone);
93 // if the command is OnOfftype
94 if (command instanceof OnOffType) {
95 if (channelType.equalsIgnoreCase(CHANNEL_SWITCH)) {
96 logger.debug("A command was sent to a sensor type so we are ignoring the command");
98 int sendCommand = (OnOffType.OFF.compareTo((OnOffType) command));
99 logger.debug("The command being sent to pin {} for channel:{} is {}", pin, channelUID.getAsString(),
101 sendActuatorCommand(Integer.toString(sendCommand), pin, channelUID);
103 } else if (command instanceof RefreshType) {
104 // check to see if handler has been initialized before attempting to get state of pin, else wait one minute
105 if (this.isInitialized()) {
106 getSwitchState(pin, channelUID);
108 scheduler.schedule(() -> {
109 handleCommand(channelUID, command);
110 }, 1, TimeUnit.MINUTES);
116 * Process a {@link WebHookEvent} that has been received by the Servlet from a Konnected module with respect to a
117 * sensor event or status update request
119 * @param event the {@link KonnectedModuleGson} event that contains the state and pin information to be processed
121 public void handleWebHookEvent(KonnectedModuleGson event) {
122 // if we receive a command upteate the thing status to being online
123 updateStatus(ThingStatus.ONLINE);
124 // get the zone number based off of the index location of the pin value
125 String sentZone = Integer.toString(Arrays.asList(PIN_TO_ZONE).indexOf(event.getPin()));
126 // check that the zone number is in one of the channelUID definitions
127 logger.debug("Looping Through all channels on thing: {} to find a match for {}", thing.getUID().getAsString(),
128 event.getAuthToken());
129 getThing().getChannels().forEach(channel -> {
130 ChannelUID channelId = channel.getUID();
131 String zoneNumber = (String) channel.getConfiguration().get(CHANNEL_ZONE);
132 // if the string zone that was sent equals the last digit of the channelId found process it as the
133 // channelId else do nothing
134 if (sentZone.equalsIgnoreCase(zoneNumber)) {
136 "The configrued zone of channelID: {} was a match for the zone sent by the alarm panel: {} on thing: {}",
137 channelId, sentZone, this.getThing().getUID().getId());
138 String channelType = channel.getChannelTypeUID().getAsString();
139 logger.debug("The channeltypeID is: {}", channelType);
140 // check if the itemType has been defined for the zone received
141 // check the itemType of the Zone, if Contact, send the State if Temp send Temp, etc.
142 if (channelType.equalsIgnoreCase(CHANNEL_SWITCH) || channelType.equalsIgnoreCase(CHANNEL_ACTUATOR)) {
143 OnOffType onOffType = event.getState().equalsIgnoreCase(getOnState(channel)) ? OnOffType.ON
145 updateState(channelId, onOffType);
146 } else if (channelType.equalsIgnoreCase(CHANNEL_HUMIDITY)) {
147 // if the state is of type number then this means it is the humidity channel of the dht22
148 updateState(channelId,
149 new QuantityType<>(Double.parseDouble(event.getHumi()), SmartHomeUnits.PERCENT));
150 } else if (channelType.equalsIgnoreCase(CHANNEL_TEMPERATURE)) {
151 Configuration configuration = channel.getConfiguration();
152 if (((Boolean) configuration.get(CHANNEL_TEMPERATURE_TYPE))) {
153 updateState(channelId,
154 new QuantityType<>(Double.parseDouble(event.getTemp()), SIUnits.CELSIUS));
156 // need to check to make sure right dsb1820 address
157 logger.debug("The address of the DSB1820 sensor received from modeule {} is: {}",
158 this.thing.getUID(), event.getAddr());
159 if (event.getAddr().toString()
160 .equalsIgnoreCase((String) (configuration.get(CHANNEL_TEMPERATURE_DS18B20_ADDRESS)))) {
161 updateState(channelId,
162 new QuantityType<>(Double.parseDouble(event.getTemp()), SIUnits.CELSIUS));
164 logger.debug("The address of {} does not match {} not updating this channel",
165 event.getAddr().toString(),
166 (configuration.get(CHANNEL_TEMPERATURE_DS18B20_ADDRESS)));
172 "The zone number sent by the alarm panel: {} was not a match the configured zone for channelId: {} for thing {}",
173 sentZone, channelId, getThing().getThingTypeUID().toString());
178 private void checkConfiguration() throws ConfigValidationException {
179 logger.debug("Checking configuration on thing {}", this.getThing().getUID().getAsString());
180 Configuration testConfig = this.getConfig();
181 String testRetryCount = testConfig.get(RETRY_COUNT).toString();
182 String testRequestTimeout = testConfig.get(REQUEST_TIMEOUT).toString();
183 logger.debug("The RequestTimeout Parameter is Configured as: {}", testRequestTimeout);
184 logger.debug("The Retry Count Parameter is Configured as: {}", testRetryCount);
186 this.retryCount = Integer.parseInt(testRetryCount);
187 } catch (NumberFormatException e) {
189 "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",
194 this.http.setRequestTimeout(Integer.parseInt(testRequestTimeout));
195 } catch (NumberFormatException e) {
197 "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",
201 if ((callbackIpAddress == null)) {
202 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
203 "Unable to obtain hostaddress from OSGI service, please configure hostaddress");
207 this.config = getConfigAs(KonnectedConfiguration.class);
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);
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 (cfg[1].equals("softreset") && value instanceof Boolean && (Boolean) value) {
228 scheduler.execute(() -> {
230 http.doGet(moduleIpAddress + "/settings?restart=true", null, retryCount);
231 } catch (KonnectedHttpRetryExceeded e) {
232 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
236 } else if (cfg[1].equals("removewifi") && value instanceof Boolean && (Boolean) value) {
237 scheduler.execute(() -> {
239 http.doGet(moduleIpAddress + "/settings?restore=true", null, retryCount);
240 } catch (KonnectedHttpRetryExceeded e) {
241 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
245 } else if (cfg[1].equals("sendConfig") && value instanceof Boolean && (Boolean) value) {
246 scheduler.execute(() -> {
248 String response = updateKonnectedModule();
249 logger.trace("The response from the konnected module with thingID {} was {}",
250 getThing().getUID().toString(), response);
251 if (response == null) {
252 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
253 "Unable to communicate with Konnected Module.");
255 updateStatus(ThingStatus.ONLINE);
257 } catch (KonnectedHttpRetryExceeded e) {
258 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
266 super.handleConfigurationUpdate(configurationParameters);
270 String response = updateKonnectedModule();
271 logger.trace("The response from the konnected module with thingID {} was {}",
272 getThing().getUID().toString(), response);
273 if (response == null) {
274 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
275 "Unable to communicate with Konnected Module confirm settings.");
277 updateStatus(ThingStatus.ONLINE);
279 } catch (KonnectedHttpRetryExceeded e) {
280 logger.trace("The number of retries was exceeeded during the HandleConfigurationUpdate(): {}",
282 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
287 public void initialize() {
288 updateStatus(ThingStatus.UNKNOWN);
290 checkConfiguration();
291 } catch (ConfigValidationException e) {
292 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getMessage());
294 this.moduleIpAddress = this.getThing().getProperties().get(HOST).toString();
295 scheduler.execute(() -> {
297 String response = updateKonnectedModule();
298 if (response == null) {
299 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
300 "Unable to communicate with Konnected Module confirm settings or readd thing.");
302 updateStatus(ThingStatus.ONLINE);
304 } catch (KonnectedHttpRetryExceeded e) {
305 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
311 public void dispose() {
312 logger.debug("Running dispose()");
317 * This method constructs the payload that will be sent
318 * to the Konnected module via the put request
319 * it adds the appropriate sensors and actuators to the {@link KonnectedModulePayload}
320 * as well as the location of the callback {@link KonnectedJTTPServlet}
321 * and auth_token which can be used for validation
323 * @return a json settings payload which can be sent to the Konnected Module based on the Thing
325 private String constructSettingsPayload() {
326 String hostPath = "";
327 hostPath = callbackIpAddress + this.konnectedServletPath;
328 String authToken = getThing().getUID().getAsString();
329 logger.debug("The Auth_Token is: {}", authToken);
330 KonnectedModulePayload payload = new KonnectedModulePayload(authToken, "http://" + hostPath);
331 payload.setBlink(config.blink);
332 payload.setDiscovery(config.discovery);
333 this.getThing().getChannels().forEach(channel -> {
334 // ChannelUID channelId = channel.getUID();
335 if (isLinked(channel.getUID())) {
336 // adds linked channels to list based on last value of Channel ID
337 // which is set to a number
338 // get the zone number in integer form
339 String zoneNumber = (String) channel.getConfiguration().get(CHANNEL_ZONE);
340 Integer zone = Integer.parseInt(zoneNumber);
341 // convert the zone to the pin based on value at index of zone
342 Integer pin = Arrays.asList(PIN_TO_ZONE).get(zone);
343 // if the pin is an actuator add to actuator string
344 // else add to sensor string
345 // This is determined based off of the accepted item type, contact types are sensors
346 // switch types are actuators
347 String channelType = channel.getChannelTypeUID().getAsString();
348 logger.debug("The channeltypeID is: {}", channelType);
349 KonnectedModuleGson module = new KonnectedModuleGson();
351 if (channelType.equalsIgnoreCase(CHANNEL_SWITCH)) {
352 payload.addSensor(module);
353 logger.trace("Channel {} will be configured on the konnected alarm panel as a switch",
355 } else if (channelType.equalsIgnoreCase(CHANNEL_ACTUATOR)) {
356 payload.addActuators(module);
357 logger.trace("Channel {} will be configured on the konnected alarm panel as an actuator",
359 } else if (channelType.equalsIgnoreCase(CHANNEL_HUMIDITY)) {
360 // the humidity channels do not need to be added because the supported sensor (dht22) is added under
362 logger.trace("Channel {} is a humidity channel.", channel.toString());
363 } else if (channelType.equalsIgnoreCase(CHANNEL_TEMPERATURE)) {
364 logger.trace("Channel {} will be configured on the konnected alarm panel as a temperature sensor",
366 Configuration configuration = channel.getConfiguration();
367 if (configuration.get(CHANNEL_TEMPERATRUE_POLL) == null) {
368 module.setPollInterval(3);
370 module.setPollInterval(((BigDecimal) configuration.get(CHANNEL_TEMPERATRUE_POLL)).intValue());
372 logger.trace("The Temperature Sensor Type is: {} ",
373 configuration.get(CHANNEL_TEMPERATURE_TYPE).toString());
374 if ((boolean) configuration.get(CHANNEL_TEMPERATURE_TYPE)) {
375 // add it as a dht22 module
376 payload.addDht22(module);
378 "Channel {} will be configured on the konnected alarm panel as a DHT22 temperature sensor",
381 // add to payload as a DS18B20 module if the parameter is false
382 payload.addDs18b20(module);
384 "Channel {} will be configured on the konnected alarm panel as a DS18B20 temperature sensor",
388 logger.debug("Channel {} is of type {} which is not supported by the konnected binding",
389 channel.toString(), channelType);
392 logger.debug("The Channel {} is not linked to an item", channel.getUID());
395 // Create Json to Send to Konnected Module
397 String payloadString = gson.toJson(payload);
398 logger.debug("The payload is: {}", payloadString);
399 return payloadString;
403 * Prepares and sends the {@link KonnectedModulePayload} via the {@link KonnectedHttpUtils}
405 * @return response obtained from sending the settings payload to Konnected module defined by the thing
407 * @throws KonnectedHttpRetryExceeded if unable to communicate with the Konnected module defined by the Thing
409 private String updateKonnectedModule() throws KonnectedHttpRetryExceeded {
410 String payload = constructSettingsPayload();
411 String response = http.doPut(moduleIpAddress + "/settings", payload, retryCount);
412 logger.debug("The response of the put request was: {}", response);
417 * Sends a command to the module via {@link KonnectedHTTPUtils}
419 * @param scommand the string command, either 0 or 1 to send to the actutor pin on the Konnected module
420 * @param pin the pin to send the command to on the Konnected Module
422 private void sendActuatorCommand(String scommand, Integer pin, ChannelUID channelId) {
424 Channel channel = getThing().getChannel(channelId.getId());
425 if (!(channel == null)) {
426 logger.debug("getasstring: {} getID: {} getGroupId: {} toString:{}", channelId.getAsString(),
427 channelId.getId(), channelId.getGroupId(), channelId.toString());
428 Configuration configuration = channel.getConfiguration();
429 KonnectedModuleGson payload = new KonnectedModuleGson();
430 payload.setState(scommand);
432 // check to see if this is an On Command type, if so add the momentary, pause, times to the payload if
433 // they exist on the configuration.
434 if (scommand.equals(getOnState(channel))) {
435 if (configuration.get(CHANNEL_ACTUATOR_TIMES) == null) {
437 "The times configuration was not set for channelID: {}, not adding it to the payload.",
438 channelId.toString());
440 payload.setTimes(configuration.get(CHANNEL_ACTUATOR_TIMES).toString());
441 logger.debug("The times configuration was set to: {} for channelID: {}.",
442 configuration.get(CHANNEL_ACTUATOR_TIMES).toString(), channelId.toString());
444 if (configuration.get(CHANNEL_ACTUATOR_MOMENTARY) == null) {
446 "The momentary configuration was not set for channelID: {}, not adding it to the payload.",
447 channelId.toString());
449 payload.setMomentary(configuration.get(CHANNEL_ACTUATOR_MOMENTARY).toString());
450 logger.debug("The momentary configuration set to: {} channelID: {}.",
451 configuration.get(CHANNEL_ACTUATOR_MOMENTARY).toString(), channelId.toString());
453 if (configuration.get(CHANNEL_ACTUATOR_PAUSE) == null) {
455 "The pause configuration was not set for channelID: {}, not adding it to the payload.",
456 channelId.toString());
458 payload.setPause(configuration.get(CHANNEL_ACTUATOR_PAUSE).toString());
459 logger.debug("The pause configuration was set to: {} for channelID: {}.",
460 configuration.get(CHANNEL_ACTUATOR_PAUSE).toString(), channelId.toString());
463 String payloadString = gson.toJson(payload);
464 logger.debug("The command payload is: {}", payloadString);
465 http.doPut(moduleIpAddress + "/device", payloadString, retryCount);
467 logger.debug("The channel {} returned null for channelId.getID(): {}", channelId.toString(),
470 } catch (KonnectedHttpRetryExceeded e) {
471 logger.debug("Attempting to set the state of the actuator on thing {} failed: {}",
472 this.thing.getUID().getId(), e.getMessage());
473 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
474 "Unable to communicate with Konnected Alarm Panel confirm settings, and that module is online.");
478 private void getSwitchState(Integer pin, ChannelUID channelId) {
479 Channel channel = getThing().getChannel(channelId.getId());
480 if (!(channel == null)) {
481 logger.debug("getasstring: {} getID: {} getGroupId: {} toString:{}", channelId.getAsString(),
482 channelId.getId(), channelId.getGroupId(), channelId.toString());
483 KonnectedModuleGson payload = new KonnectedModuleGson();
485 String payloadString = gson.toJson(payload);
486 logger.debug("The command payload is: {}", payloadString);
488 sendSetSwitchState(payloadString);
489 } catch (KonnectedHttpRetryExceeded e) {
490 // try to get the state of the device one more time 30 seconds later. This way it can be confirmed if
491 // the device was simply in a reboot loop when device state was attempted the first time
492 scheduler.schedule(() -> {
494 sendSetSwitchState(payloadString);
495 } catch (KonnectedHttpRetryExceeded ex) {
496 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
497 "Unable to communicate with Konnected Alarm Panel confirm settings, and that module is online.");
498 logger.debug("Attempting to get the state of the zone on thing {} failed for channel: {} : {}",
499 this.thing.getUID().getId(), channelId.getAsString(), ex.getMessage());
501 }, 2, TimeUnit.MINUTES);
504 logger.debug("The channel {} returned null for channelId.getID(): {}", channelId.toString(),
509 private void sendSetSwitchState(String payloadString) throws KonnectedHttpRetryExceeded {
510 String response = http.doGet(moduleIpAddress + "/device", payloadString, retryCount);
511 KonnectedModuleGson event = gson.fromJson(response, KonnectedModuleGson.class);
512 this.handleWebHookEvent(event);
515 private String getOnState(Channel channel) {
516 String config = (String) channel.getConfiguration().get(CHANNEL_ONVALUE);
517 if (config == null) {