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