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