]> git.basschouten.com Git - openhab-addons.git/blob
fc71e50f13cd6d895593f9be075e40bbe500849d
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2021 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.zoneminder.internal.handler;
14
15 import static org.openhab.binding.zoneminder.internal.ZmBindingConstants.*;
16
17 import java.io.ByteArrayInputStream;
18 import java.io.IOException;
19 import java.nio.charset.StandardCharsets;
20 import java.util.ArrayList;
21 import java.util.Collection;
22 import java.util.Collections;
23 import java.util.List;
24 import java.util.Map;
25 import java.util.Set;
26 import java.util.concurrent.ConcurrentHashMap;
27 import java.util.concurrent.ExecutionException;
28 import java.util.concurrent.Future;
29 import java.util.concurrent.TimeUnit;
30 import java.util.concurrent.TimeoutException;
31
32 import org.eclipse.jdt.annotation.NonNullByDefault;
33 import org.eclipse.jdt.annotation.Nullable;
34 import org.eclipse.jetty.client.HttpClient;
35 import org.eclipse.jetty.client.api.ContentResponse;
36 import org.eclipse.jetty.client.api.Request;
37 import org.eclipse.jetty.http.HttpHeader;
38 import org.eclipse.jetty.http.HttpMethod;
39 import org.eclipse.jetty.http.HttpStatus;
40 import org.openhab.binding.zoneminder.internal.ZmStateDescriptionOptionsProvider;
41 import org.openhab.binding.zoneminder.internal.config.ZmBridgeConfig;
42 import org.openhab.binding.zoneminder.internal.discovery.MonitorDiscoveryService;
43 import org.openhab.binding.zoneminder.internal.dto.EventDTO;
44 import org.openhab.binding.zoneminder.internal.dto.EventsDTO;
45 import org.openhab.binding.zoneminder.internal.dto.MonitorDTO;
46 import org.openhab.binding.zoneminder.internal.dto.MonitorItemDTO;
47 import org.openhab.binding.zoneminder.internal.dto.MonitorStateDTO;
48 import org.openhab.binding.zoneminder.internal.dto.MonitorStatusDTO;
49 import org.openhab.binding.zoneminder.internal.dto.MonitorsDTO;
50 import org.openhab.binding.zoneminder.internal.dto.VersionDTO;
51 import org.openhab.core.io.net.http.HttpUtil;
52 import org.openhab.core.library.types.OnOffType;
53 import org.openhab.core.library.types.RawType;
54 import org.openhab.core.library.types.StringType;
55 import org.openhab.core.thing.Bridge;
56 import org.openhab.core.thing.ChannelUID;
57 import org.openhab.core.thing.Thing;
58 import org.openhab.core.thing.ThingStatus;
59 import org.openhab.core.thing.ThingStatusDetail;
60 import org.openhab.core.thing.binding.BaseBridgeHandler;
61 import org.openhab.core.thing.binding.ThingHandler;
62 import org.openhab.core.thing.binding.ThingHandlerService;
63 import org.openhab.core.types.Command;
64 import org.openhab.core.types.RefreshType;
65 import org.openhab.core.types.StateOption;
66 import org.openhab.core.types.UnDefType;
67 import org.slf4j.Logger;
68 import org.slf4j.LoggerFactory;
69
70 import com.google.gson.Gson;
71 import com.google.gson.GsonBuilder;
72 import com.google.gson.JsonSyntaxException;
73
74 /**
75  * The {@link ZmBridgeHandler} represents the Zoneminder server. It handles all communication
76  * with the Zoneminder server.
77  *
78  * @author Mark Hilbush - Initial contribution
79  */
80 @NonNullByDefault
81 public class ZmBridgeHandler extends BaseBridgeHandler {
82
83     private static final int MONITOR_REFRESH_INTERVAL_SECONDS = 10;
84     private static final int MONITOR_REFRESH_STARTUP_DELAY_SECONDS = 5;
85
86     private static final int API_TIMEOUT_MSEC = 10000;
87
88     private static final String LOGIN_PATH = "/api/host/login.json";
89
90     private static final String STREAM_IMAGE = "single";
91     private static final String STREAM_VIDEO = "jpeg";
92
93     private static final List<String> EMPTY_LIST = Collections.emptyList();
94
95     private static final Gson GSON = new GsonBuilder().setDateFormat("yyyy-MM-dd HH:mm:ss").create();
96
97     private final Logger logger = LoggerFactory.getLogger(ZmBridgeHandler.class);
98
99     private @Nullable Future<?> refreshMonitorsJob;
100
101     private List<Monitor> savedMonitors = new ArrayList<>();
102
103     private String host = "";
104     private boolean useSSL;
105     private @Nullable String portNumber;
106     private String urlPath = DEFAULT_URL_PATH;
107     private int monitorRefreshInterval;
108     private boolean backgroundDiscoveryEnabled;
109     private int defaultAlarmDuration;
110     private @Nullable Integer defaultImageRefreshInterval;
111
112     private final HttpClient httpClient;
113     private final ZmStateDescriptionOptionsProvider stateDescriptionProvider;
114
115     private ZmAuth zmAuth;
116
117     // Maintain mapping of handler and monitor id
118     private final Map<String, ZmMonitorHandler> monitorHandlers = new ConcurrentHashMap<>();
119
120     public ZmBridgeHandler(Bridge thing, HttpClient httpClient,
121             ZmStateDescriptionOptionsProvider stateDescriptionProvider) {
122         super(thing);
123         this.httpClient = httpClient;
124         this.stateDescriptionProvider = stateDescriptionProvider;
125         // Default to use no authentication
126         zmAuth = new ZmAuth(this);
127     }
128
129     @Override
130     public void initialize() {
131         ZmBridgeConfig config = getConfigAs(ZmBridgeConfig.class);
132
133         Integer value;
134         value = config.refreshInterval;
135         monitorRefreshInterval = value == null ? MONITOR_REFRESH_INTERVAL_SECONDS : value;
136
137         value = config.defaultAlarmDuration;
138         defaultAlarmDuration = value == null ? DEFAULT_ALARM_DURATION_SECONDS : value;
139
140         defaultImageRefreshInterval = config.defaultImageRefreshInterval;
141
142         backgroundDiscoveryEnabled = config.discoveryEnabled;
143         logger.debug("Bridge: Background discovery is {}", backgroundDiscoveryEnabled == true ? "ENABLED" : "DISABLED");
144
145         host = config.host;
146         useSSL = config.useSSL.booleanValue();
147         portNumber = config.portNumber != null ? Integer.toString(config.portNumber) : null;
148         urlPath = config.urlPath;
149
150         // If user and password are configured, then use Zoneminder authentication
151         if (config.user != null && config.pass != null) {
152             zmAuth = new ZmAuth(this, config.user, config.pass);
153         }
154         if (isHostValid()) {
155             updateStatus(ThingStatus.ONLINE);
156             scheduleRefreshJob();
157         }
158     }
159
160     @Override
161     public void dispose() {
162         cancelRefreshJob();
163     }
164
165     @Override
166     public void childHandlerInitialized(ThingHandler childHandler, Thing childThing) {
167         String monitorId = (String) childThing.getConfiguration().get(CONFIG_MONITOR_ID);
168         monitorHandlers.put(monitorId, (ZmMonitorHandler) childHandler);
169         logger.debug("Bridge: Monitor handler was initialized for {} with id {}", childThing.getUID(), monitorId);
170     }
171
172     @Override
173     public void childHandlerDisposed(ThingHandler childHandler, Thing childThing) {
174         String monitorId = (String) childThing.getConfiguration().get(CONFIG_MONITOR_ID);
175         monitorHandlers.remove(monitorId);
176         logger.debug("Bridge: Monitor handler was disposed for {} with id {}", childThing.getUID(), monitorId);
177     }
178
179     @Override
180     public void handleCommand(ChannelUID channelUID, Command command) {
181         switch (channelUID.getId()) {
182             case CHANNEL_IMAGE_MONITOR_ID:
183                 handleMonitorIdCommand(command, CHANNEL_IMAGE_MONITOR_ID, CHANNEL_IMAGE_URL, STREAM_IMAGE);
184                 break;
185             case CHANNEL_VIDEO_MONITOR_ID:
186                 handleMonitorIdCommand(command, CHANNEL_VIDEO_MONITOR_ID, CHANNEL_VIDEO_URL, STREAM_VIDEO);
187                 break;
188         }
189     }
190
191     private void handleMonitorIdCommand(Command command, String monitorIdChannelId, String urlChannelId, String type) {
192         if (command instanceof RefreshType || command == OnOffType.OFF) {
193             updateState(monitorIdChannelId, UnDefType.UNDEF);
194             updateState(urlChannelId, UnDefType.UNDEF);
195         } else if (command instanceof StringType) {
196             String id = command.toString();
197             if (isMonitorIdValid(id)) {
198                 updateState(urlChannelId, new StringType(buildStreamUrl(id, type)));
199             } else {
200                 updateState(monitorIdChannelId, UnDefType.UNDEF);
201                 updateState(urlChannelId, UnDefType.UNDEF);
202             }
203         }
204     }
205
206     @Override
207     public Collection<Class<? extends ThingHandlerService>> getServices() {
208         return Collections.singleton(MonitorDiscoveryService.class);
209     }
210
211     public boolean isBackgroundDiscoveryEnabled() {
212         return backgroundDiscoveryEnabled;
213     }
214
215     public Integer getDefaultAlarmDuration() {
216         return defaultAlarmDuration;
217     }
218
219     public @Nullable Integer getDefaultImageRefreshInterval() {
220         return defaultImageRefreshInterval;
221     }
222
223     public List<Monitor> getSavedMonitors() {
224         return savedMonitors;
225     }
226
227     public Gson getGson() {
228         return GSON;
229     }
230
231     public void setFunction(String id, MonitorFunction function) {
232         if (!zmAuth.isAuthorized()) {
233             return;
234         }
235         logger.debug("Bridge: Setting monitor {} function to {}", id, function);
236         executePost(buildUrl(String.format("/api/monitors/%s.json", id)),
237                 String.format("Monitor[Function]=%s", function.toString()));
238     }
239
240     public void setEnabled(String id, OnOffType enabled) {
241         if (!zmAuth.isAuthorized()) {
242             return;
243         }
244         logger.debug("Bridge: Setting monitor {} to {}", id, enabled);
245         executePost(buildUrl(String.format("/api/monitors/%s.json", id)),
246                 String.format("Monitor[Enabled]=%s", enabled == OnOffType.ON ? "1" : "0"));
247     }
248
249     public void setAlarmOn(String id) {
250         if (!zmAuth.isAuthorized()) {
251             return;
252         }
253         logger.debug("Bridge: Turning alarm ON for monitor {}", id);
254         setAlarm(buildUrl(String.format("/api/monitors/alarm/id:%s/command:on.json", id)));
255     }
256
257     public void setAlarmOff(String id) {
258         if (!zmAuth.isAuthorized()) {
259             return;
260         }
261         logger.debug("Bridge: Turning alarm OFF for monitor {}", id);
262         setAlarm(buildUrl(String.format("/api/monitors/alarm/id:%s/command:off.json", id)));
263     }
264
265     public @Nullable RawType getImage(String id, @Nullable Integer imageRefreshIntervalSeconds) {
266         Integer localRefreshInterval = imageRefreshIntervalSeconds;
267         if (localRefreshInterval == null || localRefreshInterval.intValue() < 1 || !zmAuth.isAuthorized()) {
268             return null;
269         }
270         // Call should timeout just before the refresh interval
271         int timeout = Math.min((localRefreshInterval * 1000) - 500, API_TIMEOUT_MSEC);
272         Request request = httpClient.newRequest(buildStreamUrl(id, STREAM_IMAGE));
273         request.method(HttpMethod.GET);
274         request.timeout(timeout, TimeUnit.MILLISECONDS);
275
276         String errorMsg;
277         try {
278             ContentResponse response = request.send();
279             if (response.getStatus() == HttpStatus.OK_200) {
280                 RawType image = new RawType(response.getContent(), response.getHeaders().get(HttpHeader.CONTENT_TYPE));
281                 return image;
282             } else {
283                 errorMsg = String.format("HTTP GET failed: %d, %s", response.getStatus(), response.getReason());
284             }
285         } catch (TimeoutException e) {
286             errorMsg = String.format("TimeoutException: Call to Zoneminder API timed out after {} msec", timeout);
287         } catch (ExecutionException e) {
288             errorMsg = String.format("ExecutionException: %s", e.getMessage());
289         } catch (InterruptedException e) {
290             errorMsg = String.format("InterruptedException: %s", e.getMessage());
291             Thread.currentThread().interrupt();
292         }
293         logger.debug("{}", errorMsg);
294         return null;
295     }
296
297     @SuppressWarnings("null")
298     private synchronized List<Monitor> getMonitors() {
299         List<Monitor> monitorList = new ArrayList<>();
300         if (!zmAuth.isAuthorized()) {
301             return monitorList;
302         }
303         try {
304             String response = executeGet(buildUrl("/api/monitors.json"));
305             MonitorsDTO monitors = GSON.fromJson(response, MonitorsDTO.class);
306             if (monitors != null && monitors.monitorItems != null) {
307                 List<StateOption> options = new ArrayList<>();
308                 for (MonitorItemDTO monitorItem : monitors.monitorItems) {
309                     MonitorDTO m = monitorItem.monitor;
310                     MonitorStatusDTO mStatus = monitorItem.monitorStatus;
311                     if (m != null && mStatus != null) {
312                         Monitor monitor = new Monitor(m.id, m.name, m.function, m.enabled, mStatus.status);
313                         monitor.setHourEvents(m.hourEvents);
314                         monitor.setDayEvents(m.dayEvents);
315                         monitor.setWeekEvents(m.weekEvents);
316                         monitor.setMonthEvents(m.monthEvents);
317                         monitor.setTotalEvents(m.totalEvents);
318                         monitor.setImageUrl(buildStreamUrl(m.id, STREAM_IMAGE));
319                         monitor.setVideoUrl(buildStreamUrl(m.id, STREAM_VIDEO));
320                         monitorList.add(monitor);
321                         options.add(new StateOption(m.id, "Monitor " + m.id));
322                     }
323                     stateDescriptionProvider
324                             .setStateOptions(new ChannelUID(getThing().getUID(), CHANNEL_IMAGE_MONITOR_ID), options);
325                     stateDescriptionProvider
326                             .setStateOptions(new ChannelUID(getThing().getUID(), CHANNEL_VIDEO_MONITOR_ID), options);
327                 }
328                 // Only update alarm and event info for monitors whose handlers are initialized
329                 Set<String> ids = monitorHandlers.keySet();
330                 for (Monitor m : monitorList) {
331                     if (ids.contains(m.getId())) {
332                         m.setState(getState(m.getId()));
333                         m.setLastEvent(getLastEvent(m.getId()));
334                     }
335                 }
336             }
337         } catch (JsonSyntaxException e) {
338             logger.debug("Bridge: JsonSyntaxException: {}", e.getMessage(), e);
339         }
340         return monitorList;
341     }
342
343     @SuppressWarnings("null")
344     private @Nullable Event getLastEvent(String id) {
345         if (!zmAuth.isAuthorized()) {
346             return null;
347         }
348         try {
349             List<String> parameters = new ArrayList<>();
350             parameters.add("sort=StartTime");
351             parameters.add("direction=desc");
352             parameters.add("limit=1");
353             String response = executeGet(buildUrlWithParameters(
354                     String.format("/api/events/index/MonitorId:%s/Name!=:New%%20Event.json", id), parameters));
355             EventsDTO events = GSON.fromJson(response, EventsDTO.class);
356             if (events != null && events.eventsList != null && events.eventsList.size() == 1) {
357                 EventDTO e = events.eventsList.get(0).event;
358                 Event event = new Event(e.eventId, e.name, e.cause, e.notes, e.startTime, e.endTime);
359                 event.setFrames(e.frames);
360                 event.setAlarmFrames(e.alarmFrames);
361                 event.setLength(e.length);
362                 return event;
363             }
364         } catch (JsonSyntaxException e) {
365             logger.debug("Bridge: JsonSyntaxException: {}", e.getMessage(), e);
366         }
367         return null;
368     }
369
370     private @Nullable VersionDTO getVersion() {
371         if (!zmAuth.isAuthorized()) {
372             return null;
373         }
374         VersionDTO version = null;
375         try {
376             String response = executeGet(buildUrl("/api/host/getVersion.json"));
377             version = GSON.fromJson(response, VersionDTO.class);
378         } catch (JsonSyntaxException e) {
379             logger.debug("Bridge: JsonSyntaxException: {}", e.getMessage(), e);
380         }
381         return version;
382     }
383
384     private void setAlarm(String url) {
385         executeGet(url);
386     }
387
388     @SuppressWarnings("null")
389     private MonitorState getState(String id) {
390         if (!zmAuth.isAuthorized()) {
391             return MonitorState.UNKNOWN;
392         }
393         try {
394             String response = executeGet(buildUrl(String.format("/api/monitors/alarm/id:%s/command:status.json", id)));
395             MonitorStateDTO monitorState = GSON.fromJson(response, MonitorStateDTO.class);
396             if (monitorState != null) {
397                 MonitorState state = monitorState.state;
398                 return state != null ? state : MonitorState.UNKNOWN;
399             }
400         } catch (JsonSyntaxException e) {
401             logger.debug("Bridge: JsonSyntaxException: {}", e.getMessage(), e);
402         }
403         return MonitorState.UNKNOWN;
404     }
405
406     public @Nullable String executeGet(String url) {
407         try {
408             long startTime = System.currentTimeMillis();
409             String response = HttpUtil.executeUrl("GET", url, API_TIMEOUT_MSEC);
410             logger.trace("Bridge: Http GET of '{}' returned '{}' in {} ms", url, response,
411                     System.currentTimeMillis() - startTime);
412             return response;
413         } catch (IOException e) {
414             logger.debug("Bridge: IOException on GET request, url='{}': {}", url, e.getMessage());
415         }
416         return null;
417     }
418
419     private @Nullable String executePost(String url, String content) {
420         return executePost(url, content, "application/x-www-form-urlencoded");
421     }
422
423     public @Nullable String executePost(String url, String content, String contentType) {
424         try (ByteArrayInputStream inputStream = new ByteArrayInputStream(content.getBytes(StandardCharsets.UTF_8))) {
425             long startTime = System.currentTimeMillis();
426             String response = HttpUtil.executeUrl("POST", url, inputStream, contentType, API_TIMEOUT_MSEC);
427             logger.trace("Bridge: Http POST content '{}' to '{}' returned: {} in {} ms", content, url, response,
428                     System.currentTimeMillis() - startTime);
429             return response;
430         } catch (IOException e) {
431             logger.debug("Bridge: IOException on POST request, url='{}': {}", url, e.getMessage());
432         }
433         return null;
434     }
435
436     public String buildLoginUrl() {
437         return buildBaseUrl(LOGIN_PATH).toString();
438     }
439
440     public String buildLoginUrl(String tokenParameter) {
441         StringBuilder sb = buildBaseUrl(LOGIN_PATH);
442         sb.append(tokenParameter);
443         return sb.toString();
444     }
445
446     private String buildStreamUrl(String id, String streamType) {
447         List<String> parameters = new ArrayList<>();
448         parameters.add(String.format("mode=%s", streamType));
449         parameters.add(String.format("monitor=%s", id));
450         return buildUrlWithParameters("/cgi-bin/zms", parameters);
451     }
452
453     private String buildUrl(String path) {
454         return buildUrlWithParameters(path, EMPTY_LIST);
455     }
456
457     private String buildUrlWithParameters(String path, List<String> parameters) {
458         StringBuilder sb = buildBaseUrl(path);
459         String joiner = "?";
460         for (String parameter : parameters) {
461             sb.append(joiner).append(parameter);
462             joiner = "&";
463         }
464         if (zmAuth.usingAuthorization()) {
465             sb.append(joiner).append("token=").append(zmAuth.getAccessToken());
466         }
467         return sb.toString();
468     }
469
470     private StringBuilder buildBaseUrl(String path) {
471         StringBuilder sb = new StringBuilder();
472         sb.append(useSSL ? "https://" : "http://");
473         sb.append(host);
474         if (portNumber != null) {
475             sb.append(":").append(portNumber);
476         }
477         sb.append(urlPath);
478         sb.append(path);
479         return sb;
480     }
481
482     private boolean isMonitorIdValid(String id) {
483         return savedMonitors.stream().filter(monitor -> id.equals(monitor.getId())).findAny().isPresent();
484     }
485
486     private boolean isHostValid() {
487         logger.debug("Bridge: Checking for valid Zoneminder host: {}", host);
488         VersionDTO version = getVersion();
489         if (version != null) {
490             if (checkSoftwareVersion(version.version) && checkApiVersion(version.apiVersion)) {
491                 return true;
492             }
493         } else {
494             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Can't get version information");
495         }
496         return false;
497     }
498
499     private boolean checkSoftwareVersion(@Nullable String softwareVersion) {
500         logger.debug("Bridge: Zoneminder software version is {}", softwareVersion);
501         if (softwareVersion != null) {
502             String[] versionParts = softwareVersion.split("\\.");
503             if (versionParts.length >= 2) {
504                 try {
505                     int versionMajor = Integer.parseInt(versionParts[0]);
506                     int versionMinor = Integer.parseInt(versionParts[1]);
507                     if (versionMajor == 1 && versionMinor >= 34) {
508                         logger.debug("Bridge: Zoneminder software version check OK");
509                         return true;
510                     } else {
511                         updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, String
512                                 .format("Current Zoneminder version: %s. Requires version >= 1.34.0", softwareVersion));
513                     }
514                 } catch (NumberFormatException e) {
515                     updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
516                             String.format("Badly formatted version number: %s", softwareVersion));
517                 }
518             } else {
519                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
520                         String.format("Can't parse software version: %s", softwareVersion));
521             }
522         } else {
523             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Software version is null");
524         }
525         return false;
526     }
527
528     private boolean checkApiVersion(@Nullable String apiVersion) {
529         logger.debug("Bridge: Zoneminder API version is {}", apiVersion);
530         if (apiVersion != null) {
531             String[] versionParts = apiVersion.split("\\.");
532             if (versionParts.length >= 2) {
533                 try {
534                     int versionMajor = Integer.parseInt(versionParts[0]);
535                     if (versionMajor >= 2) {
536                         logger.debug("Bridge: Zoneminder API version check OK");
537                         return true;
538                     } else {
539                         updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, String
540                                 .format("Requires API version >= 2.0. This Zoneminder is API version {}", apiVersion));
541                     }
542                 } catch (NumberFormatException e) {
543                     updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
544                             String.format("Badly formatted API version: %s", apiVersion));
545                 }
546             } else {
547                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
548                         String.format("Can't parse API version: %s", apiVersion));
549             }
550         } else {
551             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "API version is null");
552         }
553         return false;
554     }
555
556     @SuppressWarnings("null")
557     private void refreshMonitors() {
558         List<Monitor> monitors = getMonitors();
559         savedMonitors = monitors;
560         for (Monitor monitor : monitors) {
561             ZmMonitorHandler handler = monitorHandlers.get(monitor.getId());
562             if (handler != null) {
563                 handler.updateStatus(monitor);
564             }
565         }
566     }
567
568     private void scheduleRefreshJob() {
569         logger.debug("Bridge: Scheduling monitors refresh job");
570         cancelRefreshJob();
571         refreshMonitorsJob = scheduler.scheduleWithFixedDelay(this::refreshMonitors,
572                 MONITOR_REFRESH_STARTUP_DELAY_SECONDS, monitorRefreshInterval, TimeUnit.SECONDS);
573     }
574
575     private void cancelRefreshJob() {
576         Future<?> localRefreshThermostatsJob = refreshMonitorsJob;
577         if (localRefreshThermostatsJob != null) {
578             localRefreshThermostatsJob.cancel(true);
579             logger.debug("Bridge: Canceling monitors refresh job");
580             refreshMonitorsJob = null;
581         }
582     }
583 }