2 * Copyright (c) 2010-2023 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
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
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.zoneminder.internal.handler;
15 import static org.openhab.binding.zoneminder.internal.ZmBindingConstants.*;
17 import java.io.ByteArrayInputStream;
18 import java.io.IOException;
19 import java.net.URLEncoder;
20 import java.nio.charset.Charset;
21 import java.nio.charset.StandardCharsets;
22 import java.util.ArrayList;
23 import java.util.Collection;
24 import java.util.Collections;
25 import java.util.List;
28 import java.util.concurrent.ConcurrentHashMap;
29 import java.util.concurrent.ExecutionException;
30 import java.util.concurrent.Future;
31 import java.util.concurrent.TimeUnit;
32 import java.util.concurrent.TimeoutException;
34 import org.eclipse.jdt.annotation.NonNullByDefault;
35 import org.eclipse.jdt.annotation.Nullable;
36 import org.eclipse.jetty.client.HttpClient;
37 import org.eclipse.jetty.client.api.ContentResponse;
38 import org.eclipse.jetty.client.api.Request;
39 import org.eclipse.jetty.http.HttpHeader;
40 import org.eclipse.jetty.http.HttpMethod;
41 import org.eclipse.jetty.http.HttpStatus;
42 import org.openhab.binding.zoneminder.internal.ZmStateDescriptionOptionsProvider;
43 import org.openhab.binding.zoneminder.internal.config.ZmBridgeConfig;
44 import org.openhab.binding.zoneminder.internal.discovery.MonitorDiscoveryService;
45 import org.openhab.binding.zoneminder.internal.dto.EventDTO;
46 import org.openhab.binding.zoneminder.internal.dto.EventSummaryDTO;
47 import org.openhab.binding.zoneminder.internal.dto.EventsDTO;
48 import org.openhab.binding.zoneminder.internal.dto.MonitorDTO;
49 import org.openhab.binding.zoneminder.internal.dto.MonitorItemDTO;
50 import org.openhab.binding.zoneminder.internal.dto.MonitorStateDTO;
51 import org.openhab.binding.zoneminder.internal.dto.MonitorStatusDTO;
52 import org.openhab.binding.zoneminder.internal.dto.MonitorsDTO;
53 import org.openhab.binding.zoneminder.internal.dto.RunStateDTO;
54 import org.openhab.binding.zoneminder.internal.dto.RunStateDTO.RunState;
55 import org.openhab.binding.zoneminder.internal.dto.RunStatesDTO;
56 import org.openhab.binding.zoneminder.internal.dto.VersionDTO;
57 import org.openhab.core.io.net.http.HttpUtil;
58 import org.openhab.core.library.types.OnOffType;
59 import org.openhab.core.library.types.RawType;
60 import org.openhab.core.library.types.StringType;
61 import org.openhab.core.thing.Bridge;
62 import org.openhab.core.thing.ChannelUID;
63 import org.openhab.core.thing.Thing;
64 import org.openhab.core.thing.ThingStatus;
65 import org.openhab.core.thing.ThingStatusDetail;
66 import org.openhab.core.thing.binding.BaseBridgeHandler;
67 import org.openhab.core.thing.binding.ThingHandler;
68 import org.openhab.core.thing.binding.ThingHandlerService;
69 import org.openhab.core.types.Command;
70 import org.openhab.core.types.RefreshType;
71 import org.openhab.core.types.StateOption;
72 import org.openhab.core.types.UnDefType;
73 import org.slf4j.Logger;
74 import org.slf4j.LoggerFactory;
76 import com.google.gson.Gson;
77 import com.google.gson.GsonBuilder;
78 import com.google.gson.JsonSyntaxException;
81 * The {@link ZmBridgeHandler} represents the Zoneminder server. It handles all communication
82 * with the Zoneminder server.
84 * @author Mark Hilbush - Initial contribution
87 public class ZmBridgeHandler extends BaseBridgeHandler {
89 private static final int MONITOR_REFRESH_INTERVAL_SECONDS = 10;
90 private static final int MONITOR_REFRESH_STARTUP_DELAY_SECONDS = 5;
92 private static final int API_TIMEOUT_MSEC = 10000;
94 private static final String LOGIN_PATH = "/api/host/login.json";
96 private static final String STREAM_IMAGE = "single";
97 private static final String STREAM_VIDEO = "jpeg";
99 private static final List<String> EMPTY_LIST = Collections.emptyList();
101 private static final Gson GSON = new GsonBuilder().setDateFormat("yyyy-MM-dd HH:mm:ss").create();
103 private final Logger logger = LoggerFactory.getLogger(ZmBridgeHandler.class);
105 private @Nullable Future<?> refreshMonitorsJob;
107 private List<Monitor> savedMonitors = new ArrayList<>();
109 private String host = "";
110 private boolean useSSL;
111 private @Nullable String portNumber;
112 private String urlPath = DEFAULT_URL_PATH;
113 private int monitorRefreshInterval;
114 private boolean backgroundDiscoveryEnabled;
115 private int defaultAlarmDuration;
116 private @Nullable Integer defaultImageRefreshInterval;
118 private final HttpClient httpClient;
119 private final ZmStateDescriptionOptionsProvider stateDescriptionProvider;
121 private ZmAuth zmAuth;
123 // Maintain mapping of handler and monitor id
124 private final Map<String, ZmMonitorHandler> monitorHandlers = new ConcurrentHashMap<>();
126 public ZmBridgeHandler(Bridge thing, HttpClient httpClient,
127 ZmStateDescriptionOptionsProvider stateDescriptionProvider) {
129 this.httpClient = httpClient;
130 this.stateDescriptionProvider = stateDescriptionProvider;
131 // Default to use no authentication
132 zmAuth = new ZmAuth(this);
136 public void initialize() {
137 ZmBridgeConfig config = getConfigAs(ZmBridgeConfig.class);
140 value = config.refreshInterval;
141 monitorRefreshInterval = value == null ? MONITOR_REFRESH_INTERVAL_SECONDS : value;
143 value = config.defaultAlarmDuration;
144 defaultAlarmDuration = value == null ? DEFAULT_ALARM_DURATION_SECONDS : value;
146 defaultImageRefreshInterval = config.defaultImageRefreshInterval;
148 backgroundDiscoveryEnabled = config.discoveryEnabled;
149 logger.debug("Bridge: Background discovery is {}", backgroundDiscoveryEnabled ? "ENABLED" : "DISABLED");
152 useSSL = config.useSSL.booleanValue();
153 portNumber = config.portNumber != null ? Integer.toString(config.portNumber) : null;
154 urlPath = "/".equals(config.urlPath) ? "" : config.urlPath;
156 // If user and password are configured, then use Zoneminder authentication
157 if (config.user != null && config.pass != null) {
158 zmAuth = new ZmAuth(this, config.user, config.pass);
161 updateStatus(ThingStatus.ONLINE);
162 scheduleRefreshJob();
167 public void dispose() {
172 public void childHandlerInitialized(ThingHandler childHandler, Thing childThing) {
173 String monitorId = (String) childThing.getConfiguration().get(CONFIG_MONITOR_ID);
174 monitorHandlers.put(monitorId, (ZmMonitorHandler) childHandler);
175 logger.debug("Bridge: Monitor handler was initialized for {} with id {}", childThing.getUID(), monitorId);
179 public void childHandlerDisposed(ThingHandler childHandler, Thing childThing) {
180 String monitorId = (String) childThing.getConfiguration().get(CONFIG_MONITOR_ID);
181 monitorHandlers.remove(monitorId);
182 logger.debug("Bridge: Monitor handler was disposed for {} with id {}", childThing.getUID(), monitorId);
186 public void handleCommand(ChannelUID channelUID, Command command) {
187 switch (channelUID.getId()) {
188 case CHANNEL_IMAGE_MONITOR_ID:
189 handleMonitorIdCommand(command, CHANNEL_IMAGE_MONITOR_ID, CHANNEL_IMAGE_URL, STREAM_IMAGE);
191 case CHANNEL_VIDEO_MONITOR_ID:
192 handleMonitorIdCommand(command, CHANNEL_VIDEO_MONITOR_ID, CHANNEL_VIDEO_URL, STREAM_VIDEO);
194 case CHANNEL_RUN_STATE:
195 if (command instanceof StringType) {
196 changeRunState(command);
202 private void handleMonitorIdCommand(Command command, String monitorIdChannelId, String urlChannelId, String type) {
203 if (command instanceof RefreshType || command == OnOffType.OFF) {
204 updateState(monitorIdChannelId, UnDefType.UNDEF);
205 updateState(urlChannelId, UnDefType.UNDEF);
206 } else if (command instanceof StringType) {
207 String id = command.toString();
208 if (isMonitorIdValid(id)) {
209 updateState(urlChannelId, new StringType(buildStreamUrl(id, type)));
211 updateState(monitorIdChannelId, UnDefType.UNDEF);
212 updateState(urlChannelId, UnDefType.UNDEF);
217 private void changeRunState(Command command) {
218 logger.debug("Bridge: Change run state to {}", command);
219 executeGet(buildUrl(String.format("/api/states/change/%s.json",
220 URLEncoder.encode(command.toString(), Charset.defaultCharset()))));
224 public Collection<Class<? extends ThingHandlerService>> getServices() {
225 return Collections.singleton(MonitorDiscoveryService.class);
228 public boolean isBackgroundDiscoveryEnabled() {
229 return backgroundDiscoveryEnabled;
232 public Integer getDefaultAlarmDuration() {
233 return defaultAlarmDuration;
236 public @Nullable Integer getDefaultImageRefreshInterval() {
237 return defaultImageRefreshInterval;
240 public List<Monitor> getSavedMonitors() {
241 return savedMonitors;
244 public Gson getGson() {
248 public void setFunction(String id, MonitorFunction function) {
249 if (!zmAuth.isAuthorized()) {
252 logger.debug("Bridge: Setting monitor {} function to {}", id, function);
253 executePost(buildUrl(String.format("/api/monitors/%s.json", id)),
254 String.format("Monitor[Function]=%s", function.toString()));
257 public void setEnabled(String id, OnOffType enabled) {
258 if (!zmAuth.isAuthorized()) {
261 logger.debug("Bridge: Setting monitor {} to {}", id, enabled);
262 executePost(buildUrl(String.format("/api/monitors/%s.json", id)),
263 String.format("Monitor[Enabled]=%s", enabled == OnOffType.ON ? "1" : "0"));
266 public void setAlarmOn(String id) {
267 if (!zmAuth.isAuthorized()) {
270 logger.debug("Bridge: Turning alarm ON for monitor {}", id);
271 setAlarm(buildUrl(String.format("/api/monitors/alarm/id:%s/command:on.json", id)));
274 public void setAlarmOff(String id) {
275 if (!zmAuth.isAuthorized()) {
278 logger.debug("Bridge: Turning alarm OFF for monitor {}", id);
279 setAlarm(buildUrl(String.format("/api/monitors/alarm/id:%s/command:off.json", id)));
282 public @Nullable RawType getImage(String id, @Nullable Integer imageRefreshIntervalSeconds) {
283 Integer localRefreshInterval = imageRefreshIntervalSeconds;
284 if (localRefreshInterval == null || localRefreshInterval.intValue() < 1 || !zmAuth.isAuthorized()) {
287 // Call should timeout just before the refresh interval
288 int timeout = Math.min((localRefreshInterval * 1000) - 500, API_TIMEOUT_MSEC);
289 Request request = httpClient.newRequest(buildStreamUrl(id, STREAM_IMAGE));
290 request.method(HttpMethod.GET);
291 request.timeout(timeout, TimeUnit.MILLISECONDS);
295 ContentResponse response = request.send();
296 if (response.getStatus() == HttpStatus.OK_200) {
297 RawType image = new RawType(response.getContent(), response.getHeaders().get(HttpHeader.CONTENT_TYPE));
300 errorMsg = String.format("HTTP GET failed: %d, %s", response.getStatus(), response.getReason());
302 } catch (TimeoutException e) {
303 errorMsg = String.format("TimeoutException: Call to Zoneminder API timed out after {} msec", timeout);
304 } catch (ExecutionException e) {
305 errorMsg = String.format("ExecutionException: %s", e.getMessage());
306 } catch (InterruptedException e) {
307 errorMsg = String.format("InterruptedException: %s", e.getMessage());
308 Thread.currentThread().interrupt();
310 logger.debug("{}", errorMsg);
314 @SuppressWarnings("null")
315 private synchronized List<Monitor> getMonitors() {
316 List<Monitor> monitorList = new ArrayList<>();
317 if (!zmAuth.isAuthorized()) {
321 String response = executeGet(buildUrl("/api/monitors.json"));
322 MonitorsDTO monitorsDTO = GSON.fromJson(response, MonitorsDTO.class);
323 if (monitorsDTO != null && monitorsDTO.monitorItems != null) {
324 List<StateOption> options = new ArrayList<>();
325 for (MonitorItemDTO monitorItemDTO : monitorsDTO.monitorItems) {
326 MonitorDTO monitorDTO = monitorItemDTO.monitor;
327 MonitorStatusDTO monitorStatusDTO = monitorItemDTO.monitorStatus;
328 if (monitorDTO != null && monitorStatusDTO != null) {
329 Monitor monitor = new Monitor(monitorDTO.id, monitorDTO.name, monitorDTO.function,
330 monitorDTO.enabled, monitorStatusDTO.status);
331 extractEventCounts(monitor, monitorItemDTO);
332 monitor.setImageUrl(buildStreamUrl(monitorDTO.id, STREAM_IMAGE));
333 monitor.setVideoUrl(buildStreamUrl(monitorDTO.id, STREAM_VIDEO));
334 monitorList.add(monitor);
335 options.add(new StateOption(monitorDTO.id, "Monitor " + monitorDTO.id));
338 // Update state options
339 stateDescriptionProvider.setStateOptions(new ChannelUID(getThing().getUID(), CHANNEL_IMAGE_MONITOR_ID),
341 stateDescriptionProvider.setStateOptions(new ChannelUID(getThing().getUID(), CHANNEL_VIDEO_MONITOR_ID),
343 // Only update alarm and event info for monitors whose handlers are initialized
344 Set<String> ids = monitorHandlers.keySet();
345 for (Monitor m : monitorList) {
346 if (ids.contains(m.getId())) {
347 m.setState(getState(m.getId()));
348 m.setLastEvent(getLastEvent(m.getId()));
353 } catch (JsonSyntaxException e) {
354 logger.debug("Bridge: JsonSyntaxException: {}", e.getMessage(), e);
359 private void extractEventCounts(Monitor monitor, MonitorItemDTO monitorItemDTO) {
361 * The Zoneminder API changed in version 1.36.x such that the event counts moved from the
362 * monitor object to a new event summary object. Therefore, if the event summary object
363 * exists in the JSON response, pull the event counts from that object, otherwise get the
364 * counts from the monitor object.
366 if (monitorItemDTO.eventSummary != null) {
367 EventSummaryDTO eventSummaryDTO = monitorItemDTO.eventSummary;
368 monitor.setHourEvents(eventSummaryDTO.hourEvents);
369 monitor.setDayEvents(eventSummaryDTO.dayEvents);
370 monitor.setWeekEvents(eventSummaryDTO.weekEvents);
371 monitor.setMonthEvents(eventSummaryDTO.monthEvents);
372 monitor.setTotalEvents(eventSummaryDTO.totalEvents);
374 MonitorDTO monitorDTO = monitorItemDTO.monitor;
375 monitor.setHourEvents(monitorDTO.hourEvents);
376 monitor.setDayEvents(monitorDTO.dayEvents);
377 monitor.setWeekEvents(monitorDTO.weekEvents);
378 monitor.setMonthEvents(monitorDTO.monthEvents);
379 monitor.setTotalEvents(monitorDTO.totalEvents);
383 @SuppressWarnings("null")
384 private @Nullable Event getLastEvent(String id) {
385 if (!zmAuth.isAuthorized()) {
389 List<String> parameters = new ArrayList<>();
390 parameters.add("sort=StartTime");
391 parameters.add("direction=desc");
392 parameters.add("limit=1");
393 String response = executeGet(buildUrlWithParameters(
394 String.format("/api/events/index/MonitorId:%s/Name!=:New%%20Event.json", id), parameters));
395 EventsDTO events = GSON.fromJson(response, EventsDTO.class);
396 if (events != null && events.eventsList != null && events.eventsList.size() == 1) {
397 EventDTO e = events.eventsList.get(0).event;
398 Event event = new Event(e.eventId, e.name, e.cause, e.notes, e.startTime, e.endTime);
399 event.setFrames(e.frames);
400 event.setAlarmFrames(e.alarmFrames);
401 event.setLength(e.length);
404 } catch (JsonSyntaxException e) {
405 logger.debug("Bridge: JsonSyntaxException: {}", e.getMessage(), e);
410 private void updateRunStates() {
411 if (!zmAuth.isAuthorized() || !isLinked(CHANNEL_RUN_STATE)) {
415 String response = executeGet(buildUrl("/api/states.json"));
416 RunStatesDTO runStates = GSON.fromJson(response, RunStatesDTO.class);
417 if (runStates != null) {
418 List<StateOption> options = new ArrayList<>();
419 for (RunStateDTO runState : runStates.runStatesList) {
420 RunState state = runState.runState;
421 logger.debug("Found runstate: id={}, name={}, desc={}, isActive={}", state.id, state.name,
422 state.definition, state.isActive);
423 options.add(new StateOption(state.name, state.name));
424 if ("1".equals(state.isActive)) {
425 updateState(CHANNEL_RUN_STATE, new StringType(state.name));
428 stateDescriptionProvider.setStateOptions(new ChannelUID(getThing().getUID(), CHANNEL_RUN_STATE),
431 } catch (JsonSyntaxException e) {
432 logger.debug("Bridge: JsonSyntaxException: {}", e.getMessage(), e);
436 private @Nullable VersionDTO getVersion() {
437 if (!zmAuth.isAuthorized()) {
440 VersionDTO version = null;
442 String response = executeGet(buildUrl("/api/host/getVersion.json"));
443 version = GSON.fromJson(response, VersionDTO.class);
444 } catch (JsonSyntaxException e) {
445 logger.debug("Bridge: JsonSyntaxException: {}", e.getMessage(), e);
450 private void setAlarm(String url) {
454 @SuppressWarnings("null")
455 private MonitorState getState(String id) {
456 if (!zmAuth.isAuthorized()) {
457 return MonitorState.UNKNOWN;
460 String response = executeGet(buildUrl(String.format("/api/monitors/alarm/id:%s/command:status.json", id)));
461 MonitorStateDTO monitorState = GSON.fromJson(response, MonitorStateDTO.class);
462 if (monitorState != null) {
463 MonitorState state = monitorState.state;
464 return state != null ? state : MonitorState.UNKNOWN;
466 } catch (JsonSyntaxException e) {
467 logger.debug("Bridge: JsonSyntaxException: {}", e.getMessage(), e);
469 return MonitorState.UNKNOWN;
472 public @Nullable String executeGet(String url) {
474 long startTime = System.currentTimeMillis();
475 String response = HttpUtil.executeUrl("GET", url, API_TIMEOUT_MSEC);
476 logger.trace("Bridge: Http GET of '{}' returned '{}' in {} ms", url, response,
477 System.currentTimeMillis() - startTime);
479 } catch (IOException e) {
480 logger.debug("Bridge: IOException on GET request, url='{}': {}", url, e.getMessage());
485 private @Nullable String executePost(String url, String content) {
486 return executePost(url, content, "application/x-www-form-urlencoded");
489 public @Nullable String executePost(String url, String content, String contentType) {
490 try (ByteArrayInputStream inputStream = new ByteArrayInputStream(content.getBytes(StandardCharsets.UTF_8))) {
491 long startTime = System.currentTimeMillis();
492 String response = HttpUtil.executeUrl("POST", url, inputStream, contentType, API_TIMEOUT_MSEC);
493 logger.trace("Bridge: Http POST content '{}' to '{}' returned: {} in {} ms", content, url, response,
494 System.currentTimeMillis() - startTime);
496 } catch (IOException e) {
497 logger.debug("Bridge: IOException on POST request, url='{}': {}", url, e.getMessage());
502 public String buildLoginUrl() {
503 return buildBaseUrl(LOGIN_PATH).toString();
506 public String buildLoginUrl(String tokenParameter) {
507 StringBuilder sb = buildBaseUrl(LOGIN_PATH);
508 sb.append(tokenParameter);
509 return sb.toString();
512 private String buildStreamUrl(String id, String streamType) {
513 List<String> parameters = new ArrayList<>();
514 parameters.add(String.format("mode=%s", streamType));
515 parameters.add(String.format("monitor=%s", id));
516 return buildUrlWithParameters("/cgi-bin/zms", parameters);
519 private String buildUrl(String path) {
520 return buildUrlWithParameters(path, EMPTY_LIST);
523 private String buildUrlWithParameters(String path, List<String> parameters) {
524 StringBuilder sb = buildBaseUrl(path);
526 for (String parameter : parameters) {
527 sb.append(joiner).append(parameter);
530 if (zmAuth.usingAuthorization()) {
531 sb.append(joiner).append("token=").append(zmAuth.getAccessToken());
533 return sb.toString();
536 private StringBuilder buildBaseUrl(String path) {
537 StringBuilder sb = new StringBuilder();
538 sb.append(useSSL ? "https://" : "http://");
540 if (portNumber != null) {
541 sb.append(":").append(portNumber);
548 private boolean isMonitorIdValid(String id) {
549 return savedMonitors.stream().filter(monitor -> id.equals(monitor.getId())).findAny().isPresent();
552 private boolean isHostValid() {
553 logger.debug("Bridge: Checking for valid Zoneminder host: {}", host);
554 VersionDTO version = getVersion();
555 if (version != null) {
556 if (checkSoftwareVersion(version.version) && checkApiVersion(version.apiVersion)) {
560 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Can't get version information");
565 private boolean checkSoftwareVersion(@Nullable String softwareVersion) {
566 logger.debug("Bridge: Zoneminder software version is {}", softwareVersion);
567 if (softwareVersion != null) {
568 String[] versionParts = softwareVersion.split("\\.");
569 if (versionParts.length >= 2) {
571 int versionMajor = Integer.parseInt(versionParts[0]);
572 int versionMinor = Integer.parseInt(versionParts[1]);
573 if (versionMajor == 1 && versionMinor >= 34) {
574 logger.debug("Bridge: Zoneminder software version check OK");
577 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, String
578 .format("Current Zoneminder version: %s. Requires version >= 1.34.0", softwareVersion));
580 } catch (NumberFormatException e) {
581 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
582 String.format("Badly formatted version number: %s", softwareVersion));
585 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
586 String.format("Can't parse software version: %s", softwareVersion));
589 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Software version is null");
594 private boolean checkApiVersion(@Nullable String apiVersion) {
595 logger.debug("Bridge: Zoneminder API version is {}", apiVersion);
596 if (apiVersion != null) {
597 String[] versionParts = apiVersion.split("\\.");
598 if (versionParts.length >= 2) {
600 int versionMajor = Integer.parseInt(versionParts[0]);
601 if (versionMajor >= 2) {
602 logger.debug("Bridge: Zoneminder API version check OK");
605 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, String
606 .format("Requires API version >= 2.0. This Zoneminder is API version {}", apiVersion));
608 } catch (NumberFormatException e) {
609 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
610 String.format("Badly formatted API version: %s", apiVersion));
613 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
614 String.format("Can't parse API version: %s", apiVersion));
617 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "API version is null");
622 @SuppressWarnings("null")
623 private void refreshMonitors() {
624 List<Monitor> monitors = getMonitors();
625 savedMonitors = monitors;
626 for (Monitor monitor : monitors) {
627 ZmMonitorHandler handler = monitorHandlers.get(monitor.getId());
628 if (handler != null) {
629 handler.updateStatus(monitor);
634 private void scheduleRefreshJob() {
635 logger.debug("Bridge: Scheduling monitors refresh job");
637 refreshMonitorsJob = scheduler.scheduleWithFixedDelay(this::refreshMonitors,
638 MONITOR_REFRESH_STARTUP_DELAY_SECONDS, monitorRefreshInterval, TimeUnit.SECONDS);
641 private void cancelRefreshJob() {
642 Future<?> localRefreshThermostatsJob = refreshMonitorsJob;
643 if (localRefreshThermostatsJob != null) {
644 localRefreshThermostatsJob.cancel(true);
645 logger.debug("Bridge: Canceling monitors refresh job");
646 refreshMonitorsJob = null;