]> git.basschouten.com Git - openhab-addons.git/blob
a4bdb083bd2ec4c0685fefb99e4f38c9fdcd5330
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2020 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.io.neeo.internal.servletservices;
14
15 import java.beans.PropertyChangeEvent;
16 import java.beans.PropertyChangeListener;
17 import java.io.IOException;
18 import java.util.Map.Entry;
19 import java.util.Objects;
20 import java.util.concurrent.ScheduledExecutorService;
21
22 import javax.servlet.http.HttpServletRequest;
23 import javax.servlet.http.HttpServletResponse;
24 import javax.ws.rs.client.ClientBuilder;
25
26 import org.apache.commons.lang.StringUtils;
27 import org.eclipse.jdt.annotation.NonNull;
28 import org.eclipse.jdt.annotation.NonNullByDefault;
29 import org.eclipse.jdt.annotation.Nullable;
30 import org.openhab.core.common.ThreadPoolManager;
31 import org.openhab.core.events.Event;
32 import org.openhab.core.events.EventFilter;
33 import org.openhab.core.items.Item;
34 import org.openhab.core.items.ItemNotFoundException;
35 import org.openhab.core.items.events.ItemCommandEvent;
36 import org.openhab.core.items.events.ItemEventFactory;
37 import org.openhab.core.items.events.ItemStateChangedEvent;
38 import org.openhab.core.library.types.OnOffType;
39 import org.openhab.core.thing.ChannelUID;
40 import org.openhab.core.thing.ThingUID;
41 import org.openhab.core.thing.events.ChannelTriggeredEvent;
42 import org.openhab.core.thing.events.ThingEventFactory;
43 import org.openhab.core.types.Command;
44 import org.openhab.core.types.State;
45 import org.openhab.io.neeo.internal.NeeoApi;
46 import org.openhab.io.neeo.internal.NeeoConstants;
47 import org.openhab.io.neeo.internal.NeeoDeviceKeys;
48 import org.openhab.io.neeo.internal.NeeoItemValueConverter;
49 import org.openhab.io.neeo.internal.NeeoUtil;
50 import org.openhab.io.neeo.internal.ServiceContext;
51 import org.openhab.io.neeo.internal.models.ButtonInfo;
52 import org.openhab.io.neeo.internal.models.NeeoButtonGroup;
53 import org.openhab.io.neeo.internal.models.NeeoCapabilityType;
54 import org.openhab.io.neeo.internal.models.NeeoDevice;
55 import org.openhab.io.neeo.internal.models.NeeoDeviceChannel;
56 import org.openhab.io.neeo.internal.models.NeeoDeviceChannelDirectory;
57 import org.openhab.io.neeo.internal.models.NeeoDeviceChannelKind;
58 import org.openhab.io.neeo.internal.models.NeeoDirectoryRequest;
59 import org.openhab.io.neeo.internal.models.NeeoDirectoryRequestAction;
60 import org.openhab.io.neeo.internal.models.NeeoDirectoryResult;
61 import org.openhab.io.neeo.internal.models.NeeoItemValue;
62 import org.openhab.io.neeo.internal.models.NeeoNotification;
63 import org.openhab.io.neeo.internal.models.NeeoSensorNotification;
64 import org.openhab.io.neeo.internal.models.NeeoThingUID;
65 import org.openhab.io.neeo.internal.net.HttpRequest;
66 import org.openhab.io.neeo.internal.servletservices.models.PathInfo;
67 import org.openhab.io.neeo.internal.servletservices.models.ReturnStatus;
68 import org.slf4j.Logger;
69 import org.slf4j.LoggerFactory;
70
71 import com.google.gson.Gson;
72
73 /**
74  * The implementation of {@link ServletService} that will handle device callbacks from the Neeo Brain
75  *
76  * @author Tim Roberts - Initial Contribution
77  */
78 @NonNullByDefault
79 public class NeeoBrainService extends DefaultServletService {
80
81     /** The logger */
82     private final Logger logger = LoggerFactory.getLogger(NeeoBrainService.class);
83
84     /** The gson used for communications */
85     private final Gson gson = NeeoUtil.createGson();
86
87     /** The NEEO API to use */
88     private final NeeoApi api;
89
90     /** The service context */
91     private final ServiceContext context;
92
93     /** The HTTP request */
94     private final HttpRequest request;
95
96     /** The scheduler to use to schedule recipe execution */
97     private final ScheduledExecutorService scheduler = ThreadPoolManager
98             .getScheduledPool(NeeoConstants.THREAD_POOL_NAME);
99
100     /** The {@link NeeoItemValueConverter} used to convert values with */
101     private final NeeoItemValueConverter itemConverter;
102
103     private final PropertyChangeListener listener = new PropertyChangeListener() {
104         @Override
105         public void propertyChange(@Nullable PropertyChangeEvent evt) {
106             if (evt != null && (Boolean) evt.getNewValue()) {
107                 resendState();
108             }
109         }
110     };
111
112     /**
113      * Constructs the service from the {@link NeeoApi} and {@link ServiceContext}
114      *
115      * @param api the non-null api
116      * @param context the non-null context
117      */
118     public NeeoBrainService(NeeoApi api, ServiceContext context, ClientBuilder clientBuilder) {
119         Objects.requireNonNull(api, "api cannot be null");
120         Objects.requireNonNull(context, "context cannot be null");
121
122         this.context = context;
123         this.itemConverter = new NeeoItemValueConverter(context);
124         this.api = api;
125         this.api.addPropertyChangeListener(NeeoApi.CONNECTED, listener);
126         scheduler.execute(() -> {
127             resendState();
128         });
129         request = new HttpRequest(clientBuilder);
130     }
131
132     /**
133      * Returns true if the path start with 'device' or ends with either 'subscribe' or 'unsubscribe'
134      *
135      * @see DefaultServletService#canHandleRoute(String[])
136      */
137     @Override
138     public boolean canHandleRoute(String[] paths) {
139         Objects.requireNonNull(paths, "paths cannot be null");
140
141         if (paths.length == 0) {
142             return false;
143         }
144
145         if (StringUtils.equalsIgnoreCase(paths[0], "device")) {
146             return true;
147         }
148
149         final String lastPath = paths.length >= 2 ? paths[1] : null;
150         return StringUtils.equalsIgnoreCase(lastPath, "subscribe")
151                 || StringUtils.equalsIgnoreCase(lastPath, "unsubscribe");
152     }
153
154     @Override
155     public void handlePost(HttpServletRequest req, String[] paths, HttpServletResponse resp) throws IOException {
156         Objects.requireNonNull(req, "req cannot be null");
157         Objects.requireNonNull(paths, "paths cannot be null");
158         Objects.requireNonNull(resp, "resp cannot be null");
159         if (paths.length == 0) {
160             throw new IllegalArgumentException("paths cannot be empty");
161         }
162
163         final boolean hasDeviceStart = StringUtils.equalsIgnoreCase(paths[0], "device");
164
165         if (hasDeviceStart) {
166             final PathInfo pathInfo = new PathInfo(paths);
167
168             if (StringUtils.equalsIgnoreCase("directory", pathInfo.getComponentType())) {
169                 handleDirectory(req, resp, pathInfo);
170             } else {
171                 logger.debug("Unknown/unhandled brain service device route (POST): {}", StringUtils.join(paths, '/'));
172
173             }
174         } else {
175             logger.debug("Unknown/unhandled brain service route (POST): {}", StringUtils.join(paths, '/'));
176         }
177     }
178
179     @Override
180     public void handleGet(HttpServletRequest req, String[] paths, HttpServletResponse resp) throws IOException {
181         Objects.requireNonNull(req, "req cannot be null");
182         Objects.requireNonNull(paths, "paths cannot be null");
183         Objects.requireNonNull(resp, "resp cannot be null");
184         if (paths.length == 0) {
185             throw new IllegalArgumentException("paths cannot be empty");
186         }
187
188         // Paths handled specially
189         // 1. See PATHINFO for various /device/* keys (except for the next)
190         // 2. New subscribe path: /device/{thingUID}/subscribe/default/{devicekey}
191         // 3. New unsubscribe path: /device/{thingUID}/unsubscribe/default
192         // 4. Old subscribe path: /{thingUID}/subscribe or unsubscribe/{deviceid}/{devicekey}
193         // 4. Old unsubscribe path: /{thingUID}/subscribe or unsubscribe/{deviceid}
194
195         final boolean hasDeviceStart = StringUtils.equalsIgnoreCase(paths[0], "device");
196         if (hasDeviceStart && (paths.length >= 3 && !StringUtils.equalsIgnoreCase(paths[2], "subscribe")
197                 && !StringUtils.equalsIgnoreCase(paths[2], "unsubscribe"))) {
198             try {
199                 final PathInfo pathInfo = new PathInfo(paths);
200
201                 if (StringUtils.isEmpty(pathInfo.getActionValue())) {
202                     handleGetValue(resp, pathInfo);
203                 } else {
204                     handleSetValue(resp, pathInfo);
205                 }
206             } catch (IllegalArgumentException e) {
207                 logger.debug("Bad path: {} - {}", StringUtils.join(paths), e.getMessage(), e);
208             }
209         } else {
210             int idx = hasDeviceStart ? 1 : 0;
211
212             if (idx + 2 < paths.length) {
213                 final String adapterName = paths[idx++];
214                 final String action = StringUtils.lowerCase(paths[idx++]);
215                 idx++; // deviceId/default - not used
216
217                 switch (action) {
218                     case "subscribe":
219                         if (idx < paths.length) {
220                             final String deviceKey = paths[idx++];
221                             handleSubscribe(resp, adapterName, deviceKey);
222                         } else {
223                             logger.debug("No device key set for a subscribe action: {}", StringUtils.join(paths, '/'));
224                         }
225                         break;
226                     case "unsubscribe":
227                         handleUnsubscribe(resp, adapterName);
228                         break;
229                     default:
230                         logger.debug("Unknown action: {}", action);
231                 }
232
233             } else {
234                 logger.debug("Unknown/unhandled brain service route (GET): {}", StringUtils.join(paths, '/'));
235             }
236         }
237     }
238
239     /**
240      * Handle set value from the path
241      *
242      * @param resp the non-null response to write the response to
243      * @param pathInfo the non-null path information
244      */
245     private void handleSetValue(HttpServletResponse resp, PathInfo pathInfo) {
246         Objects.requireNonNull(resp, "resp cannot be null");
247         Objects.requireNonNull(pathInfo, "pathInfo cannot be null");
248
249         logger.debug("handleSetValue {}", pathInfo);
250         final NeeoDevice device = context.getDefinitions().getDevice(pathInfo.getThingUid());
251         if (device != null) {
252             final NeeoDeviceChannel channel = device.getChannel(pathInfo.getItemName(), pathInfo.getSubType(),
253                     pathInfo.getChannelNbr());
254             if (channel != null && channel.getKind() == NeeoDeviceChannelKind.TRIGGER) {
255                 final ChannelTriggeredEvent event = ThingEventFactory.createTriggerEvent(channel.getValue(),
256                         new ChannelUID(device.getUid(), channel.getItemName()));
257                 logger.debug("Posting triggered event: {}", event);
258                 context.getEventPublisher().post(event);
259             } else {
260                 try {
261                     final Item item = context.getItemRegistry().getItem(pathInfo.getItemName());
262                     final Command cmd = NeeoItemValueConverter.convert(item, pathInfo);
263                     if (cmd != null) {
264                         final ItemCommandEvent event = ItemEventFactory.createCommandEvent(item.getName(), cmd);
265                         logger.debug("Posting item event: {}", event);
266                         context.getEventPublisher().post(event);
267                     } else {
268                         logger.debug("Cannot set value - no command for path: {}", pathInfo);
269                     }
270                 } catch (ItemNotFoundException e) {
271                     logger.debug("Cannot set value - no linked items: {}", pathInfo);
272                 }
273             }
274         } else {
275             logger.debug("Cannot set value - no device definition: {}", pathInfo);
276         }
277     }
278
279     /**
280      * Handle set value from the path
281      *
282      * @param resp the non-null response to write the response to
283      * @param pathInfo the non-null path information
284      * @throws IOException Signals that an I/O exception has occurred.
285      */
286     private void handleGetValue(HttpServletResponse resp, PathInfo pathInfo) throws IOException {
287         Objects.requireNonNull(resp, "resp cannot be null");
288         Objects.requireNonNull(pathInfo, "pathInfo cannot be null");
289
290         NeeoItemValue niv = new NeeoItemValue("");
291
292         try {
293             final NeeoDevice device = context.getDefinitions().getDevice(pathInfo.getThingUid());
294             if (device != null) {
295                 final NeeoDeviceChannel channel = device.getChannel(pathInfo.getItemName(), pathInfo.getSubType(),
296                         pathInfo.getChannelNbr());
297                 if (channel != null && channel.getKind() == NeeoDeviceChannelKind.ITEM) {
298                     try {
299                         final Item item = context.getItemRegistry().getItem(pathInfo.getItemName());
300                         niv = itemConverter.convert(channel, item.getState());
301                     } catch (ItemNotFoundException e) {
302                         logger.debug("Item '{}' not found to get a value ({})", pathInfo.getItemName(), pathInfo);
303                     }
304                 } else {
305                     logger.debug("Channel definition for '{}' not found to get a value ({})", pathInfo.getItemName(),
306                             pathInfo);
307                 }
308             } else {
309                 logger.debug("Device definition for '{}' not found to get a value ({})", pathInfo.getItemName(),
310                         pathInfo);
311             }
312
313             NeeoUtil.write(resp, gson.toJson(niv));
314         } finally {
315             logger.debug("handleGetValue {}: {}", pathInfo, niv.getValue());
316         }
317     }
318
319     /**
320      * Handle unsubscribing from a device by removing all device keys for the related {@link ThingUID}
321      *
322      * @param resp the non-null response to write to
323      * @param adapterName the non-empty adapter name
324      * @throws IOException Signals that an I/O exception has occurred.
325      */
326     private void handleUnsubscribe(HttpServletResponse resp, String adapterName) throws IOException {
327         Objects.requireNonNull(resp, "resp cannot be null");
328         NeeoUtil.requireNotEmpty(adapterName, "adapterName cannot be empty");
329
330         logger.debug("handleUnsubscribe {}", adapterName);
331
332         try {
333             final NeeoThingUID uid = new NeeoThingUID(adapterName);
334             api.getDeviceKeys().remove(uid);
335             NeeoUtil.write(resp, gson.toJson(ReturnStatus.SUCCESS));
336         } catch (IllegalArgumentException e) {
337             logger.debug("AdapterName {} is not a valid thinguid - ignoring", adapterName);
338             NeeoUtil.write(resp, gson.toJson(new ReturnStatus("AdapterName not a valid ThingUID: " + adapterName)));
339         }
340     }
341
342     /**
343      * Handle subscribe to a device by adding the device key to the API for the related {@link ThingUID}
344      *
345      * @param resp the non-null response to write to
346      * @param adapterName the non-empty adapter name
347      * @param deviceKey the non-empty device key
348      * @throws IOException Signals that an I/O exception has occurred.
349      */
350     private void handleSubscribe(HttpServletResponse resp, String adapterName, String deviceKey) throws IOException {
351         Objects.requireNonNull(resp, "resp cannot be null");
352         NeeoUtil.requireNotEmpty(adapterName, "adapterName cannot be empty");
353         NeeoUtil.requireNotEmpty(deviceKey, "deviceKey cannot be empty");
354
355         logger.debug("handleSubscribe {}/{}", adapterName, deviceKey);
356
357         try {
358             final NeeoThingUID uid = new NeeoThingUID(adapterName);
359             api.getDeviceKeys().put(uid, deviceKey);
360             NeeoUtil.write(resp, gson.toJson(ReturnStatus.SUCCESS));
361         } catch (IllegalArgumentException e) {
362             logger.debug("AdapterName {} is not a valid thinguid - ignoring", adapterName);
363             NeeoUtil.write(resp, gson.toJson(new ReturnStatus("AdapterName not a valid ThingUID: " + adapterName)));
364         }
365     }
366
367     /**
368      * Handle a directory request
369      *
370      * @param req the non-null request to use
371      * @param resp the non-null response to write to
372      * @param pathInfo the non-null path information
373      * @throws IOException Signals that an I/O exception has occurred.
374      */
375     private void handleDirectory(HttpServletRequest req, HttpServletResponse resp, PathInfo pathInfo)
376             throws IOException {
377         Objects.requireNonNull(req, "req cannot be null");
378         Objects.requireNonNull(resp, "resp cannot be null");
379         Objects.requireNonNull(pathInfo, "pathInfo cannot be null");
380
381         logger.debug("handleDirectory {}", pathInfo);
382
383         final NeeoDevice device = context.getDefinitions().getDevice(pathInfo.getThingUid());
384         if (device != null) {
385             final NeeoDeviceChannel channel = device.getChannel(pathInfo.getItemName(), pathInfo.getSubType(),
386                     pathInfo.getChannelNbr());
387             if (StringUtils.equalsIgnoreCase("action", pathInfo.getActionValue())) {
388                 final NeeoDirectoryRequestAction discoveryAction = gson.fromJson(req.getReader(),
389                         NeeoDirectoryRequestAction.class);
390
391                 try {
392                     final Item item = context.getItemRegistry().getItem(pathInfo.getItemName());
393                     final Command cmd = NeeoItemValueConverter.convert(item, pathInfo,
394                             discoveryAction.getActionIdentifier());
395                     if (cmd != null) {
396                         final ItemCommandEvent event = ItemEventFactory.createCommandEvent(item.getName(), cmd);
397                         logger.debug("Posting item event: {}", event);
398                         context.getEventPublisher().post(event);
399                     } else {
400                         logger.debug("Cannot set value (directory) - no command for path: {}", pathInfo);
401                     }
402                 } catch (ItemNotFoundException e) {
403                     logger.debug("Cannot set value(directory)  - no linked items: {}", pathInfo);
404                 }
405
406             } else {
407                 if (channel instanceof NeeoDeviceChannelDirectory) {
408                     final NeeoDirectoryRequest discoveryRequest = gson.fromJson(req.getReader(),
409                             NeeoDirectoryRequest.class);
410                     final NeeoDeviceChannelDirectory directoryChannel = (NeeoDeviceChannelDirectory) channel;
411                     NeeoUtil.write(resp, gson.toJson(new NeeoDirectoryResult(discoveryRequest, directoryChannel)));
412                 } else {
413                     logger.debug("Channel definition for '{}' not found to directory set value ({})",
414                             pathInfo.getItemName(), pathInfo);
415                 }
416             }
417         } else {
418             logger.debug("Device definition for '{}' not found to directory set value ({})", pathInfo.getItemName(),
419                     pathInfo);
420
421         }
422     }
423
424     /**
425      * Returns the {@link EventFilter} used by this service. The {@link EventFilter} will simply filter for those items
426      * that have been bound
427      *
428      * @return a non-null {@link EventFilter}
429      */
430     @NonNull
431     @Override
432     public EventFilter getEventFilter() {
433         return new EventFilter() {
434
435             @Override
436             public boolean apply(@Nullable Event event) {
437                 Objects.requireNonNull(event, "event cannot be null");
438
439                 final ItemStateChangedEvent ise = (ItemStateChangedEvent) event;
440                 final String itemName = ise.getItemName();
441
442                 final NeeoDeviceKeys keys = api.getDeviceKeys();
443                 final boolean isBound = context.getDefinitions().isBound(keys, itemName);
444                 logger.trace("Apply Event: {} --- {} --- {} = {}", event, itemName, isBound, keys);
445                 return isBound;
446             }
447         };
448     }
449
450     /**
451      * Handles the event by notifying the NEEO brain of the new value. If the channel has been linked to the
452      * {@link NeeoButtonGroup#POWERONOFF}, then the related recipe will be powered on/off (in addition to sending the
453      * new value).
454      *
455      * @see DefaultServletService#handleEvent(Event)
456      *
457      */
458     @Override
459     public boolean handleEvent(Event event) {
460         Objects.requireNonNull(event, "event cannot be null");
461
462         final ItemStateChangedEvent ise = (ItemStateChangedEvent) event;
463         final String itemName = ise.getItemName();
464
465         logger.trace("handleEvent: {}", event);
466         notifyState(itemName, ise.getItemState());
467
468         return true;
469     }
470
471     /**
472      * Helper function to send the current state of all bound channels
473      */
474     private void resendState() {
475         for (final Entry<NeeoDevice, NeeoDeviceChannel> boundEntry : context.getDefinitions()
476                 .getBound(api.getDeviceKeys())) {
477
478             final NeeoDevice device = boundEntry.getKey();
479             final NeeoDeviceChannel channel = boundEntry.getValue();
480
481             try {
482                 final State state = context.getItemRegistry().getItem(channel.getItemName()).getState();
483
484                 for (String deviceKey : api.getDeviceKeys().get(device.getUid())) {
485                     sendNotification(channel, deviceKey, state);
486                 }
487             } catch (ItemNotFoundException e) {
488                 logger.debug("Item not found {}", channel.getItemName());
489             }
490         }
491     }
492
493     /**
494      * Helper function to send some state for an itemName to the brain
495      *
496      * @param itemName a non-null, non-empty item name
497      * @param state a non-null state
498      */
499     private void notifyState(String itemName, State state) {
500         NeeoUtil.requireNotEmpty(itemName, "itemName cannot be empty");
501         Objects.requireNonNull(state, "state cannot be null");
502
503         logger.trace("notifyState: {} --- {}", itemName, state);
504
505         for (final Entry<NeeoDevice, NeeoDeviceChannel> boundEntry : context.getDefinitions()
506                 .getBound(api.getDeviceKeys(), itemName)) {
507             final NeeoDevice device = boundEntry.getKey();
508             final NeeoDeviceChannel channel = boundEntry.getValue();
509             final NeeoThingUID uid = new NeeoThingUID(device.getUid());
510
511             logger.trace("notifyState (device): {} --- {} ", uid, channel);
512             for (String deviceKey : api.getDeviceKeys().get(uid)) {
513                 logger.trace("notifyState (key): {} --- {}", uid, deviceKey);
514
515                 if (state instanceof OnOffType) {
516                     Boolean recipeState = null;
517                     final String label = channel.getLabel();
518                     if (StringUtils.equalsIgnoreCase(NeeoButtonGroup.POWERONOFF.getText(), label)) {
519                         recipeState = state == OnOffType.ON;
520                     } else if (state == OnOffType.ON
521                             && StringUtils.equalsIgnoreCase(ButtonInfo.POWERON.getLabel(), label)) {
522                         recipeState = true;
523                     } else if (state == OnOffType.OFF
524                             && StringUtils.equalsIgnoreCase(ButtonInfo.POWEROFF.getLabel(), label)) {
525                         recipeState = false;
526                     }
527
528                     if (recipeState != null) {
529                         logger.trace("notifyState (executeRecipe): {} --- {} --- {}", uid, deviceKey, recipeState);
530                         final boolean turnOn = recipeState;
531                         scheduler.submit(() -> {
532                             try {
533                                 api.executeRecipe(deviceKey, turnOn);
534                             } catch (IOException e) {
535                                 logger.debug("Exception occurred while handling executing a recipe: {}", e.getMessage(),
536                                         e);
537                             }
538                         });
539                     }
540                 }
541
542                 sendNotification(channel, deviceKey, state);
543             }
544         }
545     }
546
547     /**
548      * Helper method to send a notification
549      *
550      * @param channel a non-null channel
551      * @param deviceKey a non-null, non-empty device id
552      * @param state a non-null state
553      */
554     private void sendNotification(NeeoDeviceChannel channel, String deviceKey, State state) {
555         Objects.requireNonNull(channel, "channel cannot be null");
556         NeeoUtil.requireNotEmpty(deviceKey, "deviceKey cannot be empty");
557         Objects.requireNonNull(state, "state cannot be null");
558
559         scheduler.execute(() -> {
560             final String uin = channel.getUniqueItemName();
561
562             final NeeoItemValue niv = itemConverter.convert(channel, state);
563
564             // Use sensor notification if we have a >= 0.50 firmware AND it's not a power sensor
565             if (api.getSystemInfo().isFirmwareGreaterOrEqual(NeeoConstants.NEEO_FIRMWARE_0_51_1)
566                     && channel.getType() != NeeoCapabilityType.SENSOR_POWER) {
567                 final NeeoSensorNotification notify = new NeeoSensorNotification(deviceKey, uin, niv.getValue());
568                 try {
569                     api.notify(gson.toJson(notify));
570                 } catch (IOException e) {
571                     logger.debug("Exception occurred while handling event: {}", e.getMessage(), e);
572                 }
573             } else {
574                 final NeeoNotification notify = new NeeoNotification(deviceKey, uin, niv.getValue());
575                 try {
576                     api.notify(gson.toJson(notify));
577                 } catch (IOException e) {
578                     logger.debug("Exception occurred while handling event: {}", e.getMessage(), e);
579                 }
580             }
581         });
582     }
583
584     /**
585      * Simply closes the {@link #request}
586      *
587      * @see DefaultServletService#close()
588      */
589     @Override
590     public void close() {
591         this.api.removePropertyChangeListener(listener);
592         request.close();
593     }
594 }