]> git.basschouten.com Git - openhab-addons.git/blob
55cf970b59b2cc9a6d125ccbb1eda1a3b729bdf8
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2023 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.binding.miele.internal.handler;
14
15 import static org.openhab.binding.miele.internal.MieleBindingConstants.*;
16
17 import java.io.IOException;
18 import java.net.DatagramPacket;
19 import java.net.InetAddress;
20 import java.net.InetSocketAddress;
21 import java.net.InterfaceAddress;
22 import java.net.MulticastSocket;
23 import java.net.NetworkInterface;
24 import java.net.SocketException;
25 import java.net.SocketTimeoutException;
26 import java.net.URISyntaxException;
27 import java.net.UnknownHostException;
28 import java.nio.charset.StandardCharsets;
29 import java.util.ArrayList;
30 import java.util.Enumeration;
31 import java.util.IllformedLocaleException;
32 import java.util.Iterator;
33 import java.util.List;
34 import java.util.Locale;
35 import java.util.Map;
36 import java.util.Map.Entry;
37 import java.util.Set;
38 import java.util.concurrent.ConcurrentHashMap;
39 import java.util.concurrent.ExecutorService;
40 import java.util.concurrent.Executors;
41 import java.util.concurrent.Future;
42 import java.util.concurrent.ScheduledFuture;
43 import java.util.concurrent.TimeUnit;
44 import java.util.regex.Pattern;
45
46 import org.eclipse.jdt.annotation.NonNullByDefault;
47 import org.eclipse.jdt.annotation.Nullable;
48 import org.eclipse.jetty.client.HttpClient;
49 import org.openhab.binding.miele.internal.FullyQualifiedApplianceIdentifier;
50 import org.openhab.binding.miele.internal.MieleGatewayCommunicationController;
51 import org.openhab.binding.miele.internal.api.dto.DeviceClassObject;
52 import org.openhab.binding.miele.internal.api.dto.DeviceProperty;
53 import org.openhab.binding.miele.internal.api.dto.HomeDevice;
54 import org.openhab.binding.miele.internal.exceptions.MieleRpcException;
55 import org.openhab.core.common.NamedThreadFactory;
56 import org.openhab.core.config.core.Configuration;
57 import org.openhab.core.thing.Bridge;
58 import org.openhab.core.thing.ChannelUID;
59 import org.openhab.core.thing.ThingStatus;
60 import org.openhab.core.thing.ThingStatusDetail;
61 import org.openhab.core.thing.ThingTypeUID;
62 import org.openhab.core.thing.binding.BaseBridgeHandler;
63 import org.openhab.core.types.Command;
64 import org.openhab.core.types.RefreshType;
65 import org.slf4j.Logger;
66 import org.slf4j.LoggerFactory;
67
68 import com.google.gson.Gson;
69 import com.google.gson.JsonElement;
70
71 /**
72  * The {@link MieleBridgeHandler} is responsible for handling commands, which are
73  * sent to one of the channels.
74  *
75  * @author Karel Goderis - Initial contribution
76  * @author Kai Kreuzer - Fixed lifecycle issues
77  * @author Martin Lepsy - Added protocol information to support WiFi devices and some refactoring for HomeDevice
78  * @author Jacob Laursen - Fixed multicast and protocol support (Zigbee/LAN)
79  **/
80 @NonNullByDefault
81 public class MieleBridgeHandler extends BaseBridgeHandler {
82
83     public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES = Set.of(THING_TYPE_XGW3000);
84
85     private static final Pattern IP_PATTERN = Pattern
86             .compile("^(([01]?\\d\\d?|2[0-4]\\d|25[0-5])\\.){3}([01]?\\d\\d?|2[0-4]\\d|25[0-5])$");
87
88     private static final int POLLING_PERIOD_SECONDS = 15;
89     private static final int JSON_RPC_PORT = 2810;
90     private static final String JSON_RPC_MULTICAST_IP1 = "239.255.68.139";
91     private static final String JSON_RPC_MULTICAST_IP2 = "224.255.68.139";
92     private static final int MULTICAST_TIMEOUT_MILLIS = 100;
93     private static final int MULTICAST_SLEEP_MILLIS = 500;
94
95     private final Logger logger = LoggerFactory.getLogger(MieleBridgeHandler.class);
96
97     private boolean lastBridgeConnectionState = false;
98
99     private final HttpClient httpClient;
100     private final Gson gson = new Gson();
101     private @NonNullByDefault({}) MieleGatewayCommunicationController gatewayCommunication;
102
103     private Set<DiscoveryListener> discoveryListeners = ConcurrentHashMap.newKeySet();
104     private Map<String, ApplianceStatusListener> applianceStatusListeners = new ConcurrentHashMap<>();
105     private @Nullable ScheduledFuture<?> pollingJob;
106     private @Nullable ExecutorService executor;
107     private @Nullable Future<?> eventListenerJob;
108
109     private Map<String, HomeDevice> cachedHomeDevicesByApplianceId = new ConcurrentHashMap<>();
110     private Map<String, HomeDevice> cachedHomeDevicesByRemoteUid = new ConcurrentHashMap<>();
111
112     public MieleBridgeHandler(Bridge bridge, HttpClient httpClient) {
113         super(bridge);
114         this.httpClient = httpClient;
115     }
116
117     @Override
118     public void initialize() {
119         logger.debug("Initializing handler for bridge {}", getThing().getUID());
120
121         if (!validateConfig(getConfig())) {
122             return;
123         }
124
125         try {
126             gatewayCommunication = new MieleGatewayCommunicationController(httpClient, (String) getConfig().get(HOST));
127         } catch (URISyntaxException e) {
128             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, e.getMessage());
129             return;
130         }
131
132         updateStatus(ThingStatus.UNKNOWN);
133         lastBridgeConnectionState = false;
134         schedulePollingAndEventListener();
135     }
136
137     private boolean validateConfig(Configuration config) {
138         if (config.get(HOST) == null || ((String) config.get(HOST)).isBlank()) {
139             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR,
140                     "@text/offline.configuration-error.ip-address-not-set");
141             return false;
142         }
143         if (config.get(INTERFACE) == null || ((String) config.get(INTERFACE)).isBlank()) {
144             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR,
145                     "@text/offline.configuration-error.ip-multicast-interface-not-set");
146             return false;
147         }
148         if (!IP_PATTERN.matcher((String) config.get(INTERFACE)).matches()) {
149             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR,
150                     "@text/offline.configuration-error.invalid-ip-multicast-interface [\"" + config.get(INTERFACE)
151                             + "\"]");
152             return false;
153         }
154         String language = (String) config.get(LANGUAGE);
155         if (language != null && !language.isBlank()) {
156             try {
157                 new Locale.Builder().setLanguageTag(language).build();
158             } catch (IllformedLocaleException e) {
159                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR,
160                         "@text/offline.configuration-error.invalid-language [\"" + language + "\"]");
161                 return false;
162             }
163         }
164         return true;
165     }
166
167     private Runnable pollingRunnable = new Runnable() {
168         @Override
169         public void run() {
170             String host = (String) getConfig().get(HOST);
171             try {
172                 List<HomeDevice> homeDevices = getHomeDevices();
173
174                 if (!lastBridgeConnectionState) {
175                     logger.debug("Connection to Miele Gateway {} established.", host);
176                     lastBridgeConnectionState = true;
177                 }
178                 updateStatus(ThingStatus.ONLINE);
179
180                 refreshHomeDevices(homeDevices);
181
182                 for (Entry<String, ApplianceStatusListener> entry : applianceStatusListeners.entrySet()) {
183                     String applianceId = entry.getKey();
184                     ApplianceStatusListener listener = entry.getValue();
185                     FullyQualifiedApplianceIdentifier applianceIdentifier = getApplianceIdentifierFromApplianceId(
186                             applianceId);
187                     if (applianceIdentifier == null) {
188                         logger.debug("The appliance with ID '{}' was not found in appliance list from bridge.",
189                                 applianceId);
190                         listener.onApplianceRemoved();
191                         continue;
192                     }
193
194                     Object[] args = new Object[2];
195                     args[0] = applianceIdentifier.getUid();
196                     args[1] = true;
197                     JsonElement result = gatewayCommunication.invokeRPC("HDAccess/getDeviceClassObjects", args);
198
199                     for (JsonElement obj : result.getAsJsonArray()) {
200                         try {
201                             DeviceClassObject dco = gson.fromJson(obj, DeviceClassObject.class);
202
203                             // Skip com.prosyst.mbs.services.zigbee.hdm.deviceclasses.ReportingControl
204                             if (dco == null || !dco.DeviceClass.startsWith(MIELE_CLASS)) {
205                                 continue;
206                             }
207
208                             listener.onApplianceStateChanged(dco);
209                         } catch (Exception e) {
210                             logger.debug("An exception occurred while querying an appliance : '{}'", e.getMessage());
211                         }
212                     }
213                 }
214             } catch (MieleRpcException e) {
215                 Throwable cause = e.getCause();
216                 String message;
217                 if (cause == null) {
218                     message = e.getMessage();
219                     logger.debug("An exception occurred while polling an appliance: '{}'", message);
220                 } else {
221                     message = cause.getMessage();
222                     logger.debug("An exception occurred while polling an appliance: '{}' -> '{}'", e.getMessage(),
223                             message);
224                 }
225                 if (lastBridgeConnectionState) {
226                     logger.debug("Connection to Miele Gateway {} lost.", host);
227                     lastBridgeConnectionState = false;
228                 }
229                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, message);
230             }
231         }
232     };
233
234     private synchronized void refreshHomeDevices(List<HomeDevice> homeDevices) {
235         for (HomeDevice hd : homeDevices) {
236             String key = hd.getApplianceIdentifier().getApplianceId();
237             if (!cachedHomeDevicesByApplianceId.containsKey(key)) {
238                 logger.debug("A new appliance with ID '{}' has been added", hd.UID);
239                 for (DiscoveryListener listener : discoveryListeners) {
240                     listener.onApplianceAdded(hd);
241                 }
242                 ApplianceStatusListener listener = applianceStatusListeners
243                         .get(hd.getApplianceIdentifier().getApplianceId());
244                 if (listener != null) {
245                     listener.onApplianceAdded(hd);
246                 }
247             }
248             cachedHomeDevicesByApplianceId.put(key, hd);
249             cachedHomeDevicesByRemoteUid.put(hd.getRemoteUid(), hd);
250         }
251
252         Set<Entry<String, HomeDevice>> cachedEntries = cachedHomeDevicesByApplianceId.entrySet();
253         Iterator<Entry<String, HomeDevice>> iterator = cachedEntries.iterator();
254
255         while (iterator.hasNext()) {
256             Entry<String, HomeDevice> cachedEntry = iterator.next();
257             HomeDevice cachedHomeDevice = cachedEntry.getValue();
258             if (!homeDevices.stream().anyMatch(d -> d.UID.equals(cachedHomeDevice.UID))) {
259                 logger.debug("The appliance with ID '{}' has been removed", cachedHomeDevice.UID);
260                 for (DiscoveryListener listener : discoveryListeners) {
261                     listener.onApplianceRemoved(cachedHomeDevice);
262                 }
263                 ApplianceStatusListener listener = applianceStatusListeners
264                         .get(cachedHomeDevice.getApplianceIdentifier().getApplianceId());
265                 if (listener != null) {
266                     listener.onApplianceRemoved();
267                 }
268                 cachedHomeDevicesByRemoteUid.remove(cachedHomeDevice.getRemoteUid());
269                 iterator.remove();
270             }
271         }
272     }
273
274     public List<HomeDevice> getHomeDevicesEmptyOnFailure() {
275         try {
276             return getHomeDevices();
277         } catch (MieleRpcException e) {
278             Throwable cause = e.getCause();
279             if (cause == null) {
280                 logger.debug("An exception occurred while getting the home devices: '{}'", e.getMessage());
281             } else {
282                 logger.debug("An exception occurred while getting the home devices: '{}' -> '{}", e.getMessage(),
283                         cause.getMessage());
284             }
285             return new ArrayList<>();
286         }
287     }
288
289     private List<HomeDevice> getHomeDevices() throws MieleRpcException {
290         List<HomeDevice> devices = new ArrayList<>();
291
292         if (!isInitialized()) {
293             return devices;
294         }
295
296         String[] args = new String[1];
297         args[0] = "(type=SuperVision)";
298         JsonElement result = gatewayCommunication.invokeRPC("HDAccess/getHomeDevices", args);
299
300         for (JsonElement obj : result.getAsJsonArray()) {
301             HomeDevice hd = gson.fromJson(obj, HomeDevice.class);
302             if (hd != null) {
303                 devices.add(hd);
304             }
305         }
306         return devices;
307     }
308
309     private @Nullable FullyQualifiedApplianceIdentifier getApplianceIdentifierFromApplianceId(String applianceId) {
310         HomeDevice homeDevice = this.cachedHomeDevicesByApplianceId.get(applianceId);
311         if (homeDevice == null) {
312             return null;
313         }
314
315         return homeDevice.getApplianceIdentifier();
316     }
317
318     private Runnable eventListenerRunnable = () -> {
319         String interfaceIpAddress = (String) getConfig().get(INTERFACE);
320         if (!IP_PATTERN.matcher(interfaceIpAddress).matches()) {
321             logger.debug("Invalid IP address for the multicast interface: '{}'", interfaceIpAddress);
322             return;
323         }
324
325         // Get the address that we are going to connect to.
326         InetSocketAddress address1 = null;
327         InetSocketAddress address2 = null;
328         try {
329             address1 = new InetSocketAddress(InetAddress.getByName(JSON_RPC_MULTICAST_IP1), JSON_RPC_PORT);
330             address2 = new InetSocketAddress(InetAddress.getByName(JSON_RPC_MULTICAST_IP2), JSON_RPC_PORT);
331         } catch (UnknownHostException e) {
332             // This can only happen if the hardcoded literal IP addresses are invalid.
333             logger.debug("An exception occurred while setting up the multicast receiver: '{}'", e.getMessage());
334             return;
335         }
336
337         while (!Thread.currentThread().isInterrupted()) {
338             MulticastSocket clientSocket = null;
339             try {
340                 clientSocket = new MulticastSocket(JSON_RPC_PORT);
341                 clientSocket.setSoTimeout(MULTICAST_TIMEOUT_MILLIS);
342
343                 NetworkInterface networkInterface = getMulticastInterface(interfaceIpAddress);
344                 if (networkInterface == null) {
345                     logger.warn("Unable to find network interface for address {}", interfaceIpAddress);
346                     return;
347                 }
348                 clientSocket.setNetworkInterface(networkInterface);
349                 clientSocket.joinGroup(address1, null);
350                 clientSocket.joinGroup(address2, null);
351
352                 while (!Thread.currentThread().isInterrupted()) {
353                     try {
354                         byte[] buf = new byte[256];
355                         DatagramPacket packet = new DatagramPacket(buf, buf.length);
356                         clientSocket.receive(packet);
357
358                         String event = new String(packet.getData(), packet.getOffset(), packet.getLength(),
359                                 StandardCharsets.ISO_8859_1);
360                         logger.debug("Received a multicast event '{}' from '{}:{}'", event, packet.getAddress(),
361                                 packet.getPort());
362
363                         String[] parts = event.split("&");
364                         String id = null, name = null, value = null;
365                         for (String p : parts) {
366                             String[] subparts = p.split("=");
367                             switch (subparts[0]) {
368                                 case "property": {
369                                     name = subparts[1];
370                                     break;
371                                 }
372                                 case "value": {
373                                     value = subparts[1];
374                                     break;
375                                 }
376                                 case "id": {
377                                     id = subparts[1];
378                                     break;
379                                 }
380                             }
381                         }
382
383                         if (id == null || name == null || value == null) {
384                             continue;
385                         }
386
387                         // In XGW 3000 firmware 2.03 this was changed from UID (hdm:ZigBee:0123456789abcdef#210)
388                         // to serial number (001234567890)
389                         FullyQualifiedApplianceIdentifier applianceIdentifier;
390                         if (id.startsWith("hdm:")) {
391                             applianceIdentifier = new FullyQualifiedApplianceIdentifier(id);
392                         } else {
393                             HomeDevice device = cachedHomeDevicesByRemoteUid.get(id);
394                             if (device == null) {
395                                 logger.debug("Multicast event not handled as id {} is unknown.", id);
396                                 continue;
397                             }
398                             applianceIdentifier = device.getApplianceIdentifier();
399                         }
400                         var deviceProperty = new DeviceProperty();
401                         deviceProperty.Name = name;
402                         deviceProperty.Value = value;
403                         ApplianceStatusListener listener = applianceStatusListeners
404                                 .get(applianceIdentifier.getApplianceId());
405                         if (listener != null) {
406                             listener.onAppliancePropertyChanged(deviceProperty);
407                         }
408                     } catch (SocketTimeoutException e) {
409                         try {
410                             Thread.sleep(MULTICAST_SLEEP_MILLIS);
411                         } catch (InterruptedException ex) {
412                             Thread.currentThread().interrupt();
413                             logger.debug("Event listener has been interrupted.");
414                             break;
415                         }
416                     }
417                 }
418             } catch (IOException e) {
419                 logger.debug("An exception occurred while receiving multicast packets: '{}'", e.getMessage());
420             } finally {
421                 // restart the cycle with a clean slate
422                 try {
423                     if (clientSocket != null) {
424                         clientSocket.leaveGroup(address1, null);
425                         clientSocket.leaveGroup(address2, null);
426                     }
427                 } catch (IOException e) {
428                     logger.debug("An exception occurred while leaving multicast group: '{}'", e.getMessage());
429                 }
430                 if (clientSocket != null) {
431                     clientSocket.close();
432                 }
433             }
434         }
435     };
436
437     private @Nullable NetworkInterface getMulticastInterface(String interfaceIpAddress) throws SocketException {
438         Enumeration<NetworkInterface> networkInterfaces = NetworkInterface.getNetworkInterfaces();
439
440         @Nullable
441         NetworkInterface networkInterface;
442         while (networkInterfaces.hasMoreElements()) {
443             networkInterface = networkInterfaces.nextElement();
444             if (networkInterface.isLoopback()) {
445                 continue;
446             }
447             for (InterfaceAddress interfaceAddress : networkInterface.getInterfaceAddresses()) {
448                 if (logger.isTraceEnabled()) {
449                     logger.trace("Found interface address {} -> {}", interfaceAddress.toString(),
450                             interfaceAddress.getAddress().toString());
451                 }
452                 if (interfaceAddress.getAddress().toString().endsWith("/" + interfaceIpAddress)) {
453                     return networkInterface;
454                 }
455             }
456         }
457
458         return null;
459     }
460
461     public JsonElement invokeOperation(String applianceId, String modelID, String methodName) throws MieleRpcException {
462         if (getThing().getStatus() != ThingStatus.ONLINE) {
463             throw new MieleRpcException("Bridge is offline, operations can not be invoked");
464         }
465
466         FullyQualifiedApplianceIdentifier applianceIdentifier = getApplianceIdentifierFromApplianceId(applianceId);
467         if (applianceIdentifier == null) {
468             throw new MieleRpcException("Appliance with ID" + applianceId
469                     + " was not found in appliance list from gateway - operations can not be invoked");
470         }
471
472         return gatewayCommunication.invokeOperation(applianceIdentifier, modelID, methodName);
473     }
474
475     private synchronized void schedulePollingAndEventListener() {
476         logger.debug("Scheduling the Miele polling job");
477         ScheduledFuture<?> pollingJob = this.pollingJob;
478         if (pollingJob == null || pollingJob.isCancelled()) {
479             logger.trace("Scheduling the Miele polling job period is {}", POLLING_PERIOD_SECONDS);
480             pollingJob = scheduler.scheduleWithFixedDelay(pollingRunnable, 0, POLLING_PERIOD_SECONDS, TimeUnit.SECONDS);
481             this.pollingJob = pollingJob;
482             logger.trace("Scheduling the Miele polling job Job is done ?{}", pollingJob.isDone());
483         }
484
485         logger.debug("Scheduling the Miele event listener job");
486         Future<?> eventListenerJob = this.eventListenerJob;
487         if (eventListenerJob == null || eventListenerJob.isCancelled()) {
488             ExecutorService executor = Executors
489                     .newSingleThreadExecutor(new NamedThreadFactory("binding-" + BINDING_ID));
490             this.executor = executor;
491             this.eventListenerJob = executor.submit(eventListenerRunnable);
492         }
493     }
494
495     public boolean registerApplianceStatusListener(String applianceId,
496             ApplianceStatusListener applianceStatusListener) {
497         ApplianceStatusListener existingListener = applianceStatusListeners.get(applianceId);
498         if (existingListener != null) {
499             if (!existingListener.equals(applianceStatusListener)) {
500                 logger.warn("Unsupported configuration: appliance with ID '{}' referenced by multiple things",
501                         applianceId);
502             } else {
503                 logger.debug("Duplicate listener registration attempted for '{}'", applianceId);
504             }
505             return false;
506         }
507         applianceStatusListeners.put(applianceId, applianceStatusListener);
508
509         HomeDevice cachedHomeDevice = cachedHomeDevicesByApplianceId.get(applianceId);
510         if (cachedHomeDevice != null) {
511             applianceStatusListener.onApplianceAdded(cachedHomeDevice);
512         } else {
513             try {
514                 refreshHomeDevices(getHomeDevices());
515             } catch (MieleRpcException e) {
516                 Throwable cause = e.getCause();
517                 if (cause == null) {
518                     logger.debug("An exception occurred while getting the home devices: '{}'", e.getMessage());
519                 } else {
520                     logger.debug("An exception occurred while getting the home devices: '{}' -> '{}", e.getMessage(),
521                             cause.getMessage());
522                 }
523             }
524         }
525
526         return true;
527     }
528
529     public boolean unregisterApplianceStatusListener(String applianceId,
530             ApplianceStatusListener applianceStatusListener) {
531         return applianceStatusListeners.remove(applianceId) != null;
532     }
533
534     public boolean registerDiscoveryListener(DiscoveryListener discoveryListener) {
535         if (!discoveryListeners.add(discoveryListener)) {
536             return false;
537         }
538         if (cachedHomeDevicesByApplianceId.isEmpty()) {
539             try {
540                 refreshHomeDevices(getHomeDevices());
541             } catch (MieleRpcException e) {
542                 Throwable cause = e.getCause();
543                 if (cause == null) {
544                     logger.debug("An exception occurred while getting the home devices: '{}'", e.getMessage());
545                 } else {
546                     logger.debug("An exception occurred while getting the home devices: '{}' -> '{}'", e.getMessage(),
547                             cause.getMessage());
548                 }
549             }
550         } else {
551             for (Entry<String, HomeDevice> entry : cachedHomeDevicesByApplianceId.entrySet()) {
552                 discoveryListener.onApplianceAdded(entry.getValue());
553             }
554         }
555         return true;
556     }
557
558     public boolean unregisterDiscoveryListener(DiscoveryListener discoveryListener) {
559         return discoveryListeners.remove(discoveryListener);
560     }
561
562     @Override
563     public void handleCommand(ChannelUID channelUID, Command command) {
564         // Nothing to do here - the XGW bridge does not handle commands, for now
565         if (command instanceof RefreshType) {
566             // Placeholder for future refinement
567             return;
568         }
569     }
570
571     @Override
572     public void dispose() {
573         super.dispose();
574         ScheduledFuture<?> pollingJob = this.pollingJob;
575         if (pollingJob != null) {
576             pollingJob.cancel(true);
577             this.pollingJob = null;
578         }
579         Future<?> eventListenerJob = this.eventListenerJob;
580         if (eventListenerJob != null) {
581             eventListenerJob.cancel(true);
582             this.eventListenerJob = null;
583         }
584         ExecutorService executor = this.executor;
585         if (executor != null) {
586             executor.shutdownNow();
587             this.executor = null;
588         }
589     }
590 }