2 * Copyright (c) 2010-2023 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.*;
18 import java.util.Map.Entry;
19 import java.util.concurrent.TimeUnit;
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;
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 callbackUrl the webaddress of the openHAB server instance obtained by the runtime
70 public KonnectedHandler(Thing thing, String callbackUrl) {
72 this.callbackUrl = callbackUrl;
73 logger.debug("The auto discovered callback URL is: {}", this.callbackUrl);
75 thingID = getThing().getThingTypeUID().getId();
76 authToken = getThing().getUID().getAsString();
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");
92 sendActuatorCommand(onOffCommand, zone, channelUID);
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);
99 scheduler.schedule(() -> {
100 handleCommand(channelUID, command);
101 }, 1, TimeUnit.MINUTES);
107 * Process a {@link KonnectedModuleGson} that has been received by the Servlet from a Konnected module with respect
108 * to a sensor event or status update request
110 * @param event the {@link KonnectedModuleGson} event that contains the state and pin information to be processed
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(),
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)) {
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);
138 OnOffType onOffType = state == zoneConfig.onValue ? OnOffType.ON : OnOffType.OFF;
139 updateState(channelId, onOffType);
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));
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());
153 if (event.getAddr().equalsIgnoreCase(zoneConfig.ds18b20Address)) {
154 updateState(channelId,
155 new QuantityType<>(Double.parseDouble(event.getTemp()), SIUnits.CELSIUS));
157 logger.debug("The address of {} does not match {} not updating this channel",
158 event.getAddr(), zoneConfig.ds18b20Address);
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());
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;
180 getThing().getConfiguration().put(CALLBACK_URL, callbackUrl);
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);
187 this.retryCount = Integer.parseInt(testRetryCount);
188 } catch (NumberFormatException e) {
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",
195 this.http.setRequestTimeout(Integer.parseInt(testRequestTimeout));
196 } catch (NumberFormatException e) {
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",
202 if ((callbackUrl == null)) {
203 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Unable to obtain callback URL");
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 ("softreset".equals(cfg[1]) && value instanceof Boolean bool && bool) {
228 scheduler.execute(() -> {
230 http.doGet(baseUrl + "/settings?restart=true", null, retryCount);
231 } catch (KonnectedHttpRetryExceeded e) {
232 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
235 } else if ("removewifi".equals(cfg[1]) && value instanceof Boolean bool && bool) {
236 scheduler.execute(() -> {
238 http.doGet(baseUrl + "/settings?restore=true", null, retryCount);
239 } catch (KonnectedHttpRetryExceeded e) {
240 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
243 } else if ("sendConfig".equals(cfg[1]) && value instanceof Boolean bool && bool) {
244 scheduler.execute(() -> {
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.");
253 updateStatus(ThingStatus.ONLINE);
255 } catch (KonnectedHttpRetryExceeded e) {
256 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
263 super.handleConfigurationUpdate(configurationParameters);
267 String response = updateKonnectedModule();
268 logger.trace("The response from the konnected module with thingID {} was {}", getThing().getUID(),
270 if (response == null) {
271 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
272 "Unable to communicate with Konnected Module confirm settings.");
274 updateStatus(ThingStatus.ONLINE);
276 } catch (KonnectedHttpRetryExceeded e) {
277 logger.trace("The number of retries was exceeeded during the HandleConfigurationUpdate(): {}",
279 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
284 public void initialize() {
285 updateStatus(ThingStatus.UNKNOWN);
287 checkConfiguration();
288 } catch (ConfigValidationException e) {
289 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getMessage());
291 scheduler.execute(() -> {
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.");
298 updateStatus(ThingStatus.ONLINE);
300 } catch (KonnectedHttpRetryExceeded e) {
301 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
307 public void dispose() {
308 logger.debug("Running dispose()");
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
319 * @return a json settings payload which can be sent to the Konnected Module based on the Thing
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
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",
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);
359 "Channel {} will be configured on the konnected alarm panel as a DHT22 temperature sensor",
362 // add to payload as a DS18B20 module if the parameter is false
363 payload.addDs18b20(module);
365 "Channel {} will be configured on the konnected alarm panel as a DS18B20 temperature sensor",
369 logger.debug("Channel {} is of type {} which is not supported by the konnected binding", channel,
373 // Create Json to Send to Konnected Module
375 String payloadString = gson.toJson(payload);
376 logger.debug("The payload is: {}", payloadString);
377 return payloadString;
381 * Prepares and sends the {@link KonnectedModulePayload} via the {@link KonnectedHttpUtils}
383 * @return response obtained from sending the settings payload to Konnected module defined by the thing
385 * @throws KonnectedHttpRetryExceeded if unable to communicate with the Konnected module defined by the Thing
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);
395 * Sends a command to the module via {@link KonnectedHTTPUtils}
397 * @param command the state to send to the actuator
398 * @param zone the zone to send the command to on the Konnected Module
400 private void sendActuatorCommand(OnOffType command, String zone, ChannelUID channelId) {
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);
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);
416 payload.setState(zoneConfig.onValue == 1 ? 0 : 1);
419 String payloadString = gson.toJson(payload);
420 logger.debug("The command payload is: {}", payloadString);
422 switch (this.thingID) {
430 http.doPut(baseUrl + path, payloadString, retryCount);
432 logger.debug("The channel {} returned null for channelId.getID(): {}", channelId, channelId.getId());
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.");
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);
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(() -> {
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());
465 }, 2, TimeUnit.MINUTES);
468 logger.debug("The channel {} returned null for channelId.getID(): {}", channelId, channelId.getId());
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);