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