]> git.basschouten.com Git - openhab-addons.git/blob
82dc52a87bb3c3aa9b334e6a79a6fb34342f4b3e
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2023 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
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
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.binding.satel.internal.handler;
14
15 import static org.openhab.binding.satel.internal.SatelBindingConstants.*;
16
17 import java.nio.charset.Charset;
18 import java.time.ZonedDateTime;
19 import java.util.Collection;
20 import java.util.Map;
21 import java.util.Optional;
22 import java.util.Set;
23 import java.util.concurrent.ConcurrentHashMap;
24 import java.util.concurrent.ScheduledFuture;
25 import java.util.concurrent.TimeUnit;
26
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;
47
48 /**
49  * The {@link SatelEventLogHandler} is responsible for handling commands, which are
50  * sent to one of the event log channels.
51  *
52  * @author Krzysztof Goworek - Initial contribution
53  */
54 @NonNullByDefault
55 public class SatelEventLogHandler extends SatelThingHandler {
56
57     public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES = Set.of(THING_TYPE_EVENTLOG);
58
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);
62
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();
68
69     /**
70      * Represents single record of the event log.
71      *
72      * @author Krzysztof Goworek
73      *
74      */
75     public static class EventLogEntry {
76
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;
82
83         private EventLogEntry(int index, int prevIndex, ZonedDateTime timestamp, String description, String details) {
84             this.index = index;
85             this.prevIndex = prevIndex;
86             this.timestamp = timestamp;
87             this.description = description;
88             this.details = details;
89         }
90
91         /**
92          * @return index of this record entry
93          */
94         public int getIndex() {
95             return index;
96         }
97
98         /**
99          * @return index of the previous record entry in the log
100          */
101         public int getPrevIndex() {
102             return prevIndex;
103         }
104
105         /**
106          * @return date and time when the event occurred
107          */
108         public ZonedDateTime getTimestamp() {
109             return timestamp;
110         }
111
112         /**
113          * @return description of the event
114          */
115         public String getDescription() {
116             return description;
117         }
118
119         /**
120          * @return details about zones, partitions, users, etc
121          */
122         public String getDetails() {
123             return details;
124         }
125
126         @Override
127         public String toString() {
128             return "EventLogEntry [index=" + index + ", prevIndex=" + prevIndex + ", timestamp=" + timestamp
129                     + ", description=" + description + ", details=" + details + "]";
130         }
131     }
132
133     public SatelEventLogHandler(Thing thing) {
134         super(thing);
135     }
136
137     @Override
138     public void initialize() {
139         super.initialize();
140
141         withBridgeHandlerPresent(bridgeHandler -> {
142             this.encoding = bridgeHandler.getEncoding();
143         });
144
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);
150         }
151     }
152
153     @Override
154     public void dispose() {
155         super.dispose();
156
157         final ScheduledFuture<?> cacheExpirationJob = this.cacheExpirationJob;
158         if (cacheExpirationJob != null && !cacheExpirationJob.isCancelled()) {
159             cacheExpirationJob.cancel(true);
160         }
161         this.cacheExpirationJob = null;
162     }
163
164     @Override
165     public void handleCommand(ChannelUID channelUID, Command command) {
166         logger.debug("New command for {}: {}", channelUID, command);
167
168         if (CHANNEL_INDEX.equals(channelUID.getId()) && command instanceof DecimalType decimalCommand) {
169             int eventIndex = decimalCommand.intValue();
170             withBridgeHandlerPresent(bridgeHandler -> readEvent(eventIndex).ifPresent(entry -> {
171                 // update items
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()));
177             }));
178         }
179     }
180
181     @Override
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);
187         }
188     }
189
190     @Override
191     public Collection<Class<? extends ThingHandlerService>> getServices() {
192         return Set.of(SatelEventLogActions.class);
193     }
194
195     /**
196      * Reads one record from the event log.
197      *
198      * @param eventIndex record index
199      * @return record data or {@linkplain Optional#empty()} if there is no record under given index
200      */
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;
208             String eventDetails;
209
210             switch (eventDesc.getKind()) {
211                 case 0:
212                     eventDetails = "";
213                     break;
214                 case 1:
215                     eventDetails = getDeviceDescription(DeviceType.PARTITION, readEventCmd.getPartition())
216                             + DETAILS_SEPARATOR + getZoneExpanderKeypadDescription(readEventCmd.getSource(), upperZone);
217                     break;
218                 case 2:
219                     eventDetails = getDeviceDescription(DeviceType.PARTITION, readEventCmd.getPartition())
220                             + DETAILS_SEPARATOR + getUserDescription(readEventCmd.getSource());
221                     break;
222                 case 3:
223                     eventDetails = getDeviceDescription(DeviceType.EXPANDER, readEventCmd.getPartitionKeypad())
224                             + DETAILS_SEPARATOR + getUserDescription(readEventCmd.getSource());
225                     break;
226                 case 4:
227                     eventDetails = getZoneExpanderKeypadDescription(readEventCmd.getSource(), upperZone);
228                     break;
229                 case 5:
230                     eventDetails = getDeviceDescription(DeviceType.PARTITION, readEventCmd.getPartition());
231                     break;
232                 case 6:
233                     eventDetails = getDeviceDescription(DeviceType.KEYPAD, readEventCmd.getPartition())
234                             + DETAILS_SEPARATOR + getUserDescription(readEventCmd.getSource());
235                     break;
236                 case 7:
237                     eventDetails = getUserDescription(readEventCmd.getSource());
238                     break;
239                 case 8:
240                     eventDetails = getDeviceDescription(DeviceType.EXPANDER, readEventCmd.getSource());
241                     break;
242                 case 9:
243                     eventDetails = getTelephoneDescription(readEventCmd.getSource());
244                     break;
245                 case 11:
246                     eventDetails = getDeviceDescription(DeviceType.PARTITION, readEventCmd.getPartition())
247                             + DETAILS_SEPARATOR + getDataBusDescription(readEventCmd.getSource());
248                     break;
249                 case 12:
250                     if (readEventCmd.getSource() <= getBridgeHandler().getIntegraType().getOnMainboard()) {
251                         eventDetails = getOutputExpanderDescription(readEventCmd.getSource(), upperZone);
252                     } else {
253                         eventDetails = getDeviceDescription(DeviceType.PARTITION, readEventCmd.getPartition())
254                                 + DETAILS_SEPARATOR + getOutputExpanderDescription(readEventCmd.getSource(), upperZone);
255                     }
256                     break;
257                 case 13:
258                     if (readEventCmd.getSource() <= 128) {
259                         eventDetails = getOutputExpanderDescription(readEventCmd.getSource(), upperZone);
260                     } else {
261                         eventDetails = getDeviceDescription(DeviceType.PARTITION, readEventCmd.getPartition())
262                                 + DETAILS_SEPARATOR + getOutputExpanderDescription(readEventCmd.getSource(), upperZone);
263                     }
264                     break;
265                 case 14:
266                     eventDetails = getTelephoneDescription(readEventCmd.getPartition()) + DETAILS_SEPARATOR
267                             + getUserDescription(readEventCmd.getSource());
268                     break;
269                 case 15:
270                     eventDetails = getDeviceDescription(DeviceType.PARTITION, readEventCmd.getPartition())
271                             + DETAILS_SEPARATOR + getDeviceDescription(DeviceType.TIMER, readEventCmd.getSource());
272                     break;
273                 case 31:
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();
280                     }
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();
286                     }
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;
292                     break;
293                 case 32:
294                     eventDetails = getDeviceDescription(DeviceType.PARTITION, readEventCmd.getPartition())
295                             + DETAILS_SEPARATOR + getDeviceDescription(DeviceType.ZONE, readEventCmd.getSource());
296                     break;
297                 default:
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());
303             }
304
305             return Optional.of(new EventLogEntry(currentIndex, readEventCmd.getNextIndex(),
306                     readEventCmd.getTimestamp().atZone(getBridgeHandler().getZoneId()), eventText, eventDetails));
307         });
308     }
309
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();
318         } else {
319             return Optional.of(readEventDescription(readEventCmd));
320         }
321     }
322
323     private static class EventDescriptionCacheEntry {
324         private final String eventText;
325         private final int descKind;
326
327         EventDescriptionCacheEntry(String eventText, int descKind) {
328             this.eventText = eventText;
329             this.descKind = descKind;
330         }
331
332         String getText() {
333             return eventText;
334         }
335
336         int getKind() {
337             return descKind;
338         }
339     }
340
341     private static class EventDescription extends EventDescriptionCacheEntry {
342         private final ReadEventCommand readEventCmd;
343
344         EventDescription(ReadEventCommand readEventCmd, String eventText, int descKind) {
345             super(eventText, descKind);
346             this.readEventCmd = readEventCmd;
347         }
348     }
349
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);
358                 return null;
359             }
360             return new EventDescriptionCacheEntry(cmd.getText(encoding), cmd.getKind());
361         });
362         if (mapValue == null) {
363             return new EventDescription(readEventCmd, NOT_AVAILABLE_TEXT, 0);
364         } else {
365             return new EventDescription(readEventCmd, mapValue.getText(), mapValue.getKind());
366         }
367     }
368
369     private String getOutputExpanderDescription(int deviceNumber, boolean upperOutput) {
370         if (deviceNumber == 0) {
371             return "mainboard";
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);
376         } else {
377             return "invalid output|expander device: " + deviceNumber;
378         }
379     }
380
381     private String getZoneExpanderKeypadDescription(int deviceNumber, boolean upperZone) {
382         if (deviceNumber == 0) {
383             return "mainboard";
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);
388         } else {
389             return getDeviceDescription(DeviceType.KEYPAD, deviceNumber);
390         }
391     }
392
393     private String getUserDescription(int deviceNumber) {
394         return deviceNumber == 0 ? "user: unknown" : getDeviceDescription(DeviceType.USER, deviceNumber);
395     }
396
397     private String getDataBusDescription(int deviceNumber) {
398         return "data bus: " + deviceNumber;
399     }
400
401     private String getTelephoneDescription(int deviceNumber) {
402         return deviceNumber == 0 ? "telephone: unknown" : getDeviceDescription(DeviceType.TELEPHONE, deviceNumber);
403     }
404
405     private String getDeviceDescription(DeviceType deviceType, int deviceNumber) {
406         return String.format("%s: %s", deviceType.name().toLowerCase(), readDeviceName(deviceType, deviceNumber));
407     }
408
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);
415                 return null;
416             }
417             return cmd.getName(encoding);
418         });
419         return result == null ? NOT_AVAILABLE_TEXT : result;
420     }
421 }