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 Set.of(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 return new RawType(response.getContent(), response.getHeaders().get(HttpHeader.CONTENT_TYPE));
299 errorMsg = String.format("HTTP GET failed: %d, %s", response.getStatus(), response.getReason());
301 } catch (TimeoutException e) {
302 errorMsg = String.format("TimeoutException: Call to Zoneminder API timed out after {} msec", timeout);
303 } catch (ExecutionException e) {
304 errorMsg = String.format("ExecutionException: %s", e.getMessage());
305 } catch (InterruptedException e) {
306 errorMsg = String.format("InterruptedException: %s", e.getMessage());
307 Thread.currentThread().interrupt();
309 logger.debug("{}", errorMsg);
313 @SuppressWarnings("null")
314 private synchronized List<Monitor> getMonitors() {
315 List<Monitor> monitorList = new ArrayList<>();
316 if (!zmAuth.isAuthorized()) {
320 String response = executeGet(buildUrl("/api/monitors.json"));
321 MonitorsDTO monitorsDTO = GSON.fromJson(response, MonitorsDTO.class);
322 if (monitorsDTO != null && monitorsDTO.monitorItems != null) {
323 List<StateOption> options = new ArrayList<>();
324 for (MonitorItemDTO monitorItemDTO : monitorsDTO.monitorItems) {
325 MonitorDTO monitorDTO = monitorItemDTO.monitor;
326 MonitorStatusDTO monitorStatusDTO = monitorItemDTO.monitorStatus;
327 if (monitorDTO != null && monitorStatusDTO != null) {
328 Monitor monitor = new Monitor(monitorDTO.id, monitorDTO.name, monitorDTO.function,
329 monitorDTO.enabled, monitorStatusDTO.status);
330 extractEventCounts(monitor, monitorItemDTO);
331 monitor.setImageUrl(buildStreamUrl(monitorDTO.id, STREAM_IMAGE));
332 monitor.setVideoUrl(buildStreamUrl(monitorDTO.id, STREAM_VIDEO));
333 monitorList.add(monitor);
334 options.add(new StateOption(monitorDTO.id, "Monitor " + monitorDTO.id));
337 // Update state options
338 stateDescriptionProvider.setStateOptions(new ChannelUID(getThing().getUID(), CHANNEL_IMAGE_MONITOR_ID),
340 stateDescriptionProvider.setStateOptions(new ChannelUID(getThing().getUID(), CHANNEL_VIDEO_MONITOR_ID),
342 // Only update alarm and event info for monitors whose handlers are initialized
343 Set<String> ids = monitorHandlers.keySet();
344 for (Monitor m : monitorList) {
345 if (ids.contains(m.getId())) {
346 m.setState(getState(m.getId()));
347 m.setLastEvent(getLastEvent(m.getId()));
352 } catch (JsonSyntaxException e) {
353 logger.debug("Bridge: JsonSyntaxException: {}", e.getMessage(), e);
358 private void extractEventCounts(Monitor monitor, MonitorItemDTO monitorItemDTO) {
360 * The Zoneminder API changed in version 1.36.x such that the event counts moved from the
361 * monitor object to a new event summary object. Therefore, if the event summary object
362 * exists in the JSON response, pull the event counts from that object, otherwise get the
363 * counts from the monitor object.
365 if (monitorItemDTO.eventSummary != null) {
366 EventSummaryDTO eventSummaryDTO = monitorItemDTO.eventSummary;
367 monitor.setHourEvents(eventSummaryDTO.hourEvents);
368 monitor.setDayEvents(eventSummaryDTO.dayEvents);
369 monitor.setWeekEvents(eventSummaryDTO.weekEvents);
370 monitor.setMonthEvents(eventSummaryDTO.monthEvents);
371 monitor.setTotalEvents(eventSummaryDTO.totalEvents);
373 MonitorDTO monitorDTO = monitorItemDTO.monitor;
374 monitor.setHourEvents(monitorDTO.hourEvents);
375 monitor.setDayEvents(monitorDTO.dayEvents);
376 monitor.setWeekEvents(monitorDTO.weekEvents);
377 monitor.setMonthEvents(monitorDTO.monthEvents);
378 monitor.setTotalEvents(monitorDTO.totalEvents);
382 @SuppressWarnings("null")
383 private @Nullable Event getLastEvent(String id) {
384 if (!zmAuth.isAuthorized()) {
388 List<String> parameters = new ArrayList<>();
389 parameters.add("sort=StartTime");
390 parameters.add("direction=desc");
391 parameters.add("limit=1");
392 String response = executeGet(buildUrlWithParameters(
393 String.format("/api/events/index/MonitorId:%s/Name!=:New%%20Event.json", id), parameters));
394 EventsDTO events = GSON.fromJson(response, EventsDTO.class);
395 if (events != null && events.eventsList != null && events.eventsList.size() == 1) {
396 EventDTO e = events.eventsList.get(0).event;
397 Event event = new Event(e.eventId, e.name, e.cause, e.notes, e.startTime, e.endTime);
398 event.setFrames(e.frames);
399 event.setAlarmFrames(e.alarmFrames);
400 event.setLength(e.length);
403 } catch (JsonSyntaxException e) {
404 logger.debug("Bridge: JsonSyntaxException: {}", e.getMessage(), e);
409 private void updateRunStates() {
410 if (!zmAuth.isAuthorized() || !isLinked(CHANNEL_RUN_STATE)) {
414 String response = executeGet(buildUrl("/api/states.json"));
415 RunStatesDTO runStates = GSON.fromJson(response, RunStatesDTO.class);
416 if (runStates != null) {
417 List<StateOption> options = new ArrayList<>();
418 for (RunStateDTO runState : runStates.runStatesList) {
419 RunState state = runState.runState;
420 logger.debug("Found runstate: id={}, name={}, desc={}, isActive={}", state.id, state.name,
421 state.definition, state.isActive);
422 options.add(new StateOption(state.name, state.name));
423 if ("1".equals(state.isActive)) {
424 updateState(CHANNEL_RUN_STATE, new StringType(state.name));
427 stateDescriptionProvider.setStateOptions(new ChannelUID(getThing().getUID(), CHANNEL_RUN_STATE),
430 } catch (JsonSyntaxException e) {
431 logger.debug("Bridge: JsonSyntaxException: {}", e.getMessage(), e);
435 private @Nullable VersionDTO getVersion() {
436 if (!zmAuth.isAuthorized()) {
439 VersionDTO version = null;
441 String response = executeGet(buildUrl("/api/host/getVersion.json"));
442 version = GSON.fromJson(response, VersionDTO.class);
443 } catch (JsonSyntaxException e) {
444 logger.debug("Bridge: JsonSyntaxException: {}", e.getMessage(), e);
449 private void setAlarm(String url) {
453 @SuppressWarnings("null")
454 private MonitorState getState(String id) {
455 if (!zmAuth.isAuthorized()) {
456 return MonitorState.UNKNOWN;
459 String response = executeGet(buildUrl(String.format("/api/monitors/alarm/id:%s/command:status.json", id)));
460 MonitorStateDTO monitorState = GSON.fromJson(response, MonitorStateDTO.class);
461 if (monitorState != null) {
462 MonitorState state = monitorState.state;
463 return state != null ? state : MonitorState.UNKNOWN;
465 } catch (JsonSyntaxException e) {
466 logger.debug("Bridge: JsonSyntaxException: {}", e.getMessage(), e);
468 return MonitorState.UNKNOWN;
471 public @Nullable String executeGet(String url) {
473 long startTime = System.currentTimeMillis();
474 String response = HttpUtil.executeUrl("GET", url, API_TIMEOUT_MSEC);
475 logger.trace("Bridge: Http GET of '{}' returned '{}' in {} ms", url, response,
476 System.currentTimeMillis() - startTime);
478 } catch (IOException e) {
479 logger.debug("Bridge: IOException on GET request, url='{}': {}", url, e.getMessage());
484 private @Nullable String executePost(String url, String content) {
485 return executePost(url, content, "application/x-www-form-urlencoded");
488 public @Nullable String executePost(String url, String content, String contentType) {
489 try (ByteArrayInputStream inputStream = new ByteArrayInputStream(content.getBytes(StandardCharsets.UTF_8))) {
490 long startTime = System.currentTimeMillis();
491 String response = HttpUtil.executeUrl("POST", url, inputStream, contentType, API_TIMEOUT_MSEC);
492 logger.trace("Bridge: Http POST content '{}' to '{}' returned: {} in {} ms", content, url, response,
493 System.currentTimeMillis() - startTime);
495 } catch (IOException e) {
496 logger.debug("Bridge: IOException on POST request, url='{}': {}", url, e.getMessage());
501 public String buildLoginUrl() {
502 return buildBaseUrl(LOGIN_PATH).toString();
505 public String buildLoginUrl(String tokenParameter) {
506 StringBuilder sb = buildBaseUrl(LOGIN_PATH);
507 sb.append(tokenParameter);
508 return sb.toString();
511 private String buildStreamUrl(String id, String streamType) {
512 List<String> parameters = new ArrayList<>();
513 parameters.add(String.format("mode=%s", streamType));
514 parameters.add(String.format("monitor=%s", id));
515 return buildUrlWithParameters("/cgi-bin/zms", parameters);
518 private String buildUrl(String path) {
519 return buildUrlWithParameters(path, EMPTY_LIST);
522 private String buildUrlWithParameters(String path, List<String> parameters) {
523 StringBuilder sb = buildBaseUrl(path);
525 for (String parameter : parameters) {
526 sb.append(joiner).append(parameter);
529 if (zmAuth.usingAuthorization()) {
530 sb.append(joiner).append("token=").append(zmAuth.getAccessToken());
532 return sb.toString();
535 private StringBuilder buildBaseUrl(String path) {
536 StringBuilder sb = new StringBuilder();
537 sb.append(useSSL ? "https://" : "http://");
539 if (portNumber != null) {
540 sb.append(":").append(portNumber);
547 private boolean isMonitorIdValid(String id) {
548 return savedMonitors.stream().filter(monitor -> id.equals(monitor.getId())).findAny().isPresent();
551 private boolean isHostValid() {
552 logger.debug("Bridge: Checking for valid Zoneminder host: {}", host);
553 VersionDTO version = getVersion();
554 if (version != null) {
555 if (checkSoftwareVersion(version.version) && checkApiVersion(version.apiVersion)) {
559 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Can't get version information");
564 private boolean checkSoftwareVersion(@Nullable String softwareVersion) {
565 logger.debug("Bridge: Zoneminder software version is {}", softwareVersion);
566 if (softwareVersion != null) {
567 String[] versionParts = softwareVersion.split("\\.");
568 if (versionParts.length >= 2) {
570 int versionMajor = Integer.parseInt(versionParts[0]);
571 int versionMinor = Integer.parseInt(versionParts[1]);
572 if (versionMajor == 1 && versionMinor >= 34) {
573 logger.debug("Bridge: Zoneminder software version check OK");
576 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, String
577 .format("Current Zoneminder version: %s. Requires version >= 1.34.0", softwareVersion));
579 } catch (NumberFormatException e) {
580 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
581 String.format("Badly formatted version number: %s", softwareVersion));
584 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
585 String.format("Can't parse software version: %s", softwareVersion));
588 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Software version is null");
593 private boolean checkApiVersion(@Nullable String apiVersion) {
594 logger.debug("Bridge: Zoneminder API version is {}", apiVersion);
595 if (apiVersion != null) {
596 String[] versionParts = apiVersion.split("\\.");
597 if (versionParts.length >= 2) {
599 int versionMajor = Integer.parseInt(versionParts[0]);
600 if (versionMajor >= 2) {
601 logger.debug("Bridge: Zoneminder API version check OK");
604 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, String
605 .format("Requires API version >= 2.0. This Zoneminder is API version {}", apiVersion));
607 } catch (NumberFormatException e) {
608 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
609 String.format("Badly formatted API version: %s", apiVersion));
612 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
613 String.format("Can't parse API version: %s", apiVersion));
616 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "API version is null");
621 @SuppressWarnings("null")
622 private void refreshMonitors() {
623 List<Monitor> monitors = getMonitors();
624 savedMonitors = monitors;
625 for (Monitor monitor : monitors) {
626 ZmMonitorHandler handler = monitorHandlers.get(monitor.getId());
627 if (handler != null) {
628 handler.updateStatus(monitor);
633 private void scheduleRefreshJob() {
634 logger.debug("Bridge: Scheduling monitors refresh job");
636 refreshMonitorsJob = scheduler.scheduleWithFixedDelay(this::refreshMonitors,
637 MONITOR_REFRESH_STARTUP_DELAY_SECONDS, monitorRefreshInterval, TimeUnit.SECONDS);
640 private void cancelRefreshJob() {
641 Future<?> localRefreshThermostatsJob = refreshMonitorsJob;
642 if (localRefreshThermostatsJob != null) {
643 localRefreshThermostatsJob.cancel(true);
644 logger.debug("Bridge: Canceling monitors refresh job");
645 refreshMonitorsJob = null;