diff --git a/bundles/org.openhab.binding.satel/src/main/java/org/openhab/binding/satel/internal/handler/SatelEventLogHandler.java b/bundles/org.openhab.binding.satel/src/main/java/org/openhab/binding/satel/internal/handler/SatelEventLogHandler.java index e72482d7040..56764a46872 100644 --- a/bundles/org.openhab.binding.satel/src/main/java/org/openhab/binding/satel/internal/handler/SatelEventLogHandler.java +++ b/bundles/org.openhab.binding.satel/src/main/java/org/openhab/binding/satel/internal/handler/SatelEventLogHandler.java @@ -14,25 +14,20 @@ package org.openhab.binding.satel.internal.handler; import static org.openhab.binding.satel.internal.SatelBindingConstants.*; -import java.nio.charset.Charset; import java.time.ZonedDateTime; import java.util.Collection; -import java.util.Map; import java.util.Optional; import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import org.openhab.binding.satel.internal.action.SatelEventLogActions; -import org.openhab.binding.satel.internal.command.ReadDeviceInfoCommand; -import org.openhab.binding.satel.internal.command.ReadDeviceInfoCommand.DeviceType; -import org.openhab.binding.satel.internal.command.ReadEventCommand; -import org.openhab.binding.satel.internal.command.ReadEventDescCommand; import org.openhab.binding.satel.internal.event.ConnectionStatusEvent; -import org.openhab.binding.satel.internal.types.IntegraType; +import org.openhab.binding.satel.internal.util.DeviceNameResolver; +import org.openhab.binding.satel.internal.util.EventLogReader; +import org.openhab.binding.satel.internal.util.EventLogReader.EventDescription; import org.openhab.core.library.types.DateTimeType; import org.openhab.core.library.types.DecimalType; import org.openhab.core.library.types.StringType; @@ -56,21 +51,16 @@ public class SatelEventLogHandler extends SatelThingHandler { public static final Set SUPPORTED_THING_TYPES = Set.of(THING_TYPE_EVENTLOG); - private static final String NOT_AVAILABLE_TEXT = "N/A"; - private static final String DETAILS_SEPARATOR = ", "; private static final long CACHE_CLEAR_INTERVAL = TimeUnit.MINUTES.toMillis(30); private final Logger logger = LoggerFactory.getLogger(SatelEventLogHandler.class); - private final Map eventDescriptions = new ConcurrentHashMap<>(); - private final Map deviceNameCache = new ConcurrentHashMap<>(); + private @Nullable EventLogReader eventLogReader; private @Nullable ScheduledFuture cacheExpirationJob; - private Charset encoding = Charset.defaultCharset(); /** * Represents single record of the event log. * * @author Krzysztof Goworek - * */ public static class EventLogEntry { @@ -138,14 +128,13 @@ public class SatelEventLogHandler extends SatelThingHandler { public void initialize() { super.initialize(); - withBridgeHandlerPresent(bridgeHandler -> { - this.encoding = bridgeHandler.getEncoding(); - }); + withBridgeHandlerPresent(bridgeHandler -> this.eventLogReader = new EventLogReader(bridgeHandler, + new DeviceNameResolver(bridgeHandler))); final ScheduledFuture cacheExpirationJob = this.cacheExpirationJob; if (cacheExpirationJob == null || cacheExpirationJob.isCancelled()) { // for simplicity all cache entries are cleared every 30 minutes - this.cacheExpirationJob = scheduler.scheduleWithFixedDelay(deviceNameCache::clear, CACHE_CLEAR_INTERVAL, + this.cacheExpirationJob = scheduler.scheduleWithFixedDelay(this::clearCache, CACHE_CLEAR_INTERVAL, CACHE_CLEAR_INTERVAL, TimeUnit.MILLISECONDS); } } @@ -159,6 +148,7 @@ public class SatelEventLogHandler extends SatelThingHandler { cacheExpirationJob.cancel(true); } this.cacheExpirationJob = null; + this.eventLogReader = null; } @Override @@ -199,223 +189,32 @@ public class SatelEventLogHandler extends SatelThingHandler { * @return record data or {@linkplain Optional#empty()} if there is no record under given index */ public Optional readEvent(int eventIndex) { - return getEventDescription(eventIndex).flatMap(eventDesc -> { - ReadEventCommand readEventCmd = eventDesc.readEventCmd; - int currentIndex = readEventCmd.getCurrentIndex(); - String eventText = eventDesc.getText(); - boolean upperZone = getBridgeHandler().getIntegraType() == IntegraType.I256_PLUS - && readEventCmd.getUserControlNumber() > 0; - String eventDetails; - - switch (eventDesc.getKind()) { - case 0: - eventDetails = ""; - break; - case 1: - eventDetails = getDeviceDescription(DeviceType.PARTITION, readEventCmd.getPartition()) - + DETAILS_SEPARATOR + getZoneExpanderKeypadDescription(readEventCmd.getSource(), upperZone); - break; - case 2: - eventDetails = getDeviceDescription(DeviceType.PARTITION, readEventCmd.getPartition()) - + DETAILS_SEPARATOR + getUserDescription(readEventCmd.getSource()); - break; - case 3: - eventDetails = getDeviceDescription(DeviceType.EXPANDER, readEventCmd.getPartitionKeypad()) - + DETAILS_SEPARATOR + getUserDescription(readEventCmd.getSource()); - break; - case 4: - eventDetails = getZoneExpanderKeypadDescription(readEventCmd.getSource(), upperZone); - break; - case 5: - eventDetails = getDeviceDescription(DeviceType.PARTITION, readEventCmd.getPartition()); - break; - case 6: - eventDetails = getDeviceDescription(DeviceType.KEYPAD, readEventCmd.getPartition()) - + DETAILS_SEPARATOR + getUserDescription(readEventCmd.getSource()); - break; - case 7: - eventDetails = getUserDescription(readEventCmd.getSource()); - break; - case 8: - eventDetails = getDeviceDescription(DeviceType.EXPANDER, readEventCmd.getSource()); - break; - case 9: - eventDetails = getTelephoneDescription(readEventCmd.getSource()); - break; - case 11: - eventDetails = getDeviceDescription(DeviceType.PARTITION, readEventCmd.getPartition()) - + DETAILS_SEPARATOR + getDataBusDescription(readEventCmd.getSource()); - break; - case 12: - if (readEventCmd.getSource() <= getBridgeHandler().getIntegraType().getOnMainboard()) { - eventDetails = getOutputExpanderDescription(readEventCmd.getSource(), upperZone); - } else { - eventDetails = getDeviceDescription(DeviceType.PARTITION, readEventCmd.getPartition()) - + DETAILS_SEPARATOR + getOutputExpanderDescription(readEventCmd.getSource(), upperZone); - } - break; - case 13: - if (readEventCmd.getSource() <= 128) { - eventDetails = getOutputExpanderDescription(readEventCmd.getSource(), upperZone); - } else { - eventDetails = getDeviceDescription(DeviceType.PARTITION, readEventCmd.getPartition()) - + DETAILS_SEPARATOR + getOutputExpanderDescription(readEventCmd.getSource(), upperZone); - } - break; - case 14: - eventDetails = getTelephoneDescription(readEventCmd.getPartition()) + DETAILS_SEPARATOR - + getUserDescription(readEventCmd.getSource()); - break; - case 15: - eventDetails = getDeviceDescription(DeviceType.PARTITION, readEventCmd.getPartition()) - + DETAILS_SEPARATOR + getDeviceDescription(DeviceType.TIMER, readEventCmd.getSource()); - break; - case 31: - // this description consists of two records, so we must read additional record from the log - eventDetails = "." + readEventCmd.getSource() + "." - + (readEventCmd.getObject() * 32 + readEventCmd.getUserControlNumber()); - Optional eventDescNext = getEventDescription(readEventCmd.getNextIndex()); - if (eventDescNext.isEmpty()) { - return Optional.empty(); - } - final EventDescription eventDescNextItem = eventDescNext.get(); - if (eventDescNextItem.getKind() != 30) { - logger.info("Unexpected event record kind {} at index {}", eventDescNextItem.getKind(), - readEventCmd.getNextIndex()); - return Optional.empty(); - } - readEventCmd = eventDescNextItem.readEventCmd; - eventText = eventDescNextItem.getText(); - eventDetails = getDeviceDescription(DeviceType.KEYPAD, readEventCmd.getPartition()) - + DETAILS_SEPARATOR + "ip: " + readEventCmd.getSource() + "." - + (readEventCmd.getObject() * 32 + readEventCmd.getUserControlNumber()) + eventDetails; - break; - case 32: - eventDetails = getDeviceDescription(DeviceType.PARTITION, readEventCmd.getPartition()) - + DETAILS_SEPARATOR + getDeviceDescription(DeviceType.ZONE, readEventCmd.getSource()); - break; - default: - logger.info("Unsupported device kind code {} at index {}", eventDesc.getKind(), - readEventCmd.getCurrentIndex()); - eventDetails = String.join(DETAILS_SEPARATOR, "kind=" + eventDesc.getKind(), - "partition=" + readEventCmd.getPartition(), "source=" + readEventCmd.getSource(), - "object=" + readEventCmd.getObject(), "ucn=" + readEventCmd.getUserControlNumber()); - } - - return Optional.of(new EventLogEntry(currentIndex, readEventCmd.getNextIndex(), - readEventCmd.getTimestamp().atZone(getBridgeHandler().getZoneId()), eventText, eventDetails)); - }); - } - - private Optional getEventDescription(int eventIndex) { - ReadEventCommand readEventCmd = new ReadEventCommand(eventIndex); - if (!getBridgeHandler().sendCommand(readEventCmd, false)) { - logger.info("Unable to read event record for given index: {}", eventIndex); + final EventLogReader eventLogReader = this.eventLogReader; + if (eventLogReader == null) { + logger.warn("Unable to read event: handler is not properly initialized"); return Optional.empty(); - } else if (readEventCmd.isEmpty()) { - logger.info("No record under given index: {}", eventIndex); - return Optional.empty(); - } else { - return Optional.of(readEventDescription(readEventCmd)); - } - } - - private static class EventDescriptionCacheEntry { - private final String eventText; - private final int descKind; - - EventDescriptionCacheEntry(String eventText, int descKind) { - this.eventText = eventText; - this.descKind = descKind; } - String getText() { - return eventText; + return readEvent(eventLogReader, eventIndex); + } + + private Optional readEvent(EventLogReader eventLogReader, int eventIndex) { + return eventLogReader.readEvent(eventIndex) + .map(eventDescription -> combineWithDetails(eventLogReader, eventDescription)); + } + + private EventLogEntry combineWithDetails(EventLogReader eventLogReader, EventDescription eventDescription) { + String eventDetails = eventLogReader.buildDetails(eventDescription); + + return new EventLogEntry(eventDescription.getCurrentIndex(), eventDescription.getNextIndex(), + eventDescription.getTimestamp().atZone(getBridgeHandler().getZoneId()), eventDescription.getText(), + eventDetails); + } + + private void clearCache() { + final EventLogReader eventLogReader = this.eventLogReader; + if (eventLogReader != null) { + eventLogReader.clearCache(); } - - int getKind() { - return descKind; - } - } - - private static class EventDescription extends EventDescriptionCacheEntry { - private final ReadEventCommand readEventCmd; - - EventDescription(ReadEventCommand readEventCmd, String eventText, int descKind) { - super(eventText, descKind); - this.readEventCmd = readEventCmd; - } - } - - private EventDescription readEventDescription(ReadEventCommand readEventCmd) { - int eventCode = readEventCmd.getEventCode(); - boolean restore = readEventCmd.isRestore(); - String mapKey = String.format("%d_%b", eventCode, restore); - EventDescriptionCacheEntry mapValue = eventDescriptions.computeIfAbsent(mapKey, k -> { - ReadEventDescCommand cmd = new ReadEventDescCommand(eventCode, restore, true); - if (!getBridgeHandler().sendCommand(cmd, false)) { - logger.info("Unable to read event description: {}, {}", eventCode, restore); - return null; - } - return new EventDescriptionCacheEntry(cmd.getText(encoding), cmd.getKind()); - }); - if (mapValue == null) { - return new EventDescription(readEventCmd, NOT_AVAILABLE_TEXT, 0); - } else { - return new EventDescription(readEventCmd, mapValue.getText(), mapValue.getKind()); - } - } - - private String getOutputExpanderDescription(int deviceNumber, boolean upperOutput) { - if (deviceNumber == 0) { - return "mainboard"; - } else if (deviceNumber <= 128) { - return getDeviceDescription(DeviceType.OUTPUT, upperOutput ? 128 + deviceNumber : deviceNumber); - } else if (deviceNumber <= 192) { - return getDeviceDescription(DeviceType.EXPANDER, deviceNumber); - } else { - return "invalid output|expander device: " + deviceNumber; - } - } - - private String getZoneExpanderKeypadDescription(int deviceNumber, boolean upperZone) { - if (deviceNumber == 0) { - return "mainboard"; - } else if (deviceNumber <= 128) { - return getDeviceDescription(DeviceType.ZONE, upperZone ? 128 + deviceNumber : deviceNumber); - } else if (deviceNumber <= 192) { - return getDeviceDescription(DeviceType.EXPANDER, deviceNumber); - } else { - return getDeviceDescription(DeviceType.KEYPAD, deviceNumber); - } - } - - private String getUserDescription(int deviceNumber) { - return deviceNumber == 0 ? "user: unknown" : getDeviceDescription(DeviceType.USER, deviceNumber); - } - - private String getDataBusDescription(int deviceNumber) { - return "data bus: " + deviceNumber; - } - - private String getTelephoneDescription(int deviceNumber) { - return deviceNumber == 0 ? "telephone: unknown" : getDeviceDescription(DeviceType.TELEPHONE, deviceNumber); - } - - private String getDeviceDescription(DeviceType deviceType, int deviceNumber) { - return String.format("%s: %s", deviceType.name().toLowerCase(), readDeviceName(deviceType, deviceNumber)); - } - - private String readDeviceName(DeviceType deviceType, int deviceNumber) { - String cacheKey = String.format("%s_%d", deviceType, deviceNumber); - String result = deviceNameCache.computeIfAbsent(cacheKey, k -> { - ReadDeviceInfoCommand cmd = new ReadDeviceInfoCommand(deviceType, deviceNumber); - if (!getBridgeHandler().sendCommand(cmd, false)) { - logger.info("Unable to read device info: {}, {}", deviceType, deviceNumber); - return null; - } - return cmd.getName(encoding); - }); - return result == null ? NOT_AVAILABLE_TEXT : result; } } diff --git a/bundles/org.openhab.binding.satel/src/main/java/org/openhab/binding/satel/internal/util/DeviceNameResolver.java b/bundles/org.openhab.binding.satel/src/main/java/org/openhab/binding/satel/internal/util/DeviceNameResolver.java new file mode 100644 index 00000000000..6071072b8bf --- /dev/null +++ b/bundles/org.openhab.binding.satel/src/main/java/org/openhab/binding/satel/internal/util/DeviceNameResolver.java @@ -0,0 +1,172 @@ +/* + * Copyright (c) 2010-2025 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.satel.internal.util; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.satel.internal.command.ReadDeviceInfoCommand; +import org.openhab.binding.satel.internal.command.ReadDeviceInfoCommand.DeviceType; +import org.openhab.binding.satel.internal.handler.SatelBridgeHandler; +import org.openhab.binding.satel.internal.handler.SatelEventLogHandler; +import org.openhab.binding.satel.internal.types.IntegraType; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Helper class for getting device names from the alarm system. + * Used for friendly descriptions in event log. Names are cached to speed up repeating requests. + * + * @author Krzysztof Goworek - Initial contribution + * @see SatelEventLogHandler + */ +@NonNullByDefault +public class DeviceNameResolver { + + private final Logger logger = LoggerFactory.getLogger(getClass()); + private final Map nameCache = new ConcurrentHashMap<>(); + private final SatelBridgeHandler bridgeHandler; + + public DeviceNameResolver(SatelBridgeHandler bridgeHandler) { + this.bridgeHandler = bridgeHandler; + } + + /** + * Clears all cached names. + */ + public void clearCache() { + nameCache.clear(); + } + + /** + * Returns name of the device with given type and number. + * + * @param deviceType device type + * @param deviceNumber device number + * @return device name + */ + public String resolve(DeviceType deviceType, int deviceNumber) { + return String.format("%s: %s", deviceType.name().toLowerCase(), readDeviceName(deviceType, deviceNumber)); + } + + /** + * Returns name of output or expander with given number. + * + * @param deviceNumber device number + * @param upperOutput if {@code true} it is upper half of outputs + * @return name of output or expander, depending on device number + */ + public String resolveOutputExpander(int deviceNumber, boolean upperOutput) { + if (deviceNumber == 0) { + return "mainboard"; + } else if (deviceNumber <= 128) { + return resolve(DeviceType.OUTPUT, upperOutput ? 128 + deviceNumber : deviceNumber); + } else if (deviceNumber <= 192) { + return resolve(DeviceType.EXPANDER, deviceNumber); + } else { + return "invalid output|expander device: " + deviceNumber; + } + } + + /** + * Returns name of zone or expander or keypad with given number. + * + * @param deviceNumber device number + * @param upperZone if {@code true} it is upper half of zones + * @return name of zone or expander or keypad, depending on device number + */ + public String resolveZoneExpanderKeypad(int deviceNumber, boolean upperZone) { + if (deviceNumber == 0) { + return "mainboard"; + } else if (deviceNumber <= 128) { + return resolve(DeviceType.ZONE, upperZone ? 128 + deviceNumber : deviceNumber); + } else if (deviceNumber <= 192) { + return resolve(DeviceType.EXPANDER, deviceNumber); + } else { + return resolve(DeviceType.KEYPAD, deviceNumber); + } + } + + /** + * Returns name of partition keypad with given number. + * + * @param deviceNumber device number + * @return name of partition keypad + */ + public String resolvePartitionKeypad(int deviceNumber) { + boolean wrlBoard = bridgeHandler.getIntegraType() == IntegraType.I128_LEON + || bridgeHandler.getIntegraType() == IntegraType.I128_SIM300; + // Integra 128-WRL has only one expander bus exposed on the mainboard, it can only have 32 expanders + // On the second bus it has connected embedded ABAX and GSM modules + if (wrlBoard && deviceNumber > 32) { + return "mainboard"; + } else { + return resolve(DeviceType.EXPANDER, deviceNumber); + } + } + + /** + * Returns name of user with given number. + * + * @param deviceNumber device number + * @return name of user + */ + public String resolveUser(int deviceNumber) { + return switch (deviceNumber) { + case 0 -> "user: unknown"; + case 249 -> "INT-AV"; + case 250 -> "ACCO NET"; + case 251 -> "SMS"; + case 252 -> "timer"; + case 253 -> "function zone"; + case 254 -> "Quick arm"; + case 255 -> "service"; + default -> resolve(DeviceType.USER, deviceNumber); + }; + } + + /** + * Returns name of data bus given number. + * + * @param deviceNumber device number + * @return name of data bus + */ + public String resolveDataBus(int deviceNumber) { + return "data bus: " + deviceNumber; + } + + /** + * Returns name of telephone with given number. + * + * @param deviceNumber device number + * @return name of telephone + */ + public String resolveTelephone(int deviceNumber) { + return deviceNumber == 0 ? "telephone: unknown" : resolve(DeviceType.TELEPHONE, deviceNumber); + } + + private String readDeviceName(DeviceType deviceType, int deviceNumber) { + String cacheKey = String.format("%s_%d", deviceType, deviceNumber); + String result = nameCache.computeIfAbsent(cacheKey, k -> { + ReadDeviceInfoCommand cmd = new ReadDeviceInfoCommand(deviceType, deviceNumber); + if (!bridgeHandler.sendCommand(cmd, false)) { + logger.warn("Unable to read device info: {}, {}", deviceType, deviceNumber); + return null; + } + return cmd.getName(bridgeHandler.getEncoding()); + }); + return result == null ? Integer.toString(deviceNumber) : result; + } +} diff --git a/bundles/org.openhab.binding.satel/src/main/java/org/openhab/binding/satel/internal/util/EventLogReader.java b/bundles/org.openhab.binding.satel/src/main/java/org/openhab/binding/satel/internal/util/EventLogReader.java new file mode 100644 index 00000000000..909acdb89a4 --- /dev/null +++ b/bundles/org.openhab.binding.satel/src/main/java/org/openhab/binding/satel/internal/util/EventLogReader.java @@ -0,0 +1,260 @@ +/* + * Copyright (c) 2010-2025 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.satel.internal.util; + +import java.time.LocalDateTime; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.satel.internal.command.ReadDeviceInfoCommand.DeviceType; +import org.openhab.binding.satel.internal.command.ReadEventCommand; +import org.openhab.binding.satel.internal.command.ReadEventDescCommand; +import org.openhab.binding.satel.internal.handler.SatelBridgeHandler; +import org.openhab.binding.satel.internal.types.IntegraType; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * This is a helper class for reading event log records. + * + * @author Krzysztof Goworek - Initial contribution + */ +@NonNullByDefault +public class EventLogReader { + + private static final String DETAILS_SEPARATOR = ", "; + + private final Logger logger = LoggerFactory.getLogger(getClass()); + private final Map eventDescriptions = new ConcurrentHashMap<>(); + private final SatelBridgeHandler bridgeHandler; + private final DeviceNameResolver deviceNameResolver; + + public EventLogReader(SatelBridgeHandler bridgeHandler, DeviceNameResolver deviceNameResolver) { + this.bridgeHandler = bridgeHandler; + this.deviceNameResolver = deviceNameResolver; + } + + /** + * Reads one record from the event log. + * + * @param eventIndex record index + * @return record description wrapped in {@linkplain Optional} or {@linkplain Optional#empty()} if there is no + * record under given index or read command failed + */ + public Optional readEvent(int eventIndex) { + ReadEventCommand readEventCmd = new ReadEventCommand(eventIndex); + if (!bridgeHandler.sendCommand(readEventCmd, false)) { + logger.warn("Unable to read event record for given index: {}", eventIndex); + return Optional.empty(); + } else if (readEventCmd.isEmpty()) { + logger.warn("No record under given index: {}", eventIndex); + return Optional.empty(); + } else { + return Optional.of(readEventDescription(readEventCmd)); + } + } + + /** + * Builds detailed text description for given event record. + * + * @param eventDescription event record + * @return string with event details + */ + public String buildDetails(EventDescription eventDescription) { + String eventDetails = getDetails(eventDescription); + if (eventDescription.isMultipartEvent()) { + // this description consists of two records, so we must read additional record from the log + eventDetails = readSecondPartOfDetails(eventDescription).orElse("") + eventDetails; + } + return eventDetails; + } + + /** + * Removes all device names from the cache. + */ + public void clearCache() { + deviceNameResolver.clearCache(); + } + + private static class EventDescriptionCacheEntry { + private final String eventText; + private final int descKind; + + private EventDescriptionCacheEntry(String eventText, int descKind) { + this.eventText = eventText; + this.descKind = descKind; + } + + public String getText() { + return eventText; + } + + int getKind() { + return descKind; + } + } + + /** + * Contains decoded data of an event record. + */ + public class EventDescription extends EventDescriptionCacheEntry { + private final int currentIndex; + private int nextIndex; + private final int userControlNumber; + private final int partition; + private final int partitionKeypad; + private final int source; + private final int object; + private final LocalDateTime timestamp; + + EventDescription(ReadEventCommand readEventCmd, String eventText, int descKind) { + super(eventText, descKind); + this.currentIndex = readEventCmd.getCurrentIndex(); + this.nextIndex = readEventCmd.getNextIndex(); + this.userControlNumber = readEventCmd.getUserControlNumber(); + this.partition = readEventCmd.getPartition(); + this.partitionKeypad = readEventCmd.getPartitionKeypad(); + this.source = readEventCmd.getSource(); + this.object = readEventCmd.getObject(); + this.timestamp = readEventCmd.getTimestamp(); + } + + public int getCurrentIndex() { + return currentIndex; + } + + public int getNextIndex() { + return nextIndex; + } + + public LocalDateTime getTimestamp() { + return timestamp; + } + + private void setNextIndex(int nextIndex) { + this.nextIndex = nextIndex; + } + + private int getUserControlNumber() { + return userControlNumber; + } + + private int getPartition() { + return partition; + } + + private int getPartitionKeypad() { + return partitionKeypad; + } + + private int getSource() { + return source; + } + + private int getObject() { + return object; + } + + private boolean isMultipartEvent() { + return getKind() == 31; + } + + private boolean isSecondPart() { + if (getKind() != 30) { + logger.warn("Unexpected event record kind {} at index {}", getKind(), getCurrentIndex()); + return false; + } + return true; + } + } + + private EventDescription readEventDescription(ReadEventCommand readEventCmd) { + int eventCode = readEventCmd.getEventCode(); + boolean restore = readEventCmd.isRestore(); + String mapKey = String.format("%d_%b", eventCode, restore); + EventDescriptionCacheEntry mapValue = eventDescriptions.computeIfAbsent(mapKey, k -> { + ReadEventDescCommand cmd = new ReadEventDescCommand(eventCode, restore, true); + if (!bridgeHandler.sendCommand(cmd, false)) { + logger.warn("Unable to read event description: {}, {}", eventCode, restore); + return null; + } + return new EventDescriptionCacheEntry(cmd.getText(bridgeHandler.getEncoding()), cmd.getKind()); + }); + if (mapValue == null) { + String eventText = String.format("event #%d%s", eventCode, restore ? " (restore)" : ""); + return new EventDescription(readEventCmd, eventText, 0); + } else { + return new EventDescription(readEventCmd, mapValue.getText(), mapValue.getKind()); + } + } + + private String getDetails(EventDescription eventDesc) { + boolean upperZone = bridgeHandler.getIntegraType() == IntegraType.I256_PLUS + && eventDesc.getUserControlNumber() > 0; + + return switch (eventDesc.getKind()) { + case 0 -> ""; + case 1 -> deviceNameResolver.resolve(DeviceType.PARTITION, eventDesc.getPartition()) + DETAILS_SEPARATOR + + deviceNameResolver.resolveZoneExpanderKeypad(eventDesc.getSource(), upperZone); + case 2 -> deviceNameResolver.resolve(DeviceType.PARTITION, eventDesc.getPartition()) + DETAILS_SEPARATOR + + deviceNameResolver.resolveUser(eventDesc.getSource()); + case 3 -> deviceNameResolver.resolvePartitionKeypad(eventDesc.getPartitionKeypad()) + DETAILS_SEPARATOR + + deviceNameResolver.resolveUser(eventDesc.getSource()); + case 4 -> deviceNameResolver.resolveZoneExpanderKeypad(eventDesc.getSource(), upperZone); + case 5 -> deviceNameResolver.resolve(DeviceType.PARTITION, eventDesc.getPartition()); + case 6 -> deviceNameResolver.resolve(DeviceType.KEYPAD, eventDesc.getPartition()) + DETAILS_SEPARATOR + + deviceNameResolver.resolveUser(eventDesc.getSource()); + case 7 -> deviceNameResolver.resolveUser(eventDesc.getSource()); + case 8 -> deviceNameResolver.resolve(DeviceType.EXPANDER, eventDesc.getSource()); + case 9 -> deviceNameResolver.resolveTelephone(eventDesc.getSource()); + case 11 -> deviceNameResolver.resolve(DeviceType.PARTITION, eventDesc.getPartition()) + DETAILS_SEPARATOR + + deviceNameResolver.resolveDataBus(eventDesc.getSource()); + case 12 -> (eventDesc.getSource() <= bridgeHandler.getIntegraType().getOnMainboard()) + ? deviceNameResolver.resolveOutputExpander(eventDesc.getSource(), upperZone) + : deviceNameResolver.resolve(DeviceType.PARTITION, eventDesc.getPartition()) + DETAILS_SEPARATOR + + deviceNameResolver.resolveOutputExpander(eventDesc.getSource(), upperZone); + case 13 -> (eventDesc.getSource() <= 128) + ? deviceNameResolver.resolveOutputExpander(eventDesc.getSource(), upperZone) + : deviceNameResolver.resolve(DeviceType.PARTITION, eventDesc.getPartition()) + DETAILS_SEPARATOR + + deviceNameResolver.resolveOutputExpander(eventDesc.getSource(), upperZone); + case 14 -> deviceNameResolver.resolveTelephone(eventDesc.getPartition()) + DETAILS_SEPARATOR + + deviceNameResolver.resolveUser(eventDesc.getSource()); + case 15 -> deviceNameResolver.resolve(DeviceType.PARTITION, eventDesc.getPartition()) + DETAILS_SEPARATOR + + deviceNameResolver.resolve(DeviceType.TIMER, eventDesc.getSource()); + case 30 -> + deviceNameResolver.resolve(DeviceType.KEYPAD, eventDesc.getPartition()) + DETAILS_SEPARATOR + "ip: " + + eventDesc.getSource() + "." + (eventDesc.getObject() * 32 + eventDesc.getUserControlNumber()); + case 31 -> + "." + eventDesc.getSource() + "." + (eventDesc.getObject() * 32 + eventDesc.getUserControlNumber()); + case 32 -> deviceNameResolver.resolve(DeviceType.PARTITION, eventDesc.getPartition()) + DETAILS_SEPARATOR + + deviceNameResolver.resolve(DeviceType.ZONE, eventDesc.getSource()); + default -> { + logger.warn("Unsupported device kind code {} at index {}", eventDesc.getKind(), + eventDesc.getCurrentIndex()); + yield String.join(DETAILS_SEPARATOR, "kind=" + eventDesc.getKind(), + "partition=" + eventDesc.getPartition(), "source=" + eventDesc.getSource(), + "object=" + eventDesc.getObject(), "ucn=" + eventDesc.getUserControlNumber()); + } + }; + } + + private Optional readSecondPartOfDetails(EventDescription eventDesc) { + return readEvent(eventDesc.getNextIndex()).filter(EventDescription::isSecondPart).map(descNext -> { + eventDesc.setNextIndex(descNext.getNextIndex()); + return getDetails(descNext); + }); + } +} diff --git a/bundles/org.openhab.binding.satel/src/test/java/org/openhab/binding/satel/internal/handler/SatelEventLogHandlerTest.java b/bundles/org.openhab.binding.satel/src/test/java/org/openhab/binding/satel/internal/handler/SatelEventLogHandlerTest.java new file mode 100644 index 00000000000..6ed8454cc92 --- /dev/null +++ b/bundles/org.openhab.binding.satel/src/test/java/org/openhab/binding/satel/internal/handler/SatelEventLogHandlerTest.java @@ -0,0 +1,152 @@ +/* + * Copyright (c) 2010-2025 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.satel.internal.handler; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.*; +import static org.openhab.binding.satel.internal.SatelBindingConstants.*; + +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.util.Collection; +import java.util.Optional; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.openhab.binding.satel.internal.action.SatelEventLogActions; +import org.openhab.binding.satel.internal.event.ConnectionStatusEvent; +import org.openhab.binding.satel.internal.util.EventLogReader; +import org.openhab.binding.satel.internal.util.EventLogReader.EventDescription; +import org.openhab.core.library.types.DateTimeType; +import org.openhab.core.library.types.DecimalType; +import org.openhab.core.library.types.StringType; +import org.openhab.core.thing.ChannelUID; +import org.openhab.core.thing.Thing; +import org.openhab.core.thing.ThingStatus; +import org.openhab.core.thing.ThingStatusInfo; +import org.openhab.core.thing.binding.ThingHandlerCallback; +import org.openhab.core.thing.binding.ThingHandlerService; +import org.openhab.core.thing.internal.ThingImpl; + +/** + * @author Krzysztof Goworek - Initial contribution + */ +@ExtendWith(MockitoExtension.class) +class SatelEventLogHandlerTest { + + @Mock + private ThingHandlerCallback callback; + + @Mock + private SatelBridgeHandler bridgeHandler; + + @Mock + private EventLogReader eventLogReader; + + private final Thing thing = new ThingImpl(THING_TYPE_EVENTLOG, "thingId"); + + @InjectMocks + private final SatelEventLogHandler testSubject = new SatelEventLogHandler(thing); + + @Test + void handleCommandShouldNotUpdateStateWhenOtherChannelIsGiven() { + testSubject.handleCommand(new ChannelUID(thing.getUID(), CHANNEL_DESCRIPTION), new DecimalType(0)); + + verify(eventLogReader, never()).readEvent(0); + verify(callback, never()).stateUpdated(any(), any()); + } + + @Test + void handleCommandShouldNotUpdateStateWhenOtherCommandIsGiven() { + testSubject.handleCommand(new ChannelUID(thing.getUID(), CHANNEL_INDEX), new StringType("")); + + verify(eventLogReader, never()).readEvent(0); + verify(callback, never()).stateUpdated(any(), any()); + } + + @Test + void handleCommandShouldNotUpdateStateWhenEventLogReaderIsNotPresent() { + testSubject.dispose(); + + testSubject.handleCommand(new ChannelUID(thing.getUID(), CHANNEL_INDEX), new DecimalType(0)); + + verify(eventLogReader, never()).readEvent(0); + verify(callback, never()).stateUpdated(any(), any()); + } + + @Test + void handleCommandShouldNotUpdateStateWhenReadEventFailed() { + testSubject.handleCommand(new ChannelUID(thing.getUID(), CHANNEL_INDEX), new DecimalType(0)); + + verify(eventLogReader).readEvent(0); + verify(callback, never()).stateUpdated(any(), any()); + } + + @Test + void handleCommandShouldUpdateStateWhenSendCommandSucceeded() { + LocalDateTime timestamp = LocalDateTime.parse("2020-03-12T12:34:56"); + EventDescription eventDescription = mock(EventDescription.class); + when(eventDescription.getCurrentIndex()).thenReturn(1); + when(eventDescription.getNextIndex()).thenReturn(2); + when(eventDescription.getTimestamp()).thenReturn(timestamp); + when(eventDescription.getText()).thenReturn("description"); + when(eventLogReader.readEvent(0)).thenReturn(Optional.of(eventDescription)); + when(eventLogReader.buildDetails(same(eventDescription))).thenReturn("details"); + when(bridgeHandler.getZoneId()).thenReturn(ZoneId.systemDefault()); + + testSubject.handleCommand(new ChannelUID(thing.getUID(), CHANNEL_INDEX), new DecimalType(0)); + + verify(callback).stateUpdated(new ChannelUID(thing.getUID(), CHANNEL_INDEX), new DecimalType(1)); + verify(callback).stateUpdated(new ChannelUID(thing.getUID(), CHANNEL_PREV_INDEX), new DecimalType(2)); + verify(callback).stateUpdated(new ChannelUID(thing.getUID(), CHANNEL_TIMESTAMP), + new DateTimeType(timestamp.atZone(ZoneId.systemDefault()))); + verify(callback).stateUpdated(new ChannelUID(thing.getUID(), CHANNEL_DESCRIPTION), + new StringType("description")); + verify(callback).stateUpdated(new ChannelUID(thing.getUID(), CHANNEL_DETAILS), new StringType("details")); + } + + @Test + void incomingEventShouldUpdateStatusIfConnected() { + testSubject.incomingEvent(new ConnectionStatusEvent(true)); + + ArgumentCaptor statusCaptor = ArgumentCaptor.forClass(ThingStatusInfo.class); + verify(callback).statusUpdated(eq(thing), statusCaptor.capture()); + assertEquals(ThingStatus.ONLINE, statusCaptor.getValue().getStatus()); + } + + @Test + void incomingEventShouldNotUpdateStatusIfNotConnected() { + testSubject.incomingEvent(new ConnectionStatusEvent(false)); + + verify(callback, never()).statusUpdated(eq(thing), any()); + } + + @Test + void getServicesShouldReturnEventLogActions() { + Collection> result = testSubject.getServices(); + + assertEquals(1, result.size()); + assertTrue(result.contains(SatelEventLogActions.class)); + } + + @Test + void readEvent() { + } +} diff --git a/bundles/org.openhab.binding.satel/src/test/java/org/openhab/binding/satel/internal/util/DeviceNameResolverTest.java b/bundles/org.openhab.binding.satel/src/test/java/org/openhab/binding/satel/internal/util/DeviceNameResolverTest.java new file mode 100644 index 00000000000..f463b2ad259 --- /dev/null +++ b/bundles/org.openhab.binding.satel/src/test/java/org/openhab/binding/satel/internal/util/DeviceNameResolverTest.java @@ -0,0 +1,254 @@ +/* + * Copyright (c) 2010-2025 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.satel.internal.util; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.*; + +import java.nio.charset.StandardCharsets; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.openhab.binding.satel.internal.command.ReadDeviceInfoCommand; +import org.openhab.binding.satel.internal.command.ReadDeviceInfoCommand.DeviceType; +import org.openhab.binding.satel.internal.event.EventDispatcher; +import org.openhab.binding.satel.internal.handler.SatelBridgeHandler; +import org.openhab.binding.satel.internal.protocol.SatelMessage; +import org.openhab.binding.satel.internal.types.IntegraType; + +/** + * @author Krzysztof Goworek - Initial contribution + */ +class DeviceNameResolverTest { + + private final SatelBridgeHandler bridgeHandler = mock(SatelBridgeHandler.class); + + private final EventDispatcher eventDispatcher = mock(EventDispatcher.class); + + private final DeviceNameResolver testSubject = new DeviceNameResolver(bridgeHandler); + + @BeforeEach + void setUpBridgeHandler() { + when(bridgeHandler.getEncoding()).thenReturn(StandardCharsets.US_ASCII); + } + + @Test + void resolveShouldReadDeviceName() { + setUpResponse("partition name"); + + String result = testSubject.resolve(DeviceType.PARTITION, 1); + assertEquals("partition: partition name", result); + } + + @Test + void resolveShouldCacheDevice() { + setUpResponse("partition name"); + testSubject.resolve(DeviceType.PARTITION, 1); + + String result = testSubject.resolve(DeviceType.PARTITION, 1); + + assertEquals("partition: partition name", result); + verify(bridgeHandler, times(1)).sendCommand(any(), eq(false)); + } + + @Test + void resolveShouldReturnDeviceNumberWhenNameNotAvailable() { + when(bridgeHandler.sendCommand(any(), eq(false))).thenReturn(false); + + String result = testSubject.resolve(DeviceType.PARTITION, 1); + + assertEquals("partition: 1", result); + } + + @Test + void clearCacheShouldRemoveCachedName() { + setUpResponse("partition name"); + + testSubject.resolve(DeviceType.PARTITION, 1); + testSubject.clearCache(); + testSubject.resolve(DeviceType.PARTITION, 1); + + verify(bridgeHandler, times(2)).sendCommand(any(), eq(false)); + } + + @Test + void resolveOutputExpanderShouldReturnMainboardWhenDeviceNumberIsZero() { + assertEquals("mainboard", testSubject.resolveOutputExpander(0, false)); + } + + @Test + void resolveOutputExpanderShouldReturnOutputName() { + setUpResponse("output name"); + + String result = testSubject.resolveOutputExpander(1, false); + + assertEquals("output: output name", result); + } + + @Test + void resolveOutputExpanderShouldReturnResolveUpperOutputName() { + when(bridgeHandler.sendCommand(any(), eq(false))).thenReturn(false); + + String result = testSubject.resolveOutputExpander(1, true); + + assertEquals("output: 129", result); + } + + @Test + void resolveOutputExpanderShouldReturnExpanderName() { + setUpResponse("expander name"); + + String result = testSubject.resolveOutputExpander(129, false); + + assertEquals("expander: expander name", result); + } + + @Test + void resolveOutputExpanderShouldHandleInvalidDeviceNumber() { + String result = testSubject.resolveOutputExpander(193, false); + + assertEquals("invalid output|expander device: 193", result); + } + + @Test + void resolveZoneExpanderKeypadShouldReturnMainboardWhenDeviceNumberIsZero() { + assertEquals("mainboard", testSubject.resolveZoneExpanderKeypad(0, false)); + } + + @Test + void resolveZoneExpanderKeypadShouldReturnZoneName() { + setUpResponse("zone name"); + + String result = testSubject.resolveZoneExpanderKeypad(1, false); + + assertEquals("zone: zone name", result); + } + + @Test + void resolveZoneExpanderKeypadShouldReturnUpperZoneName() { + when(bridgeHandler.sendCommand(any(), eq(false))).thenReturn(false); + + String result = testSubject.resolveZoneExpanderKeypad(1, true); + + assertEquals("zone: 129", result); + } + + @Test + void resolveZoneExpanderKeypadShouldReturnExpanderName() { + setUpResponse("expander name"); + + String result = testSubject.resolveZoneExpanderKeypad(129, false); + + assertEquals("expander: expander name", result); + } + + @Test + void resolveZoneExpanderKeypadShouldReturnKeypadName() { + setUpResponse("keypad name"); + + String result = testSubject.resolveZoneExpanderKeypad(193, false); + + assertEquals("keypad: keypad name", result); + } + + @Test + void resolvePartitionKeypadShouldReturnPartitionKeypadNameForSecondBus() { + setUpResponse("keypad name"); + + String result = testSubject.resolvePartitionKeypad(33); + + assertEquals("expander: keypad name", result); + } + + @Test + void resolvePartitionKeypadShouldReturnPartitionKeypadNameForWrlLeonBoard() { + when(bridgeHandler.getIntegraType()).thenReturn(IntegraType.I128_LEON); + setUpResponse("keypad name"); + + String result = testSubject.resolvePartitionKeypad(1); + + assertEquals("expander: keypad name", result); + } + + @Test + void resolvePartitionKeypadShouldReturnMainboardForWrlLeonBoard() { + when(bridgeHandler.getIntegraType()).thenReturn(IntegraType.I128_LEON); + + String result = testSubject.resolvePartitionKeypad(33); + + assertEquals("mainboard", result); + } + + @Test + void resolvePartitionKeypadShouldReturnMainboardForWrlSim300Board() { + when(bridgeHandler.getIntegraType()).thenReturn(IntegraType.I128_SIM300); + + String result = testSubject.resolvePartitionKeypad(33); + + assertEquals("mainboard", result); + } + + @Test + void resolveUserShouldReturnUserName() { + setUpResponse("user name"); + + String result = testSubject.resolveUser(1); + + assertEquals("user: user name", result); + } + + @ParameterizedTest + @CsvSource({ "0,user: unknown", "249,INT-AV", "250,ACCO NET", "251,SMS", "252,timer", "253,function zone", + "254,Quick arm", "255,service" }) + void resolveUserShouldReturnStaticNameForSpecificDeviceNumber(int deviceNumber, String expectedResult) { + String result = testSubject.resolveUser(deviceNumber); + + assertEquals(expectedResult, result); + } + + @Test + void resolveDataBusShouldReturnDataBusName() { + assertEquals("data bus: 1", testSubject.resolveDataBus(1)); + } + + @Test + void resolveTelephoneShouldReturnTelephoneName() { + setUpResponse("telephone name"); + + String result = testSubject.resolveTelephone(1); + + assertEquals("telephone: telephone name", result); + } + + @Test + void resolveTelephoneShouldReturnUnknownTelephoneWhenDeviceNumberIs0() { + String result = testSubject.resolveTelephone(0); + + assertEquals("telephone: unknown", result); + } + + void setUpResponse(String deviceName) { + when(bridgeHandler.sendCommand(isA(ReadDeviceInfoCommand.class), eq(false))).thenAnswer(invocationOnMock -> { + ReadDeviceInfoCommand cmd = invocationOnMock.getArgument(0); + byte[] payload = new byte[19]; + byte[] nameBytes = deviceName.getBytes(StandardCharsets.US_ASCII); + System.arraycopy(nameBytes, 0, payload, 3, nameBytes.length); + cmd.handleResponse(eventDispatcher, new SatelMessage(ReadDeviceInfoCommand.COMMAND_CODE, payload)); + return true; + }); + } +} diff --git a/bundles/org.openhab.binding.satel/src/test/java/org/openhab/binding/satel/internal/util/EventLogReaderTest.java b/bundles/org.openhab.binding.satel/src/test/java/org/openhab/binding/satel/internal/util/EventLogReaderTest.java new file mode 100644 index 00000000000..e8489fc72fc --- /dev/null +++ b/bundles/org.openhab.binding.satel/src/test/java/org/openhab/binding/satel/internal/util/EventLogReaderTest.java @@ -0,0 +1,394 @@ +/* + * Copyright (c) 2010-2025 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.satel.internal.util; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.isA; +import static org.mockito.Mockito.*; + +import java.nio.charset.StandardCharsets; +import java.time.LocalDateTime; +import java.util.Optional; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.openhab.binding.satel.internal.command.ReadDeviceInfoCommand.DeviceType; +import org.openhab.binding.satel.internal.command.ReadEventCommand; +import org.openhab.binding.satel.internal.command.ReadEventDescCommand; +import org.openhab.binding.satel.internal.event.EventDispatcher; +import org.openhab.binding.satel.internal.handler.SatelBridgeHandler; +import org.openhab.binding.satel.internal.protocol.SatelMessage; +import org.openhab.binding.satel.internal.types.IntegraType; +import org.openhab.binding.satel.internal.util.EventLogReader.EventDescription; + +/** + * @author Krzysztof Goworek - Initial contribution + */ +class EventLogReaderTest { + + private final SatelBridgeHandler bridgeHandler = mock(SatelBridgeHandler.class); + + private final DeviceNameResolver deviceNameResolver = mock(DeviceNameResolver.class); + + private final EventDispatcher eventDispatcher = mock(EventDispatcher.class); + + private final EventLogReader testSubject = new EventLogReader(bridgeHandler, deviceNameResolver); + + @BeforeEach + void setUpBridgeHandler() { + when(bridgeHandler.getEncoding()).thenReturn(StandardCharsets.US_ASCII); + } + + @Test + void readEventShouldReturnEmptyResultWhenCommandFailed() { + when(bridgeHandler.sendCommand(isA(ReadEventCommand.class), eq(false))).thenReturn(false); + + Optional result = testSubject.readEvent(0); + + assertFalse(result.isPresent()); + } + + @Test + void readEventShouldReturnEmptyResultWhenInvalidIndexIsGiven() { + setUpReadEventResponse(new byte[0]); + + Optional result = testSubject.readEvent(0); + + assertFalse(result.isPresent()); + } + + @Test + void readEventShouldReturnEventCodeWhenReadDescriptionFailed() { + setUpReadEventResponse(new byte[] { 0x30, 0x01, 0x10, 0x00, 0x00, 0x01 }); + + Optional result = testSubject.readEvent(0); + + assertTrue(result.isPresent()); + EventDescription eventDescription = result.get(); + assertEquals(0, eventDescription.getKind()); + assertEquals("event #1", eventDescription.getText()); + } + + @Test + void readEventShouldReturnEventCodeWithRestoreFlagWhenReadDescriptionFailed() { + setUpReadEventResponse(new byte[] { 0x30, 0x01, 0x10, 0x00, 0x04, 0x01 }); + + Optional result = testSubject.readEvent(0); + + assertTrue(result.isPresent()); + EventDescription eventDescription = result.get(); + assertEquals(0, eventDescription.getKind()); + assertEquals("event #1 (restore)", eventDescription.getText()); + } + + @Test + void readEventShouldReturnEventDescription() { + setUpReadEventResponse(new byte[] { 0x30, 0x01, 0x10, 0x00, 0x04, 0x01 }); + setUpReadEventDescResponse(new byte[] { 0x00, 0x00, 0x55, 0x00, 0x00, 'A', 'r', 'm' }); + + Optional result = testSubject.readEvent(0); + + assertTrue(result.isPresent()); + EventDescription eventDescription = result.get(); + assertEquals(0x55, eventDescription.getKind()); + assertEquals("Arm", eventDescription.getText()); + } + + @Test + void readEventShouldCacheEventDescription() { + setUpReadEventResponse(new byte[] { 0x30, 0x01, 0x10, 0x00, 0x04, 0x01 }); + setUpReadEventDescResponse(new byte[0]); + + testSubject.readEvent(0); + testSubject.readEvent(0); + + verify(bridgeHandler).sendCommand(isA(ReadEventDescCommand.class), eq(false)); + } + + @Test + void buildDetailsShouldReturnDetailsForKind0() { + String result = testSubject.buildDetails(createEventDescription(0)); + + assertEquals("", result); + } + + @Test + void buildDetailsShouldReturnDetailsForKind1() { + when(deviceNameResolver.resolve(DeviceType.PARTITION, 40)).thenReturn("partition"); + when(deviceNameResolver.resolveZoneExpanderKeypad(70, false)).thenReturn("zone|expander|keypad"); + + String result = testSubject.buildDetails(createEventDescription(1)); + + assertEquals("partition, zone|expander|keypad", result); + } + + @Test + void buildDetailsShouldReturnDetailsForKind2() { + when(deviceNameResolver.resolve(DeviceType.PARTITION, 40)).thenReturn("partition"); + when(deviceNameResolver.resolveUser(70)).thenReturn("user"); + + String result = testSubject.buildDetails(createEventDescription(2)); + + assertEquals("partition, user", result); + } + + @Test + void buildDetailsShouldReturnDetailsForKind3() { + when(deviceNameResolver.resolvePartitionKeypad(50)).thenReturn("partition keypad"); + when(deviceNameResolver.resolveUser(70)).thenReturn("user"); + + String result = testSubject.buildDetails(createEventDescription(3)); + + assertEquals("partition keypad, user", result); + } + + @Test + void buildDetailsShouldReturnDetailsForKind4() { + when(bridgeHandler.getIntegraType()).thenReturn(IntegraType.I256_PLUS); + when(deviceNameResolver.resolveZoneExpanderKeypad(70, true)).thenReturn("zone|expander|keypad"); + + String result = testSubject.buildDetails(createEventDescription(4)); + + assertEquals("zone|expander|keypad", result); + } + + @Test + void buildDetailsShouldReturnDetailsForKind5() { + when(deviceNameResolver.resolve(DeviceType.PARTITION, 40)).thenReturn("partition"); + + String result = testSubject.buildDetails(createEventDescription(5)); + + assertEquals("partition", result); + } + + @Test + void buildDetailsShouldReturnDetailsForKind6() { + when(deviceNameResolver.resolve(DeviceType.KEYPAD, 40)).thenReturn("keypad"); + when(deviceNameResolver.resolveUser(70)).thenReturn("user"); + + String result = testSubject.buildDetails(createEventDescription(6)); + + assertEquals("keypad, user", result); + } + + @Test + void buildDetailsShouldReturnDetailsForKind7() { + when(deviceNameResolver.resolveUser(70)).thenReturn("user"); + + String result = testSubject.buildDetails(createEventDescription(7)); + + assertEquals("user", result); + } + + @Test + void buildDetailsShouldReturnDetailsForKind8() { + when(deviceNameResolver.resolve(DeviceType.EXPANDER, 70)).thenReturn("expander"); + + String result = testSubject.buildDetails(createEventDescription(8)); + + assertEquals("expander", result); + } + + @Test + void buildDetailsShouldReturnDetailsForKind9() { + when(deviceNameResolver.resolveTelephone(70)).thenReturn("telephone"); + + String result = testSubject.buildDetails(createEventDescription(9)); + + assertEquals("telephone", result); + } + + @Test + void buildDetailsShouldReturnDefaultDetailsForKind10() { + String result = testSubject.buildDetails(createEventDescription(10)); + + assertEquals("kind=10, partition=40, source=70, object=1, ucn=30", result); + } + + @Test + void buildDetailsShouldReturnDetailsForKind11() { + when(deviceNameResolver.resolve(DeviceType.PARTITION, 40)).thenReturn("partition"); + when(deviceNameResolver.resolveDataBus(70)).thenReturn("data bus"); + + String result = testSubject.buildDetails(createEventDescription(11)); + + assertEquals("partition, data bus", result); + } + + @Test + void buildDetailsShouldReturnDetailsForKind12() { + when(bridgeHandler.getIntegraType()).thenReturn(IntegraType.I256_PLUS); + when(deviceNameResolver.resolveOutputExpander(5, false)).thenReturn("output|expander"); + + String result = testSubject.buildDetails(createEventDescription(12, 0, 5)); + + assertEquals("output|expander", result); + } + + @Test + void buildDetailsShouldReturnDetailsForKind12WithPartition() { + when(bridgeHandler.getIntegraType()).thenReturn(IntegraType.I256_PLUS); + when(deviceNameResolver.resolve(DeviceType.PARTITION, 40)).thenReturn("partition"); + when(deviceNameResolver.resolveOutputExpander(70, true)).thenReturn("output|expander"); + + String result = testSubject.buildDetails(createEventDescription(12)); + + assertEquals("partition, output|expander", result); + } + + @Test + void buildDetailsShouldReturnDetailsForKind13() { + when(deviceNameResolver.resolveOutputExpander(70, false)).thenReturn("output|expander"); + + String result = testSubject.buildDetails(createEventDescription(13)); + + assertEquals("output|expander", result); + } + + @Test + void buildDetailsShouldReturnDetailsForKind13WithPartition() { + when(deviceNameResolver.resolve(DeviceType.PARTITION, 40)).thenReturn("partition"); + when(deviceNameResolver.resolveOutputExpander(130, false)).thenReturn("output|expander"); + + String result = testSubject.buildDetails(createEventDescription(13, 0, 130)); + + assertEquals("partition, output|expander", result); + } + + @Test + void buildDetailsShouldReturnDetailsForKind14() { + when(deviceNameResolver.resolveTelephone(40)).thenReturn("telephone"); + when(deviceNameResolver.resolveUser(70)).thenReturn("user"); + + String result = testSubject.buildDetails(createEventDescription(14)); + + assertEquals("telephone, user", result); + } + + @Test + void buildDetailsShouldReturnDetailsForKind15() { + when(deviceNameResolver.resolve(DeviceType.PARTITION, 40)).thenReturn("partition"); + when(deviceNameResolver.resolve(DeviceType.TIMER, 70)).thenReturn("timer"); + + String result = testSubject.buildDetails(createEventDescription(15)); + + assertEquals("partition, timer", result); + } + + @Test + void buildDetailsShouldReturnDetailsForKind30() { + when(deviceNameResolver.resolve(DeviceType.KEYPAD, 40)).thenReturn("keypad"); + + String result = testSubject.buildDetails(createEventDescription(30)); + + assertEquals("keypad, ip: 70.62", result); + } + + @Test + void buildDetailsShouldReturnDetailsForKind31() { + String result = testSubject.buildDetails(createEventDescription(31)); + + assertEquals(".70.62", result); + } + + @Test + void buildDetailsShouldReturnDetailsForKind32() { + when(deviceNameResolver.resolve(DeviceType.PARTITION, 40)).thenReturn("partition"); + when(deviceNameResolver.resolve(DeviceType.ZONE, 70)).thenReturn("zone"); + + String result = testSubject.buildDetails(createEventDescription(32)); + + assertEquals("partition, zone", result); + } + + @Test + void buildDetailsShouldReturnDefaultDetailsForKind33() { + String result = testSubject.buildDetails(createEventDescription(33)); + + assertEquals("kind=33, partition=40, source=70, object=1, ucn=30", result); + } + + @Test + void buildDetailsShouldReadEventForKind31() { + setUpReadEventResponse(new byte[] { 0x30, 0x01, 0x10, 0x00, 0x14, 0x01, (byte) 192, (byte) 168 }); + setUpReadEventDescResponse(new byte[] { 0x00, 0x00, 30, 0x00, 0x00, 'A', 'r', 'm' }); + when(deviceNameResolver.resolve(DeviceType.KEYPAD, 3)).thenReturn("keypad"); + EventDescription eventDescription = createEventDescription(31); + + String result = testSubject.buildDetails(eventDescription); + + assertEquals("keypad, ip: 192.168.70.62", result); + assertEquals(0, eventDescription.getNextIndex()); + } + + @Test + void buildDetailsShouldSkipNextEventForKind31() { + setUpReadEventResponse(new byte[] { 0x30, 0x01, 0x10, 0x00, 0x14, 0x01, (byte) 192, (byte) 168 }); + setUpReadEventDescResponse(new byte[] { 0x00, 0x00, 29, 0x00, 0x00, 'A', 'r', 'm' }); + EventDescription eventDescription = createEventDescription(31); + + String result = testSubject.buildDetails(eventDescription); + + assertEquals(".70.62", result); + assertEquals(20, eventDescription.getNextIndex()); + } + + @Test + void clearCacheShouldClearDeviceNameCache() { + testSubject.clearCache(); + + verify(deviceNameResolver).clearCache(); + } + + private void setUpReadEventResponse(byte[] responseBytes) { + when(bridgeHandler.sendCommand(isA(ReadEventCommand.class), eq(false))).thenAnswer(invocationOnMock -> { + ReadEventCommand cmd = invocationOnMock.getArgument(0); + byte[] payload = new byte[14]; + System.arraycopy(responseBytes, 0, payload, 0, responseBytes.length); + cmd.handleResponse(eventDispatcher, new SatelMessage(ReadEventCommand.COMMAND_CODE, payload)); + return true; + }); + } + + private void setUpReadEventDescResponse(byte[] responseBytes) { + when(bridgeHandler.sendCommand(isA(ReadEventDescCommand.class), eq(false))).thenAnswer(invocationOnMock -> { + ReadEventDescCommand cmd = invocationOnMock.getArgument(0); + byte[] payload = new byte[51]; + System.arraycopy(responseBytes, 0, payload, 0, responseBytes.length); + cmd.handleResponse(eventDispatcher, new SatelMessage(ReadEventDescCommand.COMMAND_CODE, payload)); + return true; + }); + } + + private EventDescription createEventDescription(int descKind) { + return createEventDescription(descKind, 30, 70); + } + + private EventDescription createEventDescription(int descKind, int userControlNumber, int source) { + return testSubject.new EventDescription(mockReadEventCommand(userControlNumber, source), "", descKind); + } + + private ReadEventCommand mockReadEventCommand(int userControlNumber, int source) { + ReadEventCommand result = mock(ReadEventCommand.class); + when(result.getCurrentIndex()).thenReturn(10); + when(result.getNextIndex()).thenReturn(20); + when(result.getUserControlNumber()).thenReturn(userControlNumber); + when(result.getPartition()).thenReturn(40); + when(result.getPartitionKeypad()).thenReturn(50); + when(result.getObject()).thenReturn(1); + when(result.getSource()).thenReturn(source); + when(result.getTimestamp()).thenReturn(LocalDateTime.parse("2020-03-12T12:34:56")); + return result; + } +}