2 * Copyright (c) 2010-2022 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;
20 import java.util.Collections;
22 import java.util.Optional;
24 import java.util.concurrent.ConcurrentHashMap;
25 import java.util.concurrent.ScheduledFuture;
26 import java.util.concurrent.TimeUnit;
28 import org.eclipse.jdt.annotation.NonNullByDefault;
29 import org.eclipse.jdt.annotation.Nullable;
30 import org.openhab.binding.satel.internal.action.SatelEventLogActions;
31 import org.openhab.binding.satel.internal.command.ReadDeviceInfoCommand;
32 import org.openhab.binding.satel.internal.command.ReadDeviceInfoCommand.DeviceType;
33 import org.openhab.binding.satel.internal.command.ReadEventCommand;
34 import org.openhab.binding.satel.internal.command.ReadEventDescCommand;
35 import org.openhab.binding.satel.internal.event.ConnectionStatusEvent;
36 import org.openhab.binding.satel.internal.types.IntegraType;
37 import org.openhab.core.library.types.DateTimeType;
38 import org.openhab.core.library.types.DecimalType;
39 import org.openhab.core.library.types.StringType;
40 import org.openhab.core.thing.ChannelUID;
41 import org.openhab.core.thing.Thing;
42 import org.openhab.core.thing.ThingStatus;
43 import org.openhab.core.thing.ThingTypeUID;
44 import org.openhab.core.thing.binding.ThingHandlerService;
45 import org.openhab.core.types.Command;
46 import org.slf4j.Logger;
47 import org.slf4j.LoggerFactory;
50 * The {@link SatelEventLogHandler} is responsible for handling commands, which are
51 * sent to one of the event log channels.
53 * @author Krzysztof Goworek - Initial contribution
56 public class SatelEventLogHandler extends SatelThingHandler {
58 public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES = Collections.singleton(THING_TYPE_EVENTLOG);
60 private static final String NOT_AVAILABLE_TEXT = "N/A";
61 private static final String DETAILS_SEPARATOR = ", ";
62 private static final long CACHE_CLEAR_INTERVAL = TimeUnit.MINUTES.toMillis(30);
64 private final Logger logger = LoggerFactory.getLogger(SatelEventLogHandler.class);
65 private final Map<String, @Nullable EventDescriptionCacheEntry> eventDescriptions = new ConcurrentHashMap<>();
66 private final Map<String, @Nullable String> deviceNameCache = new ConcurrentHashMap<>();
67 private @Nullable ScheduledFuture<?> cacheExpirationJob;
68 private Charset encoding = Charset.defaultCharset();
71 * Represents single record of the event log.
73 * @author Krzysztof Goworek
76 public static class EventLogEntry {
78 private final int index;
79 private final int prevIndex;
80 private final ZonedDateTime timestamp;
81 private final String description;
82 private final String details;
84 private EventLogEntry(int index, int prevIndex, ZonedDateTime timestamp, String description, String details) {
86 this.prevIndex = prevIndex;
87 this.timestamp = timestamp;
88 this.description = description;
89 this.details = details;
93 * @return index of this record entry
95 public int getIndex() {
100 * @return index of the previous record entry in the log
102 public int getPrevIndex() {
107 * @return date and time when the event occurred
109 public ZonedDateTime getTimestamp() {
114 * @return description of the event
116 public String getDescription() {
121 * @return details about zones, partitions, users, etc
123 public String getDetails() {
128 public String toString() {
129 return "EventLogEntry [index=" + index + ", prevIndex=" + prevIndex + ", timestamp=" + timestamp
130 + ", description=" + description + ", details=" + details + "]";
134 public SatelEventLogHandler(Thing thing) {
139 public void initialize() {
142 withBridgeHandlerPresent(bridgeHandler -> {
143 this.encoding = bridgeHandler.getEncoding();
146 final ScheduledFuture<?> cacheExpirationJob = this.cacheExpirationJob;
147 if (cacheExpirationJob == null || cacheExpirationJob.isCancelled()) {
148 // for simplicity all cache entries are cleared every 30 minutes
149 this.cacheExpirationJob = scheduler.scheduleWithFixedDelay(deviceNameCache::clear, CACHE_CLEAR_INTERVAL,
150 CACHE_CLEAR_INTERVAL, TimeUnit.MILLISECONDS);
155 public void dispose() {
158 final ScheduledFuture<?> cacheExpirationJob = this.cacheExpirationJob;
159 if (cacheExpirationJob != null && !cacheExpirationJob.isCancelled()) {
160 cacheExpirationJob.cancel(true);
162 this.cacheExpirationJob = null;
166 public void handleCommand(ChannelUID channelUID, Command command) {
167 logger.debug("New command for {}: {}", channelUID, command);
169 if (CHANNEL_INDEX.equals(channelUID.getId()) && command instanceof DecimalType) {
170 int eventIndex = ((DecimalType) command).intValue();
171 withBridgeHandlerPresent(bridgeHandler -> readEvent(eventIndex).ifPresent(entry -> {
173 updateState(CHANNEL_INDEX, new DecimalType(entry.getIndex()));
174 updateState(CHANNEL_PREV_INDEX, new DecimalType(entry.getPrevIndex()));
175 updateState(CHANNEL_TIMESTAMP, new DateTimeType(entry.getTimestamp()));
176 updateState(CHANNEL_DESCRIPTION, new StringType(entry.getDescription()));
177 updateState(CHANNEL_DETAILS, new StringType(entry.getDetails()));
183 public void incomingEvent(ConnectionStatusEvent event) {
184 logger.trace("Handling incoming event: {}", event);
185 // we have just connected, change thing's status
186 if (event.isConnected()) {
187 updateStatus(ThingStatus.ONLINE);
192 public Collection<Class<? extends ThingHandlerService>> getServices() {
193 return Collections.singleton(SatelEventLogActions.class);
197 * Reads one record from the event log.
199 * @param eventIndex record index
200 * @return record data or {@linkplain Optional#empty()} if there is no record under given index
202 public Optional<EventLogEntry> readEvent(int eventIndex) {
203 return getEventDescription(eventIndex).flatMap(eventDesc -> {
204 ReadEventCommand readEventCmd = eventDesc.readEventCmd;
205 int currentIndex = readEventCmd.getCurrentIndex();
206 String eventText = eventDesc.getText();
207 boolean upperZone = getBridgeHandler().getIntegraType() == IntegraType.I256_PLUS
208 && readEventCmd.getUserControlNumber() > 0;
211 switch (eventDesc.getKind()) {
216 eventDetails = getDeviceDescription(DeviceType.PARTITION, readEventCmd.getPartition())
217 + DETAILS_SEPARATOR + getZoneExpanderKeypadDescription(readEventCmd.getSource(), upperZone);
220 eventDetails = getDeviceDescription(DeviceType.PARTITION, readEventCmd.getPartition())
221 + DETAILS_SEPARATOR + getUserDescription(readEventCmd.getSource());
224 eventDetails = getDeviceDescription(DeviceType.EXPANDER, readEventCmd.getPartitionKeypad())
225 + DETAILS_SEPARATOR + getUserDescription(readEventCmd.getSource());
228 eventDetails = getZoneExpanderKeypadDescription(readEventCmd.getSource(), upperZone);
231 eventDetails = getDeviceDescription(DeviceType.PARTITION, readEventCmd.getPartition());
234 eventDetails = getDeviceDescription(DeviceType.KEYPAD, readEventCmd.getPartition())
235 + DETAILS_SEPARATOR + getUserDescription(readEventCmd.getSource());
238 eventDetails = getUserDescription(readEventCmd.getSource());
241 eventDetails = getDeviceDescription(DeviceType.EXPANDER, readEventCmd.getSource());
244 eventDetails = getTelephoneDescription(readEventCmd.getSource());
247 eventDetails = getDeviceDescription(DeviceType.PARTITION, readEventCmd.getPartition())
248 + DETAILS_SEPARATOR + getDataBusDescription(readEventCmd.getSource());
251 if (readEventCmd.getSource() <= getBridgeHandler().getIntegraType().getOnMainboard()) {
252 eventDetails = getOutputExpanderDescription(readEventCmd.getSource(), upperZone);
254 eventDetails = getDeviceDescription(DeviceType.PARTITION, readEventCmd.getPartition())
255 + DETAILS_SEPARATOR + getOutputExpanderDescription(readEventCmd.getSource(), upperZone);
259 if (readEventCmd.getSource() <= 128) {
260 eventDetails = getOutputExpanderDescription(readEventCmd.getSource(), upperZone);
262 eventDetails = getDeviceDescription(DeviceType.PARTITION, readEventCmd.getPartition())
263 + DETAILS_SEPARATOR + getOutputExpanderDescription(readEventCmd.getSource(), upperZone);
267 eventDetails = getTelephoneDescription(readEventCmd.getPartition()) + DETAILS_SEPARATOR
268 + getUserDescription(readEventCmd.getSource());
271 eventDetails = getDeviceDescription(DeviceType.PARTITION, readEventCmd.getPartition())
272 + DETAILS_SEPARATOR + getDeviceDescription(DeviceType.TIMER, readEventCmd.getSource());
275 // this description consists of two records, so we must read additional record from the log
276 eventDetails = "." + readEventCmd.getSource() + "."
277 + (readEventCmd.getObject() * 32 + readEventCmd.getUserControlNumber());
278 Optional<EventDescription> eventDescNext = getEventDescription(readEventCmd.getNextIndex());
279 if (!eventDescNext.isPresent()) {
280 return Optional.empty();
282 final EventDescription eventDescNextItem = eventDescNext.get();
283 if (eventDescNextItem.getKind() != 30) {
284 logger.info("Unexpected event record kind {} at index {}", eventDescNextItem.getKind(),
285 readEventCmd.getNextIndex());
286 return Optional.empty();
288 readEventCmd = eventDescNextItem.readEventCmd;
289 eventText = eventDescNextItem.getText();
290 eventDetails = getDeviceDescription(DeviceType.KEYPAD, readEventCmd.getPartition())
291 + DETAILS_SEPARATOR + "ip: " + readEventCmd.getSource() + "."
292 + (readEventCmd.getObject() * 32 + readEventCmd.getUserControlNumber()) + eventDetails;
295 eventDetails = getDeviceDescription(DeviceType.PARTITION, readEventCmd.getPartition())
296 + DETAILS_SEPARATOR + getDeviceDescription(DeviceType.ZONE, readEventCmd.getSource());
299 logger.info("Unsupported device kind code {} at index {}", eventDesc.getKind(),
300 readEventCmd.getCurrentIndex());
301 eventDetails = String.join(DETAILS_SEPARATOR, "kind=" + eventDesc.getKind(),
302 "partition=" + readEventCmd.getPartition(), "source=" + readEventCmd.getSource(),
303 "object=" + readEventCmd.getObject(), "ucn=" + readEventCmd.getUserControlNumber());
306 return Optional.of(new EventLogEntry(currentIndex, readEventCmd.getNextIndex(),
307 readEventCmd.getTimestamp().atZone(getBridgeHandler().getZoneId()), eventText, eventDetails));
311 private Optional<EventDescription> getEventDescription(int eventIndex) {
312 ReadEventCommand readEventCmd = new ReadEventCommand(eventIndex);
313 if (!getBridgeHandler().sendCommand(readEventCmd, false)) {
314 logger.info("Unable to read event record for given index: {}", eventIndex);
315 return Optional.empty();
316 } else if (readEventCmd.isEmpty()) {
317 logger.info("No record under given index: {}", eventIndex);
318 return Optional.empty();
320 return Optional.of(readEventDescription(readEventCmd));
324 private static class EventDescriptionCacheEntry {
325 private final String eventText;
326 private final int descKind;
328 EventDescriptionCacheEntry(String eventText, int descKind) {
329 this.eventText = eventText;
330 this.descKind = descKind;
342 private static class EventDescription extends EventDescriptionCacheEntry {
343 private final ReadEventCommand readEventCmd;
345 EventDescription(ReadEventCommand readEventCmd, String eventText, int descKind) {
346 super(eventText, descKind);
347 this.readEventCmd = readEventCmd;
351 private EventDescription readEventDescription(ReadEventCommand readEventCmd) {
352 int eventCode = readEventCmd.getEventCode();
353 boolean restore = readEventCmd.isRestore();
354 String mapKey = String.format("%d_%b", eventCode, restore);
355 EventDescriptionCacheEntry mapValue = eventDescriptions.computeIfAbsent(mapKey, k -> {
356 ReadEventDescCommand cmd = new ReadEventDescCommand(eventCode, restore, true);
357 if (!getBridgeHandler().sendCommand(cmd, false)) {
358 logger.info("Unable to read event description: {}, {}", eventCode, restore);
361 return new EventDescriptionCacheEntry(cmd.getText(encoding), cmd.getKind());
363 if (mapValue == null) {
364 return new EventDescription(readEventCmd, NOT_AVAILABLE_TEXT, 0);
366 return new EventDescription(readEventCmd, mapValue.getText(), mapValue.getKind());
370 private String getOutputExpanderDescription(int deviceNumber, boolean upperOutput) {
371 if (deviceNumber == 0) {
373 } else if (deviceNumber <= 128) {
374 return getDeviceDescription(DeviceType.OUTPUT, upperOutput ? 128 + deviceNumber : deviceNumber);
375 } else if (deviceNumber <= 192) {
376 return getDeviceDescription(DeviceType.EXPANDER, deviceNumber);
378 return "invalid output|expander device: " + deviceNumber;
382 private String getZoneExpanderKeypadDescription(int deviceNumber, boolean upperZone) {
383 if (deviceNumber == 0) {
385 } else if (deviceNumber <= 128) {
386 return getDeviceDescription(DeviceType.ZONE, upperZone ? 128 + deviceNumber : deviceNumber);
387 } else if (deviceNumber <= 192) {
388 return getDeviceDescription(DeviceType.EXPANDER, deviceNumber);
390 return getDeviceDescription(DeviceType.KEYPAD, deviceNumber);
394 private String getUserDescription(int deviceNumber) {
395 return deviceNumber == 0 ? "user: unknown" : getDeviceDescription(DeviceType.USER, deviceNumber);
398 private String getDataBusDescription(int deviceNumber) {
399 return "data bus: " + deviceNumber;
402 private String getTelephoneDescription(int deviceNumber) {
403 return deviceNumber == 0 ? "telephone: unknown" : getDeviceDescription(DeviceType.TELEPHONE, deviceNumber);
406 private String getDeviceDescription(DeviceType deviceType, int deviceNumber) {
407 return String.format("%s: %s", deviceType.name().toLowerCase(), readDeviceName(deviceType, deviceNumber));
410 private String readDeviceName(DeviceType deviceType, int deviceNumber) {
411 String cacheKey = String.format("%s_%d", deviceType, deviceNumber);
412 String result = deviceNameCache.computeIfAbsent(cacheKey, k -> {
413 ReadDeviceInfoCommand cmd = new ReadDeviceInfoCommand(deviceType, deviceNumber);
414 if (!getBridgeHandler().sendCommand(cmd, false)) {
415 logger.info("Unable to read device info: {}, {}", deviceType, deviceNumber);
418 return cmd.getName(encoding);
420 return result == null ? NOT_AVAILABLE_TEXT : result;