2 * Copyright (c) 2010-2022 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;
19 import java.util.Map.Entry;
20 import java.util.concurrent.TimeUnit;
22 import org.openhab.binding.konnected.internal.KonnectedConfiguration;
23 import org.openhab.binding.konnected.internal.KonnectedHTTPUtils;
24 import org.openhab.binding.konnected.internal.KonnectedHttpRetryExceeded;
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;
44 import com.google.gson.Gson;
45 import com.google.gson.GsonBuilder;
48 * The {@link KonnectedHandler} is responsible for handling commands, which are
49 * sent to one of the channels.
51 * @author Zachary Christiansen - Initial contribution
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;
65 * This is the constructor of the Konnected Handler.
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
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.
73 public KonnectedHandler(Thing thing, String callbackUrl) {
75 this.callbackUrl = callbackUrl;
76 logger.debug("The auto discovered callback URL is: {}", this.callbackUrl);
78 thingID = getThing().getThingTypeUID().getId();
79 authToken = getThing().getUID().getAsString();
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 String zone = (String) channel.getConfiguration().get(CHANNEL_ZONE);
88 logger.debug("The channelUID is: {} and the zone is : {}", channelUID.getAsString(), zone);
89 // if the command is OnOfftype
90 if (command instanceof OnOffType) {
91 if (channelType.contains(CHANNEL_SWITCH)) {
92 logger.debug("A command was sent to a sensor type so we are ignoring the command");
94 int sendCommand = (OnOffType.OFF.compareTo((OnOffType) command));
95 logger.debug("The command being sent to zone {} for channel:{} is {}", zone, channelUID.getAsString(),
97 sendActuatorCommand(sendCommand, zone, channelUID);
99 } else if (command instanceof RefreshType) {
100 // check to see if handler has been initialized before attempting to get state of pin, else wait one minute
101 if (this.isInitialized()) {
102 getSwitchState(zone, channelUID);
104 scheduler.schedule(() -> {
105 handleCommand(channelUID, command);
106 }, 1, TimeUnit.MINUTES);
112 * Process a {@link WebHookEvent} that has been received by the Servlet from a Konnected module with respect to a
113 * sensor event or status update request
115 * @param event the {@link KonnectedModuleGson} event that contains the state and pin information to be processed
117 public void handleWebHookEvent(KonnectedModuleGson event) {
118 // if we receive a command update the thing status to being online
119 updateStatus(ThingStatus.ONLINE);
120 // get the zone number based off of the index location of the pin value
121 // String sentZone = Integer.toString(Arrays.asList(PIN_TO_ZONE).indexOf(event.getPin()));
122 String zone = event.getZone(thingID);
123 // check that the zone number is in one of the channelUID definitions
124 logger.debug("Looping Through all channels on thing: {} to find a match for {}", thing.getUID().getAsString(),
126 getThing().getChannels().forEach(channel -> {
127 ChannelUID channelId = channel.getUID();
128 String zoneNumber = (String) channel.getConfiguration().get(CHANNEL_ZONE);
129 // if the string zone that was sent equals the last digit of the channelId found process it as the
130 // channelId else do nothing
131 if (zone.equalsIgnoreCase(zoneNumber)) {
133 "The configrued zone of channelID: {} was a match for the zone sent by the alarm panel: {} on thing: {}",
134 channelId, zone, this.getThing().getUID().getId());
135 String channelType = channel.getChannelTypeUID().getAsString();
136 logger.debug("The channeltypeID is: {}", channelType);
137 // check if the itemType has been defined for the zone received
138 // check the itemType of the Zone, if Contact, send the State if Temp send Temp, etc.
139 if (channelType.contains(CHANNEL_SWITCH) || channelType.contains(CHANNEL_ACTUATOR)) {
140 Integer state = event.getState();
141 logger.debug("The event state is: {}", state);
143 OnOffType onOffType = state == getOnState(channel) ? OnOffType.ON : OnOffType.OFF;
144 updateState(channelId, onOffType);
146 } else if (channelType.contains(CHANNEL_HUMIDITY)) {
147 // if the state is of type number then this means it is the humidity channel of the dht22
148 updateState(channelId, new QuantityType<>(Double.parseDouble(event.getHumi()), Units.PERCENT));
149 } else if (channelType.contains(CHANNEL_TEMPERATURE)) {
150 Configuration configuration = channel.getConfiguration();
151 if (((Boolean) configuration.get(CHANNEL_TEMPERATURE_TYPE))) {
152 updateState(channelId,
153 new QuantityType<>(Double.parseDouble(event.getTemp()), SIUnits.CELSIUS));
155 // need to check to make sure right dsb1820 address
156 logger.debug("The address of the DSB1820 sensor received from modeule {} is: {}",
157 this.thing.getUID(), event.getAddr());
159 .equalsIgnoreCase((String) (configuration.get(CHANNEL_TEMPERATURE_DS18B20_ADDRESS)))) {
160 updateState(channelId,
161 new QuantityType<>(Double.parseDouble(event.getTemp()), SIUnits.CELSIUS));
163 logger.debug("The address of {} does not match {} not updating this channel",
164 event.getAddr(), (configuration.get(CHANNEL_TEMPERATURE_DS18B20_ADDRESS)));
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().toString());
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;
186 getThing().getConfiguration().put(CALLBACK_URL, callbackUrl);
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);
193 this.retryCount = Integer.parseInt(testRetryCount);
194 } catch (NumberFormatException e) {
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",
201 this.http.setRequestTimeout(Integer.parseInt(testRequestTimeout));
202 } catch (NumberFormatException e) {
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",
208 if ((callbackUrl == null)) {
209 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Unable to obtain callback URL");
213 this.config = getConfigAs(KonnectedConfiguration.class);
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);
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(() -> {
236 http.doGet(baseUrl + "/settings?restart=true", null, retryCount);
237 } catch (KonnectedHttpRetryExceeded e) {
238 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
242 } else if (cfg[1].equals("removewifi") && value instanceof Boolean && (Boolean) value) {
243 scheduler.execute(() -> {
245 http.doGet(baseUrl + "/settings?restore=true", null, retryCount);
246 } catch (KonnectedHttpRetryExceeded e) {
247 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
251 } else if (cfg[1].equals("sendConfig") && value instanceof Boolean && (Boolean) value) {
252 scheduler.execute(() -> {
254 String response = updateKonnectedModule();
255 logger.trace("The response from the konnected module with thingID {} was {}",
256 getThing().getUID().toString(), response);
257 if (response == null) {
258 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
259 "Unable to communicate with Konnected Module.");
261 updateStatus(ThingStatus.ONLINE);
263 } catch (KonnectedHttpRetryExceeded e) {
264 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
272 super.handleConfigurationUpdate(configurationParameters);
276 String response = updateKonnectedModule();
277 logger.trace("The response from the konnected module with thingID {} was {}",
278 getThing().getUID().toString(), response);
279 if (response == null) {
280 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
281 "Unable to communicate with Konnected Module confirm settings.");
283 updateStatus(ThingStatus.ONLINE);
285 } catch (KonnectedHttpRetryExceeded e) {
286 logger.trace("The number of retries was exceeeded during the HandleConfigurationUpdate(): {}",
288 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
293 public void initialize() {
294 updateStatus(ThingStatus.UNKNOWN);
296 checkConfiguration();
297 } catch (ConfigValidationException e) {
298 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getMessage());
300 scheduler.execute(() -> {
302 String response = updateKonnectedModule();
303 if (response == null) {
304 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
305 "Unable to communicate with Konnected Module confirm settings or readd thing.");
307 updateStatus(ThingStatus.ONLINE);
309 } catch (KonnectedHttpRetryExceeded e) {
310 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
316 public void dispose() {
317 logger.debug("Running dispose()");
322 * This method constructs the payload that will be sent
323 * to the Konnected module via the put request
324 * it adds the appropriate sensors and actuators to the {@link KonnectedModulePayload}
325 * as well as the location of the callback {@link KonnectedJTTPServlet}
326 * and auth_token which can be used for validation
328 * @return a json settings payload which can be sent to the Konnected Module based on the Thing
330 private String constructSettingsPayload() {
331 logger.debug("The Auth_Token is: {}", authToken);
332 KonnectedModulePayload payload = new KonnectedModulePayload(authToken, callbackUrl);
333 payload.setBlink(config.blink);
334 payload.setDiscovery(config.discovery);
335 this.getThing().getChannels().forEach(channel -> {
336 // ChannelUID channelId = channel.getUID();
337 if (isLinked(channel.getUID())) {
338 // adds linked channels to list based on last value of Channel ID
339 // which is set to a number
340 // get the zone number in integer form
341 String zone = (String) channel.getConfiguration().get(CHANNEL_ZONE);
342 // if the pin is an actuator add to actuator string
343 // else add to sensor string
344 // This is determined based off of the accepted item type, contact types are sensors
345 // switch types are actuators
346 String channelType = channel.getChannelTypeUID().getAsString();
347 logger.debug("The channeltypeID is: {}", channelType);
348 KonnectedModuleGson module = new KonnectedModuleGson();
349 module.setZone(thingID, zone);
350 if (channelType.contains(CHANNEL_SWITCH)) {
351 payload.addSensor(module);
352 logger.trace("Channel {} will be configured on the konnected alarm panel as a switch",
354 } else if (channelType.contains(CHANNEL_ACTUATOR)) {
355 payload.addActuators(module);
356 logger.trace("Channel {} will be configured on the konnected alarm panel as an actuator",
358 } else if (channelType.contains(CHANNEL_HUMIDITY)) {
359 // the humidity channels do not need to be added because the supported sensor (dht22) is added under
361 logger.trace("Channel {} is a humidity channel.", channel.toString());
362 } else if (channelType.contains(CHANNEL_TEMPERATURE)) {
363 logger.trace("Channel {} will be configured on the konnected alarm panel as a temperature sensor",
365 Configuration configuration = channel.getConfiguration();
366 if (configuration.get(CHANNEL_TEMPERATRUE_POLL) == null) {
367 module.setPollInterval(3);
369 module.setPollInterval(((BigDecimal) configuration.get(CHANNEL_TEMPERATRUE_POLL)).intValue());
371 logger.trace("The Temperature Sensor Type is: {} ",
372 configuration.get(CHANNEL_TEMPERATURE_TYPE).toString());
373 if ((boolean) configuration.get(CHANNEL_TEMPERATURE_TYPE)) {
374 // add it as a dht22 module
375 payload.addDht22(module);
377 "Channel {} will be configured on the konnected alarm panel as a DHT22 temperature sensor",
380 // add to payload as a DS18B20 module if the parameter is false
381 payload.addDs18b20(module);
383 "Channel {} will be configured on the konnected alarm panel as a DS18B20 temperature sensor",
387 logger.debug("Channel {} is of type {} which is not supported by the konnected binding",
388 channel.toString(), channelType);
391 logger.debug("The Channel {} is not linked to an item", channel.getUID());
394 // Create Json to Send to Konnected Module
396 String payloadString = gson.toJson(payload);
397 logger.debug("The payload is: {}", payloadString);
398 return payloadString;
402 * Prepares and sends the {@link KonnectedModulePayload} via the {@link KonnectedHttpUtils}
404 * @return response obtained from sending the settings payload to Konnected module defined by the thing
406 * @throws KonnectedHttpRetryExceeded if unable to communicate with the Konnected module defined by the Thing
408 private String updateKonnectedModule() throws KonnectedHttpRetryExceeded {
409 String payload = constructSettingsPayload();
410 String response = http.doPut(baseUrl + "/settings", payload, retryCount);
411 logger.debug("The response of the put request was: {}", response);
416 * Sends a command to the module via {@link KonnectedHTTPUtils}
418 * @param scommand the string command, either 0 or 1 to send to the actutor pin on the Konnected module
419 * @param zone the zone to send the command to on the Konnected Module
421 private void sendActuatorCommand(Integer scommand, String zone, ChannelUID channelId) {
423 Channel channel = getThing().getChannel(channelId.getId());
424 if (!(channel == null)) {
425 logger.debug("getasstring: {} getID: {} getGroupId: {} toString:{}", channelId.getAsString(),
426 channelId.getId(), channelId.getGroupId(), channelId.toString());
427 Configuration configuration = channel.getConfiguration();
428 KonnectedModuleGson payload = new KonnectedModuleGson();
429 payload.setState(scommand);
431 payload.setZone(thingID, zone);
433 // check to see if this is an On Command type, if so add the momentary, pause, times to the payload if
434 // they exist on the configuration.
435 if (scommand == getOnState(channel)) {
436 if (configuration.get(CHANNEL_ACTUATOR_TIMES) == null) {
438 "The times configuration was not set for channelID: {}, not adding it to the payload.",
439 channelId.toString());
441 payload.setTimes(configuration.get(CHANNEL_ACTUATOR_TIMES).toString());
442 logger.debug("The times configuration was set to: {} for channelID: {}.",
443 configuration.get(CHANNEL_ACTUATOR_TIMES).toString(), channelId.toString());
445 if (configuration.get(CHANNEL_ACTUATOR_MOMENTARY) == null) {
447 "The momentary configuration was not set for channelID: {}, not adding it to the payload.",
448 channelId.toString());
450 payload.setMomentary(configuration.get(CHANNEL_ACTUATOR_MOMENTARY).toString());
451 logger.debug("The momentary configuration set to: {} channelID: {}.",
452 configuration.get(CHANNEL_ACTUATOR_MOMENTARY).toString(), channelId.toString());
454 if (configuration.get(CHANNEL_ACTUATOR_PAUSE) == null) {
456 "The pause configuration was not set for channelID: {}, not adding it to the payload.",
457 channelId.toString());
459 payload.setPause(configuration.get(CHANNEL_ACTUATOR_PAUSE).toString());
460 logger.debug("The pause configuration was set to: {} for channelID: {}.",
461 configuration.get(CHANNEL_ACTUATOR_PAUSE).toString(), channelId.toString());
464 String payloadString = gson.toJson(payload);
465 logger.debug("The command payload is: {}", payloadString);
467 switch (this.thingID) {
475 http.doPut(baseUrl + path, payloadString, retryCount);
477 logger.debug("The channel {} returned null for channelId.getID(): {}", channelId.toString(),
480 } catch (KonnectedHttpRetryExceeded e) {
481 logger.debug("Attempting to set the state of the actuator on thing {} failed: {}",
482 this.thing.getUID().getId(), e.getMessage());
483 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
484 "Unable to communicate with Konnected Alarm Panel confirm settings, and that module is online.");
488 private void getSwitchState(String zone, ChannelUID channelId) {
489 Channel channel = getThing().getChannel(channelId.getId());
490 if (!(channel == null)) {
491 logger.debug("getasstring: {} getID: {} getGroupId: {} toString:{}", channelId.getAsString(),
492 channelId.getId(), channelId.getGroupId(), channelId.toString());
493 KonnectedModuleGson payload = new KonnectedModuleGson();
494 payload.setZone(thingID, zone);
495 String payloadString = gson.toJson(payload);
496 logger.debug("The command payload is: {}", payloadString);
498 sendSetSwitchState(thingID, payloadString);
499 } catch (KonnectedHttpRetryExceeded e) {
500 // try to get the state of the device one more time 30 seconds later. This way it can be confirmed if
501 // the device was simply in a reboot loop when device state was attempted the first time
502 scheduler.schedule(() -> {
504 sendSetSwitchState(thingID, payloadString);
505 } catch (KonnectedHttpRetryExceeded ex) {
506 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
507 "Unable to communicate with Konnected Alarm Panel confirm settings, and that module is online.");
508 logger.debug("Attempting to get the state of the zone on thing {} failed for channel: {} : {}",
509 this.thing.getUID().getId(), channelId.getAsString(), ex.getMessage());
511 }, 2, TimeUnit.MINUTES);
514 logger.debug("The channel {} returned null for channelId.getID(): {}", channelId.toString(),
519 private void sendSetSwitchState(String thingId, String payloadString) throws KonnectedHttpRetryExceeded {
520 String path = thingId.equals(WIFI_MODULE) ? "/device" : "/zone";
521 String response = http.doGet(baseUrl + path, payloadString, retryCount);
522 KonnectedModuleGson[] events = gson.fromJson(response, KonnectedModuleGson[].class);
523 for (KonnectedModuleGson event : events) {
524 this.handleWebHookEvent(event);
528 private int getOnState(Channel channel) {
529 return ((Number) channel.getConfiguration().get(CHANNEL_ONVALUE)).intValue();