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.satel.internal.handler;
15 import static org.openhab.binding.satel.internal.SatelBindingConstants.*;
17 import java.nio.charset.Charset;
18 import java.time.ZonedDateTime;
19 import java.util.Collection;
21 import java.util.Optional;
23 import java.util.concurrent.ConcurrentHashMap;
24 import java.util.concurrent.ScheduledFuture;
25 import java.util.concurrent.TimeUnit;
27 import org.eclipse.jdt.annotation.NonNullByDefault;
28 import org.eclipse.jdt.annotation.Nullable;
29 import org.openhab.binding.satel.internal.action.SatelEventLogActions;
30 import org.openhab.binding.satel.internal.command.ReadDeviceInfoCommand;
31 import org.openhab.binding.satel.internal.command.ReadDeviceInfoCommand.DeviceType;
32 import org.openhab.binding.satel.internal.command.ReadEventCommand;
33 import org.openhab.binding.satel.internal.command.ReadEventDescCommand;
34 import org.openhab.binding.satel.internal.event.ConnectionStatusEvent;
35 import org.openhab.binding.satel.internal.types.IntegraType;
36 import org.openhab.core.library.types.DateTimeType;
37 import org.openhab.core.library.types.DecimalType;
38 import org.openhab.core.library.types.StringType;
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.ThingTypeUID;
43 import org.openhab.core.thing.binding.ThingHandlerService;
44 import org.openhab.core.types.Command;
45 import org.slf4j.Logger;
46 import org.slf4j.LoggerFactory;
49 * The {@link SatelEventLogHandler} is responsible for handling commands, which are
50 * sent to one of the event log channels.
52 * @author Krzysztof Goworek - Initial contribution
55 public class SatelEventLogHandler extends SatelThingHandler {
57 public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES = Set.of(THING_TYPE_EVENTLOG);
59 private static final String NOT_AVAILABLE_TEXT = "N/A";
60 private static final String DETAILS_SEPARATOR = ", ";
61 private static final long CACHE_CLEAR_INTERVAL = TimeUnit.MINUTES.toMillis(30);
63 private final Logger logger = LoggerFactory.getLogger(SatelEventLogHandler.class);
64 private final Map<String, @Nullable EventDescriptionCacheEntry> eventDescriptions = new ConcurrentHashMap<>();
65 private final Map<String, @Nullable String> deviceNameCache = new ConcurrentHashMap<>();
66 private @Nullable ScheduledFuture<?> cacheExpirationJob;
67 private Charset encoding = Charset.defaultCharset();
70 * Represents single record of the event log.
72 * @author Krzysztof Goworek
75 public static class EventLogEntry {
77 private final int index;
78 private final int prevIndex;
79 private final ZonedDateTime timestamp;
80 private final String description;
81 private final String details;
83 private EventLogEntry(int index, int prevIndex, ZonedDateTime timestamp, String description, String details) {
85 this.prevIndex = prevIndex;
86 this.timestamp = timestamp;
87 this.description = description;
88 this.details = details;
92 * @return index of this record entry
94 public int getIndex() {
99 * @return index of the previous record entry in the log
101 public int getPrevIndex() {
106 * @return date and time when the event occurred
108 public ZonedDateTime getTimestamp() {
113 * @return description of the event
115 public String getDescription() {
120 * @return details about zones, partitions, users, etc
122 public String getDetails() {
127 public String toString() {
128 return "EventLogEntry [index=" + index + ", prevIndex=" + prevIndex + ", timestamp=" + timestamp
129 + ", description=" + description + ", details=" + details + "]";
133 public SatelEventLogHandler(Thing thing) {
138 public void initialize() {
141 withBridgeHandlerPresent(bridgeHandler -> {
142 this.encoding = bridgeHandler.getEncoding();
145 final ScheduledFuture<?> cacheExpirationJob = this.cacheExpirationJob;
146 if (cacheExpirationJob == null || cacheExpirationJob.isCancelled()) {
147 // for simplicity all cache entries are cleared every 30 minutes
148 this.cacheExpirationJob = scheduler.scheduleWithFixedDelay(deviceNameCache::clear, CACHE_CLEAR_INTERVAL,
149 CACHE_CLEAR_INTERVAL, TimeUnit.MILLISECONDS);
154 public void dispose() {
157 final ScheduledFuture<?> cacheExpirationJob = this.cacheExpirationJob;
158 if (cacheExpirationJob != null && !cacheExpirationJob.isCancelled()) {
159 cacheExpirationJob.cancel(true);
161 this.cacheExpirationJob = null;
165 public void handleCommand(ChannelUID channelUID, Command command) {
166 logger.debug("New command for {}: {}", channelUID, command);
168 if (CHANNEL_INDEX.equals(channelUID.getId()) && command instanceof DecimalType decimalCommand) {
169 int eventIndex = decimalCommand.intValue();
170 withBridgeHandlerPresent(bridgeHandler -> readEvent(eventIndex).ifPresent(entry -> {
172 updateState(CHANNEL_INDEX, new DecimalType(entry.getIndex()));
173 updateState(CHANNEL_PREV_INDEX, new DecimalType(entry.getPrevIndex()));
174 updateState(CHANNEL_TIMESTAMP, new DateTimeType(entry.getTimestamp()));
175 updateState(CHANNEL_DESCRIPTION, new StringType(entry.getDescription()));
176 updateState(CHANNEL_DETAILS, new StringType(entry.getDetails()));
182 public void incomingEvent(ConnectionStatusEvent event) {
183 logger.trace("Handling incoming event: {}", event);
184 // we have just connected, change thing's status
185 if (event.isConnected()) {
186 updateStatus(ThingStatus.ONLINE);
191 public Collection<Class<? extends ThingHandlerService>> getServices() {
192 return Set.of(SatelEventLogActions.class);
196 * Reads one record from the event log.
198 * @param eventIndex record index
199 * @return record data or {@linkplain Optional#empty()} if there is no record under given index
201 public Optional<EventLogEntry> readEvent(int eventIndex) {
202 return getEventDescription(eventIndex).flatMap(eventDesc -> {
203 ReadEventCommand readEventCmd = eventDesc.readEventCmd;
204 int currentIndex = readEventCmd.getCurrentIndex();
205 String eventText = eventDesc.getText();
206 boolean upperZone = getBridgeHandler().getIntegraType() == IntegraType.I256_PLUS
207 && readEventCmd.getUserControlNumber() > 0;
210 switch (eventDesc.getKind()) {
215 eventDetails = getDeviceDescription(DeviceType.PARTITION, readEventCmd.getPartition())
216 + DETAILS_SEPARATOR + getZoneExpanderKeypadDescription(readEventCmd.getSource(), upperZone);
219 eventDetails = getDeviceDescription(DeviceType.PARTITION, readEventCmd.getPartition())
220 + DETAILS_SEPARATOR + getUserDescription(readEventCmd.getSource());
223 eventDetails = getDeviceDescription(DeviceType.EXPANDER, readEventCmd.getPartitionKeypad())
224 + DETAILS_SEPARATOR + getUserDescription(readEventCmd.getSource());
227 eventDetails = getZoneExpanderKeypadDescription(readEventCmd.getSource(), upperZone);
230 eventDetails = getDeviceDescription(DeviceType.PARTITION, readEventCmd.getPartition());
233 eventDetails = getDeviceDescription(DeviceType.KEYPAD, readEventCmd.getPartition())
234 + DETAILS_SEPARATOR + getUserDescription(readEventCmd.getSource());
237 eventDetails = getUserDescription(readEventCmd.getSource());
240 eventDetails = getDeviceDescription(DeviceType.EXPANDER, readEventCmd.getSource());
243 eventDetails = getTelephoneDescription(readEventCmd.getSource());
246 eventDetails = getDeviceDescription(DeviceType.PARTITION, readEventCmd.getPartition())
247 + DETAILS_SEPARATOR + getDataBusDescription(readEventCmd.getSource());
250 if (readEventCmd.getSource() <= getBridgeHandler().getIntegraType().getOnMainboard()) {
251 eventDetails = getOutputExpanderDescription(readEventCmd.getSource(), upperZone);
253 eventDetails = getDeviceDescription(DeviceType.PARTITION, readEventCmd.getPartition())
254 + DETAILS_SEPARATOR + getOutputExpanderDescription(readEventCmd.getSource(), upperZone);
258 if (readEventCmd.getSource() <= 128) {
259 eventDetails = getOutputExpanderDescription(readEventCmd.getSource(), upperZone);
261 eventDetails = getDeviceDescription(DeviceType.PARTITION, readEventCmd.getPartition())
262 + DETAILS_SEPARATOR + getOutputExpanderDescription(readEventCmd.getSource(), upperZone);
266 eventDetails = getTelephoneDescription(readEventCmd.getPartition()) + DETAILS_SEPARATOR
267 + getUserDescription(readEventCmd.getSource());
270 eventDetails = getDeviceDescription(DeviceType.PARTITION, readEventCmd.getPartition())
271 + DETAILS_SEPARATOR + getDeviceDescription(DeviceType.TIMER, readEventCmd.getSource());
274 // this description consists of two records, so we must read additional record from the log
275 eventDetails = "." + readEventCmd.getSource() + "."
276 + (readEventCmd.getObject() * 32 + readEventCmd.getUserControlNumber());
277 Optional<EventDescription> eventDescNext = getEventDescription(readEventCmd.getNextIndex());
278 if (eventDescNext.isEmpty()) {
279 return Optional.empty();
281 final EventDescription eventDescNextItem = eventDescNext.get();
282 if (eventDescNextItem.getKind() != 30) {
283 logger.info("Unexpected event record kind {} at index {}", eventDescNextItem.getKind(),
284 readEventCmd.getNextIndex());
285 return Optional.empty();
287 readEventCmd = eventDescNextItem.readEventCmd;
288 eventText = eventDescNextItem.getText();
289 eventDetails = getDeviceDescription(DeviceType.KEYPAD, readEventCmd.getPartition())
290 + DETAILS_SEPARATOR + "ip: " + readEventCmd.getSource() + "."
291 + (readEventCmd.getObject() * 32 + readEventCmd.getUserControlNumber()) + eventDetails;
294 eventDetails = getDeviceDescription(DeviceType.PARTITION, readEventCmd.getPartition())
295 + DETAILS_SEPARATOR + getDeviceDescription(DeviceType.ZONE, readEventCmd.getSource());
298 logger.info("Unsupported device kind code {} at index {}", eventDesc.getKind(),
299 readEventCmd.getCurrentIndex());
300 eventDetails = String.join(DETAILS_SEPARATOR, "kind=" + eventDesc.getKind(),
301 "partition=" + readEventCmd.getPartition(), "source=" + readEventCmd.getSource(),
302 "object=" + readEventCmd.getObject(), "ucn=" + readEventCmd.getUserControlNumber());
305 return Optional.of(new EventLogEntry(currentIndex, readEventCmd.getNextIndex(),
306 readEventCmd.getTimestamp().atZone(getBridgeHandler().getZoneId()), eventText, eventDetails));
310 private Optional<EventDescription> getEventDescription(int eventIndex) {
311 ReadEventCommand readEventCmd = new ReadEventCommand(eventIndex);
312 if (!getBridgeHandler().sendCommand(readEventCmd, false)) {
313 logger.info("Unable to read event record for given index: {}", eventIndex);
314 return Optional.empty();
315 } else if (readEventCmd.isEmpty()) {
316 logger.info("No record under given index: {}", eventIndex);
317 return Optional.empty();
319 return Optional.of(readEventDescription(readEventCmd));
323 private static class EventDescriptionCacheEntry {
324 private final String eventText;
325 private final int descKind;
327 EventDescriptionCacheEntry(String eventText, int descKind) {
328 this.eventText = eventText;
329 this.descKind = descKind;
341 private static class EventDescription extends EventDescriptionCacheEntry {
342 private final ReadEventCommand readEventCmd;
344 EventDescription(ReadEventCommand readEventCmd, String eventText, int descKind) {
345 super(eventText, descKind);
346 this.readEventCmd = readEventCmd;
350 private EventDescription readEventDescription(ReadEventCommand readEventCmd) {
351 int eventCode = readEventCmd.getEventCode();
352 boolean restore = readEventCmd.isRestore();
353 String mapKey = String.format("%d_%b", eventCode, restore);
354 EventDescriptionCacheEntry mapValue = eventDescriptions.computeIfAbsent(mapKey, k -> {
355 ReadEventDescCommand cmd = new ReadEventDescCommand(eventCode, restore, true);
356 if (!getBridgeHandler().sendCommand(cmd, false)) {
357 logger.info("Unable to read event description: {}, {}", eventCode, restore);
360 return new EventDescriptionCacheEntry(cmd.getText(encoding), cmd.getKind());
362 if (mapValue == null) {
363 return new EventDescription(readEventCmd, NOT_AVAILABLE_TEXT, 0);
365 return new EventDescription(readEventCmd, mapValue.getText(), mapValue.getKind());
369 private String getOutputExpanderDescription(int deviceNumber, boolean upperOutput) {
370 if (deviceNumber == 0) {
372 } else if (deviceNumber <= 128) {
373 return getDeviceDescription(DeviceType.OUTPUT, upperOutput ? 128 + deviceNumber : deviceNumber);
374 } else if (deviceNumber <= 192) {
375 return getDeviceDescription(DeviceType.EXPANDER, deviceNumber);
377 return "invalid output|expander device: " + deviceNumber;
381 private String getZoneExpanderKeypadDescription(int deviceNumber, boolean upperZone) {
382 if (deviceNumber == 0) {
384 } else if (deviceNumber <= 128) {
385 return getDeviceDescription(DeviceType.ZONE, upperZone ? 128 + deviceNumber : deviceNumber);
386 } else if (deviceNumber <= 192) {
387 return getDeviceDescription(DeviceType.EXPANDER, deviceNumber);
389 return getDeviceDescription(DeviceType.KEYPAD, deviceNumber);
393 private String getUserDescription(int deviceNumber) {
394 return deviceNumber == 0 ? "user: unknown" : getDeviceDescription(DeviceType.USER, deviceNumber);
397 private String getDataBusDescription(int deviceNumber) {
398 return "data bus: " + deviceNumber;
401 private String getTelephoneDescription(int deviceNumber) {
402 return deviceNumber == 0 ? "telephone: unknown" : getDeviceDescription(DeviceType.TELEPHONE, deviceNumber);
405 private String getDeviceDescription(DeviceType deviceType, int deviceNumber) {
406 return String.format("%s: %s", deviceType.name().toLowerCase(), readDeviceName(deviceType, deviceNumber));
409 private String readDeviceName(DeviceType deviceType, int deviceNumber) {
410 String cacheKey = String.format("%s_%d", deviceType, deviceNumber);
411 String result = deviceNameCache.computeIfAbsent(cacheKey, k -> {
412 ReadDeviceInfoCommand cmd = new ReadDeviceInfoCommand(deviceType, deviceNumber);
413 if (!getBridgeHandler().sendCommand(cmd, false)) {
414 logger.info("Unable to read device info: {}, {}", deviceType, deviceNumber);
417 return cmd.getName(encoding);
419 return result == null ? NOT_AVAILABLE_TEXT : result;