2 * Copyright (c) 2010-2024 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.time.ZonedDateTime;
18 import java.util.Collection;
21 import java.util.concurrent.ConcurrentHashMap;
22 import java.util.concurrent.ScheduledFuture;
23 import java.util.concurrent.TimeUnit;
25 import javax.measure.quantity.Time;
27 import org.eclipse.jdt.annotation.NonNullByDefault;
28 import org.eclipse.jdt.annotation.Nullable;
29 import org.openhab.binding.zoneminder.internal.action.ZmActions;
30 import org.openhab.binding.zoneminder.internal.config.ZmMonitorConfig;
31 import org.openhab.core.i18n.TimeZoneProvider;
32 import org.openhab.core.library.types.DateTimeType;
33 import org.openhab.core.library.types.DecimalType;
34 import org.openhab.core.library.types.OnOffType;
35 import org.openhab.core.library.types.QuantityType;
36 import org.openhab.core.library.types.RawType;
37 import org.openhab.core.library.types.StringType;
38 import org.openhab.core.library.unit.Units;
39 import org.openhab.core.thing.ChannelUID;
40 import org.openhab.core.thing.Thing;
41 import org.openhab.core.thing.ThingStatus;
42 import org.openhab.core.thing.binding.BaseThingHandler;
43 import org.openhab.core.thing.binding.ThingHandlerService;
44 import org.openhab.core.types.Command;
45 import org.openhab.core.types.RefreshType;
46 import org.openhab.core.types.State;
47 import org.openhab.core.types.UnDefType;
48 import org.slf4j.Logger;
49 import org.slf4j.LoggerFactory;
52 * The {@link ZmMonitorHandler} represents a Zoneminder monitor. The monitor handler
53 * interacts with the server bridge to communicate with the Zoneminder server.
55 * @author Mark Hilbush - Initial contribution
58 public class ZmMonitorHandler extends BaseThingHandler {
60 private final Logger logger = LoggerFactory.getLogger(ZmMonitorHandler.class);
62 private final TimeZoneProvider timeZoneProvider;
64 private @Nullable ZmBridgeHandler bridgeHandler;
66 private @NonNullByDefault({}) String monitorId;
67 private @Nullable Integer imageRefreshIntervalSeconds;
68 private Integer alarmDuration = DEFAULT_ALARM_DURATION_SECONDS;
70 private @Nullable ScheduledFuture<?> imageRefreshJob;
71 private @Nullable ScheduledFuture<?> alarmOffJob;
73 private final Map<String, State> monitorStatusCache = new ConcurrentHashMap<>();
75 public ZmMonitorHandler(Thing thing, TimeZoneProvider timeZoneProvider) {
77 this.timeZoneProvider = timeZoneProvider;
81 public void initialize() {
82 ZmMonitorConfig config = getConfigAs(ZmMonitorConfig.class);
83 monitorId = config.monitorId;
84 imageRefreshIntervalSeconds = config.imageRefreshInterval;
85 Integer value = config.alarmDuration;
86 alarmDuration = value != null ? value : DEFAULT_ALARM_DURATION_SECONDS;
87 bridgeHandler = (ZmBridgeHandler) getBridge().getHandler();
88 monitorStatusCache.clear();
89 updateStatus(ThingStatus.ONLINE);
90 startImageRefreshJob();
94 public void dispose() {
97 stopImageRefreshJob();
101 public void handleCommand(ChannelUID channelUID, Command command) {
102 if (command instanceof RefreshType) {
103 State state = monitorStatusCache.get(channelUID.getId());
105 updateState(channelUID, state);
109 logger.debug("Monitor {}: Received command '{}' for channel '{}'", monitorId, command, channelUID.getId());
110 ZmBridgeHandler localHandler = bridgeHandler;
111 if (localHandler == null) {
112 logger.warn("Monitor {}: Can't execute command because bridge handler is null", monitorId);
115 switch (channelUID.getId()) {
116 case CHANNEL_FUNCTION:
117 if (command instanceof StringType) {
119 MonitorFunction function = MonitorFunction.forValue(command.toString());
120 localHandler.setFunction(monitorId, function);
121 logger.debug("Monitor {}: Set monitor state to {}", monitorId, function);
122 } catch (IllegalArgumentException e) {
123 logger.debug("Monitor {}: Invalid function: {}", monitorId, command);
128 if (command instanceof OnOffType onOffCommand) {
129 localHandler.setEnabled(monitorId, onOffCommand);
130 logger.debug("Monitor {}: Set monitor enable to {}", monitorId, command);
133 case CHANNEL_TRIGGER_ALARM:
134 if (command instanceof OnOffType) {
135 logger.debug("Monitor {}: Set monitor alarm to {}", monitorId, command);
136 if (command == OnOffType.ON) {
137 localHandler.setAlarmOn(monitorId);
138 startAlarmOffJob(alarmDuration.intValue());
141 localHandler.setAlarmOff(monitorId);
149 public Collection<Class<? extends ThingHandlerService>> getServices() {
150 return Set.of(ZmActions.class);
153 public String getId() {
157 public void actionTriggerAlarm(@Nullable Number duration) {
158 if (duration == null) {
161 ZmBridgeHandler localHandler = bridgeHandler;
162 if (localHandler != null) {
163 logger.debug("Monitor {}: Action tell bridge to turn on alarm", monitorId);
164 localHandler.setAlarmOn(monitorId);
165 startAlarmOffJob(duration.intValue());
169 public void actionTriggerAlarm() {
170 actionTriggerAlarm(alarmDuration);
173 public void actionCancelAlarm() {
174 ZmBridgeHandler localHandler = bridgeHandler;
175 if (localHandler != null) {
176 logger.debug("Monitor {}: Action tell bridge to turn off alarm", monitorId);
178 localHandler.setAlarmOff(monitorId);
182 @SuppressWarnings("null")
183 public void updateStatus(Monitor m) {
184 logger.debug("Monitor {}: Updating Monitor: {}", m.getId(), m);
185 updateChannelState(CHANNEL_ID, new StringType(m.getId()));
186 updateChannelState(CHANNEL_NAME, new StringType(m.getName()));
187 updateChannelState(CHANNEL_FUNCTION, new StringType(m.getFunction()));
188 updateChannelState(CHANNEL_ENABLE, OnOffType.from(m.isEnabled()));
189 updateChannelState(CHANNEL_HOUR_EVENTS, new DecimalType(m.getHourEvents()));
190 updateChannelState(CHANNEL_DAY_EVENTS, new DecimalType(m.getDayEvents()));
191 updateChannelState(CHANNEL_WEEK_EVENTS, new DecimalType(m.getWeekEvents()));
192 updateChannelState(CHANNEL_MONTH_EVENTS, new DecimalType(m.getMonthEvents()));
193 updateChannelState(CHANNEL_TOTAL_EVENTS, new DecimalType(m.getTotalEvents()));
194 updateChannelState(CHANNEL_IMAGE_URL, new StringType(m.getImageUrl()));
195 updateChannelState(CHANNEL_VIDEO_URL, new StringType(m.getVideoUrl()));
196 updateChannelState(CHANNEL_ALARM, OnOffType.from(m.isAlarm()));
197 updateChannelState(CHANNEL_STATE, new StringType(m.getState().toString()));
199 updateChannelState(CHANNEL_TRIGGER_ALARM, OnOffType.from(m.isAlarm()));
201 Event event = m.getMostRecentCompletedEvent();
203 // No most recent event, so clear out the event channels
204 clearEventChannels();
207 // Update channels for most recent completed event
208 logger.debug("Monitor {}: Updating Event Id:{}, Name:{}, Frames:{}, AlarmFrames:{}, Length:{}", m.getId(),
209 event.getId(), event.getName(), event.getFrames(), event.getAlarmFrames(), event.getLength());
210 updateChannelState(CHANNEL_EVENT_ID, new StringType(event.getId()));
211 updateChannelState(CHANNEL_EVENT_NAME, new StringType(event.getName()));
212 updateChannelState(CHANNEL_EVENT_CAUSE, new StringType(event.getCause()));
213 updateChannelState(CHANNEL_EVENT_NOTES, new StringType(event.getNotes()));
214 updateChannelState(CHANNEL_EVENT_START, new DateTimeType(
215 ZonedDateTime.ofInstant(event.getStart().toInstant(), timeZoneProvider.getTimeZone())));
216 updateChannelState(CHANNEL_EVENT_END,
217 new DateTimeType(ZonedDateTime.ofInstant(event.getEnd().toInstant(), timeZoneProvider.getTimeZone())));
218 updateChannelState(CHANNEL_EVENT_FRAMES, new DecimalType(event.getFrames()));
219 updateChannelState(CHANNEL_EVENT_ALARM_FRAMES, new DecimalType(event.getAlarmFrames()));
220 updateChannelState(CHANNEL_EVENT_LENGTH, new QuantityType<Time>(event.getLength(), Units.SECOND));
223 private void clearEventChannels() {
224 updateChannelState(CHANNEL_EVENT_ID, UnDefType.NULL);
225 updateChannelState(CHANNEL_EVENT_NAME, UnDefType.NULL);
226 updateChannelState(CHANNEL_EVENT_CAUSE, UnDefType.NULL);
227 updateChannelState(CHANNEL_EVENT_NOTES, UnDefType.NULL);
228 updateChannelState(CHANNEL_EVENT_START, UnDefType.NULL);
229 updateChannelState(CHANNEL_EVENT_END, UnDefType.NULL);
230 updateChannelState(CHANNEL_EVENT_FRAMES, UnDefType.NULL);
231 updateChannelState(CHANNEL_EVENT_ALARM_FRAMES, UnDefType.NULL);
232 updateChannelState(CHANNEL_EVENT_LENGTH, UnDefType.NULL);
235 private void refreshImage() {
236 if (isLinked(CHANNEL_IMAGE)) {
239 logger.trace("Monitor {}: Can't update image because '{}' channel is not linked", CHANNEL_IMAGE, monitorId);
243 private void getImage() {
244 ZmBridgeHandler localHandler = bridgeHandler;
245 if (localHandler != null) {
246 logger.debug("Monitor {}: Updating image channel", monitorId);
247 RawType image = localHandler.getImage(monitorId, imageRefreshIntervalSeconds);
248 updateChannelState(CHANNEL_IMAGE, image != null ? image : UnDefType.UNDEF);
252 private void updateChannelState(String channelId, State state) {
253 updateState(channelId, state);
254 monitorStatusCache.put(channelId, state);
257 private void startImageRefreshJob() {
258 stopImageRefreshJob();
259 Integer interval = imageRefreshIntervalSeconds;
260 if (interval != null) {
261 long delay = getRandomDelay(interval);
262 imageRefreshJob = scheduler.scheduleWithFixedDelay(this::refreshImage, delay, interval, TimeUnit.SECONDS);
263 logger.debug("Monitor {}: Scheduled image refresh job will run every {} seconds starting in {} seconds",
264 monitorId, interval, delay);
268 private void stopImageRefreshJob() {
269 ScheduledFuture<?> localImageRefreshJob = imageRefreshJob;
270 if (localImageRefreshJob != null) {
271 logger.debug("Monitor {}: Canceled image refresh job", monitorId);
272 localImageRefreshJob.cancel(true);
273 imageRefreshJob = null;
277 private void turnAlarmOff() {
278 ZmBridgeHandler localHandler = bridgeHandler;
279 if (alarmOffJob != null && localHandler != null) {
280 logger.debug("Monitor {}: Tell bridge to turn off alarm", monitorId);
281 localHandler.setAlarmOff(monitorId);
285 private void startAlarmOffJob(int duration) {
288 alarmOffJob = scheduler.schedule(this::turnAlarmOff, duration, TimeUnit.SECONDS);
289 logger.debug("Monitor {}: Scheduled alarm off job in {} seconds", monitorId, duration);
293 private void stopAlarmOffJob() {
294 ScheduledFuture<?> localAlarmOffJob = alarmOffJob;
295 if (localAlarmOffJob != null) {
296 logger.debug("Monitor {}: Canceled alarm off job", monitorId);
297 localAlarmOffJob.cancel(true);
302 private long getRandomDelay(int interval) {
303 return System.currentTimeMillis() % interval;