]> git.basschouten.com Git - openhab-addons.git/blob
84f7766e57134d15a0aabaa661f971134bc54bb7
[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.omnilink.internal.handler;
14
15 import static org.openhab.binding.omnilink.internal.OmnilinkBindingConstants.*;
16
17 import java.io.IOException;
18 import java.net.UnknownHostException;
19 import java.time.ZonedDateTime;
20 import java.util.Collection;
21 import java.util.Map;
22 import java.util.Optional;
23 import java.util.Set;
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.omnilink.internal.AudioPlayer;
30 import org.openhab.binding.omnilink.internal.SystemType;
31 import org.openhab.binding.omnilink.internal.TemperatureFormat;
32 import org.openhab.binding.omnilink.internal.action.OmnilinkActions;
33 import org.openhab.binding.omnilink.internal.config.OmnilinkBridgeConfig;
34 import org.openhab.binding.omnilink.internal.discovery.OmnilinkDiscoveryService;
35 import org.openhab.binding.omnilink.internal.exceptions.BridgeOfflineException;
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.Bridge;
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.ThingStatusDetail;
44 import org.openhab.core.thing.ThingTypeUID;
45 import org.openhab.core.thing.binding.BaseBridgeHandler;
46 import org.openhab.core.thing.binding.ThingHandlerService;
47 import org.openhab.core.types.Command;
48 import org.openhab.core.types.RefreshType;
49 import org.openhab.core.types.UnDefType;
50 import org.slf4j.Logger;
51 import org.slf4j.LoggerFactory;
52
53 import com.digitaldan.jomnilinkII.Connection;
54 import com.digitaldan.jomnilinkII.DisconnectListener;
55 import com.digitaldan.jomnilinkII.Message;
56 import com.digitaldan.jomnilinkII.MessageTypes.CommandMessage;
57 import com.digitaldan.jomnilinkII.MessageTypes.EventLogData;
58 import com.digitaldan.jomnilinkII.MessageTypes.ObjectStatus;
59 import com.digitaldan.jomnilinkII.MessageTypes.SecurityCodeValidation;
60 import com.digitaldan.jomnilinkII.MessageTypes.SystemFeatures;
61 import com.digitaldan.jomnilinkII.MessageTypes.SystemFormats;
62 import com.digitaldan.jomnilinkII.MessageTypes.SystemInformation;
63 import com.digitaldan.jomnilinkII.MessageTypes.SystemStatus;
64 import com.digitaldan.jomnilinkII.MessageTypes.statuses.ExtendedAccessControlReaderLockStatus;
65 import com.digitaldan.jomnilinkII.MessageTypes.statuses.ExtendedAreaStatus;
66 import com.digitaldan.jomnilinkII.MessageTypes.statuses.ExtendedAudioZoneStatus;
67 import com.digitaldan.jomnilinkII.MessageTypes.statuses.ExtendedAuxSensorStatus;
68 import com.digitaldan.jomnilinkII.MessageTypes.statuses.ExtendedThermostatStatus;
69 import com.digitaldan.jomnilinkII.MessageTypes.statuses.ExtendedUnitStatus;
70 import com.digitaldan.jomnilinkII.MessageTypes.statuses.ExtendedZoneStatus;
71 import com.digitaldan.jomnilinkII.MessageTypes.statuses.Status;
72 import com.digitaldan.jomnilinkII.MessageTypes.systemevents.AllOnOffEvent;
73 import com.digitaldan.jomnilinkII.MessageTypes.systemevents.ButtonEvent;
74 import com.digitaldan.jomnilinkII.MessageTypes.systemevents.SwitchPressEvent;
75 import com.digitaldan.jomnilinkII.MessageTypes.systemevents.SystemEvent;
76 import com.digitaldan.jomnilinkII.MessageTypes.systemevents.UPBLinkEvent;
77 import com.digitaldan.jomnilinkII.NotificationListener;
78 import com.digitaldan.jomnilinkII.OmniInvalidResponseException;
79 import com.digitaldan.jomnilinkII.OmniNotConnectedException;
80 import com.digitaldan.jomnilinkII.OmniUnknownMessageTypeException;
81 import com.google.gson.Gson;
82
83 /**
84  * The {@link OmnilinkBridgeHandler} defines some methods that are used to
85  * interface with an OmniLink Controller. This by extension also defines the
86  * OmniLink bridge that openHAB will be able to pick up and interface with.
87  *
88  * @author Craig Hamilton - Initial contribution
89  * @author Ethan Dye - openHAB3 rewrite
90  */
91 @NonNullByDefault
92 public class OmnilinkBridgeHandler extends BaseBridgeHandler implements NotificationListener, DisconnectListener {
93     private final Logger logger = LoggerFactory.getLogger(OmnilinkBridgeHandler.class);
94     private @Nullable Connection omniConnection = null;
95     private @Nullable ScheduledFuture<?> connectJob;
96     private @Nullable ScheduledFuture<?> eventPollingJob;
97     private final int autoReconnectPeriod = 60;
98     private Optional<AudioPlayer> audioPlayer = Optional.empty();
99     private Optional<SystemType> systemType = Optional.empty();
100     private final Gson gson = new Gson();
101     private int eventLogNumber = 0;
102
103     public OmnilinkBridgeHandler(Bridge bridge) {
104         super(bridge);
105     }
106
107     @Override
108     public Collection<Class<? extends ThingHandlerService>> getServices() {
109         return Set.of(OmnilinkDiscoveryService.class, OmnilinkActions.class);
110     }
111
112     public void sendOmnilinkCommand(final int message, final int param1, final int param2)
113             throws OmniInvalidResponseException, OmniUnknownMessageTypeException, BridgeOfflineException {
114         try {
115             getOmniConnection().controllerCommand(message, param1, param2);
116         } catch (IOException | OmniNotConnectedException e) {
117             setOfflineAndReconnect(e.getMessage());
118             throw new BridgeOfflineException(e);
119         }
120     }
121
122     public SecurityCodeValidation reqSecurityCodeValidation(int area, int digit1, int digit2, int digit3, int digit4)
123             throws OmniInvalidResponseException, OmniUnknownMessageTypeException, BridgeOfflineException {
124         try {
125             return getOmniConnection().reqSecurityCodeValidation(area, digit1, digit2, digit3, digit4);
126         } catch (IOException | OmniNotConnectedException e) {
127             setOfflineAndReconnect(e.getMessage());
128             throw new BridgeOfflineException(e);
129         }
130     }
131
132     public void activateKeypadEmergency(int area, int emergencyType)
133             throws OmniInvalidResponseException, OmniUnknownMessageTypeException, BridgeOfflineException {
134         try {
135             getOmniConnection().activateKeypadEmergency(area, emergencyType);
136         } catch (IOException | OmniNotConnectedException e) {
137             setOfflineAndReconnect(e.getMessage());
138             throw new BridgeOfflineException(e);
139         }
140     }
141
142     public SystemInformation reqSystemInformation()
143             throws OmniInvalidResponseException, OmniUnknownMessageTypeException, BridgeOfflineException {
144         try {
145             return getOmniConnection().reqSystemInformation();
146         } catch (IOException | OmniNotConnectedException e) {
147             setOfflineAndReconnect(e.getMessage());
148             throw new BridgeOfflineException(e);
149         }
150     }
151
152     public SystemFormats reqSystemFormats()
153             throws OmniInvalidResponseException, OmniUnknownMessageTypeException, BridgeOfflineException {
154         try {
155             return getOmniConnection().reqSystemFormats();
156         } catch (IOException | OmniNotConnectedException e) {
157             setOfflineAndReconnect(e.getMessage());
158             throw new BridgeOfflineException(e);
159         }
160     }
161
162     public void synchronizeControllerTime(ZonedDateTime zdt) {
163         boolean inDaylightSavings = zdt.getZone().getRules().isDaylightSavings(zdt.toInstant());
164         try {
165             getOmniConnection().setTimeCommand(zdt.getYear() - 2000, zdt.getMonthValue(), zdt.getDayOfMonth(),
166                     zdt.getDayOfWeek().getValue(), zdt.getHour(), zdt.getMinute(), inDaylightSavings);
167         } catch (IOException | OmniNotConnectedException | OmniInvalidResponseException
168                 | OmniUnknownMessageTypeException e) {
169             logger.debug("Could not send set date time command to OmniLink Controller: {}", e.getMessage());
170         }
171     }
172
173     private SystemFeatures reqSystemFeatures()
174             throws OmniInvalidResponseException, OmniUnknownMessageTypeException, BridgeOfflineException {
175         try {
176             return getOmniConnection().reqSystemFeatures();
177         } catch (IOException | OmniNotConnectedException e) {
178             setOfflineAndReconnect(e.getMessage());
179             throw new BridgeOfflineException(e);
180         }
181     }
182
183     @Override
184     public void handleCommand(ChannelUID channelUID, Command command) {
185         logger.debug("handleCommand called for channel: {}, command: {}", channelUID, command);
186
187         if (command instanceof RefreshType) {
188             updateChannels();
189             return;
190         }
191
192         switch (channelUID.getId()) {
193             case CHANNEL_CONSOLE_ENABLE_DISABLE_BEEPER:
194                 if (command instanceof StringType) {
195                     try {
196                         sendOmnilinkCommand(CommandMessage.CMD_CONSOLE_ENABLE_DISABLE_BEEPER,
197                                 ((StringType) command).equals(StringType.valueOf("OFF")) ? 0 : 1, 0);
198                         updateState(CHANNEL_CONSOLE_ENABLE_DISABLE_BEEPER, UnDefType.UNDEF);
199                     } catch (NumberFormatException | OmniInvalidResponseException | OmniUnknownMessageTypeException
200                             | BridgeOfflineException e) {
201                         logger.debug("Could not send Console command to OmniLink Controller: {}", e.getMessage());
202                     }
203                 } else {
204                     logger.debug("Invalid command: {}, must be StringType", command);
205                 }
206                 break;
207             case CHANNEL_CONSOLE_BEEP:
208                 if (command instanceof DecimalType) {
209                     try {
210                         sendOmnilinkCommand(CommandMessage.CMD_CONSOLE_BEEP, ((DecimalType) command).intValue(), 0);
211                         updateState(CHANNEL_CONSOLE_BEEP, UnDefType.UNDEF);
212                     } catch (NumberFormatException | OmniInvalidResponseException | OmniUnknownMessageTypeException
213                             | BridgeOfflineException e) {
214                         logger.debug("Could not send Console command to OmniLink Controller: {}", e.getMessage());
215                     }
216                 } else {
217                     logger.debug("Invalid command: {}, must be DecimalType", command);
218                 }
219                 break;
220             default:
221                 logger.warn("Unknown channel for Bridge thing: {}", channelUID);
222         }
223     }
224
225     private void makeOmnilinkConnection() {
226         final Connection connection = omniConnection;
227         if (connection != null && connection.connected()) {
228             return;
229         }
230
231         logger.debug("Attempting to connect to controller!");
232         try {
233             OmnilinkBridgeConfig config = getConfigAs(OmnilinkBridgeConfig.class);
234
235             this.omniConnection = new Connection(config.getIpAddress(), config.getPort(),
236                     config.getKey1() + ":" + config.getKey2());
237
238             /*
239              * HAI only supports one audio player - cycle through features until we find a feature that is an audio
240              * player.
241              */
242             audioPlayer = reqSystemFeatures().getFeatures().stream()
243                     .map(featureCode -> AudioPlayer.getAudioPlayerForFeatureCode(featureCode))
244                     .filter(Optional::isPresent).findFirst().orElse(Optional.empty());
245
246             systemType = SystemType.getType(reqSystemInformation().getModel());
247
248             if (config.getLogPollingInterval() > 0) {
249                 startEventPolling(config.getLogPollingInterval());
250             }
251
252             final Connection connectionNew = omniConnection;
253             if (connectionNew != null) {
254                 connectionNew.enableNotifications();
255                 connectionNew.addNotificationListener(OmnilinkBridgeHandler.this);
256                 connectionNew.addDisconnectListener(this);
257             }
258
259             updateStatus(ThingStatus.ONLINE);
260             cancelReconnectJob(false);
261             updateChannels();
262             updateBridgeProperties();
263         } catch (UnknownHostException e) {
264             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
265         } catch (IOException e) {
266             final Throwable cause = e.getCause();
267             if (cause != null) {
268                 final String causeMessage = cause.getMessage();
269
270                 if (causeMessage != null && causeMessage.contains("Connection timed out")) {
271                     updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
272                             "IP Address probably incorrect, timed out creating connection!");
273                 } else {
274                     updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, causeMessage);
275                 }
276             } else {
277                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
278             }
279         } catch (Exception e) {
280             setOfflineAndReconnect(e.getMessage());
281             logger.debug("Error connecting to OmniLink Controller: {}", e.getMessage());
282         }
283     }
284
285     @Override
286     public void objectStatusNotification(@Nullable ObjectStatus objectStatus) {
287         if (objectStatus != null) {
288             Status[] statuses = objectStatus.getStatuses();
289             for (Status status : statuses) {
290                 if (status instanceof ExtendedUnitStatus) {
291                     ExtendedUnitStatus unitStatus = (ExtendedUnitStatus) status;
292                     int unitNumber = unitStatus.getNumber();
293
294                     logger.debug("Received status update for Unit: {}, status: {}", unitNumber, unitStatus);
295                     Optional<Thing> theThing = getUnitThing(unitNumber);
296                     theThing.map(Thing::getHandler)
297                             .ifPresent(theHandler -> ((UnitHandler) theHandler).handleStatus(unitStatus));
298                 } else if (status instanceof ExtendedZoneStatus) {
299                     ExtendedZoneStatus zoneStatus = (ExtendedZoneStatus) status;
300                     int zoneNumber = zoneStatus.getNumber();
301
302                     logger.debug("Received status update for Zone: {}, status: {}", zoneNumber, zoneStatus);
303                     Optional<Thing> theThing = getChildThing(THING_TYPE_ZONE, zoneNumber);
304                     theThing.map(Thing::getHandler)
305                             .ifPresent(theHandler -> ((ZoneHandler) theHandler).handleStatus(zoneStatus));
306                 } else if (status instanceof ExtendedAreaStatus) {
307                     ExtendedAreaStatus areaStatus = (ExtendedAreaStatus) status;
308                     int areaNumber = areaStatus.getNumber();
309
310                     logger.debug("Received status update for Area: {}, status: {}", areaNumber, areaStatus);
311                     systemType.ifPresent(t -> {
312                         Optional<Thing> theThing = Optional.empty();
313                         switch (t) {
314                             case LUMINA:
315                                 theThing = getChildThing(THING_TYPE_LUMINA_AREA, areaNumber);
316                                 break;
317                             case OMNI:
318                                 theThing = getChildThing(THING_TYPE_OMNI_AREA, areaNumber);
319                                 break;
320                         }
321                         theThing.map(Thing::getHandler)
322                                 .ifPresent(theHandler -> ((AbstractAreaHandler) theHandler).handleStatus(areaStatus));
323                     });
324                 } else if (status instanceof ExtendedAccessControlReaderLockStatus) {
325                     ExtendedAccessControlReaderLockStatus lockStatus = (ExtendedAccessControlReaderLockStatus) status;
326                     int lockNumber = lockStatus.getNumber();
327
328                     logger.debug("Received status update for Lock: {}, status: {}", lockNumber, lockStatus);
329                     Optional<Thing> theThing = getChildThing(THING_TYPE_LOCK, lockNumber);
330                     theThing.map(Thing::getHandler)
331                             .ifPresent(theHandler -> ((LockHandler) theHandler).handleStatus(lockStatus));
332                 } else if (status instanceof ExtendedThermostatStatus) {
333                     ExtendedThermostatStatus thermostatStatus = (ExtendedThermostatStatus) status;
334                     int thermostatNumber = thermostatStatus.getNumber();
335
336                     logger.debug("Received status update for Thermostat: {}, status: {}", thermostatNumber,
337                             thermostatStatus);
338                     Optional<Thing> theThing = getChildThing(THING_TYPE_THERMOSTAT, thermostatNumber);
339                     theThing.map(Thing::getHandler)
340                             .ifPresent(theHandler -> ((ThermostatHandler) theHandler).handleStatus(thermostatStatus));
341                 } else if (status instanceof ExtendedAudioZoneStatus) {
342                     ExtendedAudioZoneStatus audioZoneStatus = (ExtendedAudioZoneStatus) status;
343                     int audioZoneNumber = audioZoneStatus.getNumber();
344
345                     logger.debug("Received status update for Audio Zone: {}, status: {}", audioZoneNumber,
346                             audioZoneStatus);
347                     Optional<Thing> theThing = getChildThing(THING_TYPE_AUDIO_ZONE, audioZoneNumber);
348                     theThing.map(Thing::getHandler)
349                             .ifPresent(theHandler -> ((AudioZoneHandler) theHandler).handleStatus(audioZoneStatus));
350                 } else if (status instanceof ExtendedAuxSensorStatus) {
351                     ExtendedAuxSensorStatus auxSensorStatus = (ExtendedAuxSensorStatus) status;
352                     int auxSensorNumber = auxSensorStatus.getNumber();
353
354                     // Aux Sensors can be either temperature or humidity, need to check both.
355                     Optional<Thing> tempThing = getChildThing(THING_TYPE_TEMP_SENSOR, auxSensorNumber);
356                     Optional<Thing> humidityThing = getChildThing(THING_TYPE_HUMIDITY_SENSOR, auxSensorNumber);
357                     if (tempThing.isPresent()) {
358                         logger.debug("Received status update for Temperature Sensor: {}, status: {}", auxSensorNumber,
359                                 auxSensorStatus);
360                         tempThing.map(Thing::getHandler).ifPresent(
361                                 theHandler -> ((TempSensorHandler) theHandler).handleStatus(auxSensorStatus));
362                     }
363                     if (humidityThing.isPresent()) {
364                         logger.debug("Received status update for Humidity Sensor: {}, status: {}", auxSensorNumber,
365                                 auxSensorStatus);
366                         humidityThing.map(Thing::getHandler).ifPresent(
367                                 theHandler -> ((HumiditySensorHandler) theHandler).handleStatus(auxSensorStatus));
368                     }
369                 } else {
370                     logger.debug("Received Object Status Notification that was not processed: {}", objectStatus);
371                 }
372             }
373         } else {
374             logger.debug("Received null Object Status Notification!");
375         }
376     }
377
378     @Override
379     public void systemEventNotification(@Nullable SystemEvent event) {
380         if (event != null) {
381             logger.debug("Received System Event Notification of type: {}", event.getType());
382             switch (event.getType()) {
383                 case PHONE_LINE_DEAD:
384                 case PHONE_LINE_OFF_HOOK:
385                 case PHONE_LINE_ON_HOOK:
386                 case PHONE_LINE_RING:
387                     ChannelUID channel = new ChannelUID(getThing().getUID(), TRIGGER_CHANNEL_PHONE_LINE_EVENT);
388                     triggerChannel(channel, event.getType().toString().replaceAll("^PHONE_LINE_", ""));
389                     break;
390                 case AC_POWER_OFF:
391                 case AC_POWER_RESTORED:
392                     ChannelUID acChannel = new ChannelUID(getThing().getUID(), TRIGGER_CHANNEL_AC_POWER_EVENT);
393                     triggerChannel(acChannel, event.getType().toString().replaceAll("^AC_POWER_", ""));
394                     break;
395                 case BATTERY_LOW:
396                 case BATTERY_OK:
397                     ChannelUID batteryChannel = new ChannelUID(getThing().getUID(), TRIGGER_CHANNEL_BATTERY_EVENT);
398                     triggerChannel(batteryChannel, event.getType().toString().replaceAll("^BATTERY_", ""));
399                     break;
400                 case DCM_OK:
401                 case DCM_TROUBLE:
402                     ChannelUID dcmChannel = new ChannelUID(getThing().getUID(), TRIGGER_CHANNEL_DCM_EVENT);
403                     triggerChannel(dcmChannel, event.getType().toString().replaceAll("^DCM_", ""));
404                     break;
405                 case ENERGY_COST_CRITICAL:
406                 case ENERGY_COST_HIGH:
407                 case ENERGY_COST_LOW:
408                 case ENERGY_COST_MID:
409                     ChannelUID energyChannel = new ChannelUID(getThing().getUID(), TRIGGER_CHANNEL_ENERGY_COST_EVENT);
410                     triggerChannel(energyChannel, event.getType().toString().replaceAll("^ENERGY_COST_", ""));
411                     break;
412                 case CAMERA_1_TRIGGER:
413                 case CAMERA_2_TRIGGER:
414                 case CAMERA_3_TRIGGER:
415                 case CAMERA_4_TRIGGER:
416                 case CAMERA_5_TRIGGER:
417                 case CAMERA_6_TRIGGER:
418                     ChannelUID cameraChannel = new ChannelUID(getThing().getUID(),
419                             TRIGGER_CHANNEL_CAMERA_TRIGGER_EVENT);
420                     triggerChannel(cameraChannel, String.valueOf(event.getType().toString().charAt(8)));
421                     break;
422                 case BUTTON:
423                     Optional<Thing> buttonThing = getChildThing(THING_TYPE_BUTTON,
424                             ((ButtonEvent) event).getButtonNumber());
425                     buttonThing.map(Thing::getHandler)
426                             .ifPresent(theHandler -> ((ButtonHandler) theHandler).buttonActivated());
427                     break;
428                 case ALL_ON_OFF:
429                     Optional<Thing> areaThing = getChildThing(THING_TYPE_OMNI_AREA, ((AllOnOffEvent) event).getArea());
430                     if (areaThing.isPresent()) {
431                         logger.debug("Thing for allOnOff event: {}", areaThing.get().getUID());
432                         areaThing.map(Thing::getHandler).ifPresent(theHandler -> ((AbstractAreaHandler) theHandler)
433                                 .handleAllOnOffEvent((AllOnOffEvent) event));
434                     }
435                     break;
436                 case UPB_LINK:
437                     UPBLinkEvent linkEvent = (UPBLinkEvent) event;
438                     UPBLinkEvent.Command command = linkEvent.getLinkCommand();
439                     int link = linkEvent.getLinkNumber();
440                     handleUPBLink(link, command);
441                     break;
442                 case ALC_UPB_RADIORA_STARLITE_SWITCH_PRESS:
443                     SwitchPressEvent switchPressEvent = (SwitchPressEvent) event;
444                     int unitNumber = switchPressEvent.getUnitNumber();
445
446                     Optional<Thing> unitThing = getUnitThing(unitNumber);
447                     unitThing.map(Thing::getHandler).ifPresent(
448                             theHandler -> ((UnitHandler) theHandler).handleSwitchPressEvent(switchPressEvent));
449                     break;
450                 default:
451                     logger.warn("Ignoring System Event Notification of type: {}", event.getType());
452             }
453         } else {
454             logger.debug("Received null System Event Notification!");
455         }
456     }
457
458     private void handleUPBLink(int link, UPBLinkEvent.Command command) {
459         final ChannelUID activateChannel;
460
461         if (command == UPBLinkEvent.Command.ACTIVATED) {
462             activateChannel = new ChannelUID(getThing().getUID(), TRIGGER_CHANNEL_UPB_LINK_ACTIVATED_EVENT);
463         } else if (command == UPBLinkEvent.Command.DEACTIVATED) {
464             activateChannel = new ChannelUID(getThing().getUID(), TRIGGER_CHANNEL_UPB_LINK_DEACTIVATED_EVENT);
465         } else {
466             logger.debug("Received unsupported UPB link event: {}", command);
467             return;
468         }
469         triggerChannel(activateChannel, Integer.toString(link));
470     }
471
472     @Override
473     public void notConnectedEvent(@Nullable Exception e) {
474         if (e != null) {
475             logger.debug("Received an OmniLink Controller not connected event: {}", e.getMessage());
476             setOfflineAndReconnect(e.getMessage());
477         }
478     }
479
480     private void getSystemStatus() throws IOException, OmniNotConnectedException, OmniInvalidResponseException,
481             OmniUnknownMessageTypeException {
482         SystemStatus status = getOmniConnection().reqSystemStatus();
483         logger.debug("Received system status: {}", status);
484         // Update controller's reported time
485         String dateString = new StringBuilder().append(2000 + status.getYear()).append("-")
486                 .append(String.format("%02d", status.getMonth())).append("-")
487                 .append(String.format("%02d", status.getDay())).append("T")
488                 .append(String.format("%02d", status.getHour())).append(":")
489                 .append(String.format("%02d", status.getMinute())).append(":")
490                 .append(String.format("%02d", status.getSecond())).toString();
491         updateState(CHANNEL_SYSTEM_DATE, new DateTimeType(dateString));
492     }
493
494     public Message reqObjectProperties(int objectType, int objectNum, int direction, int filter1, int filter2,
495             int filter3) throws OmniInvalidResponseException, OmniUnknownMessageTypeException, BridgeOfflineException {
496         try {
497             return getOmniConnection().reqObjectProperties(objectType, objectNum, direction, filter1, filter2, filter3);
498         } catch (OmniNotConnectedException | IOException e) {
499             setOfflineAndReconnect(e.getMessage());
500             throw new BridgeOfflineException(e);
501         }
502     }
503
504     public Message requestAudioSourceStatus(final int source, final int position)
505             throws OmniInvalidResponseException, OmniUnknownMessageTypeException, BridgeOfflineException {
506         try {
507             return getOmniConnection().reqAudioSourceStatus(source, position);
508         } catch (OmniNotConnectedException | IOException e) {
509             setOfflineAndReconnect(e.getMessage());
510             throw new BridgeOfflineException(e);
511         }
512     }
513
514     public ObjectStatus requestObjectStatus(final int objType, final int startObject, final int endObject,
515             boolean extended)
516             throws OmniInvalidResponseException, OmniUnknownMessageTypeException, BridgeOfflineException {
517         try {
518             return getOmniConnection().reqObjectStatus(objType, startObject, endObject, extended);
519         } catch (OmniNotConnectedException | IOException e) {
520             setOfflineAndReconnect(e.getMessage());
521             throw new BridgeOfflineException(e);
522         }
523     }
524
525     public Optional<TemperatureFormat> getTemperatureFormat() {
526         try {
527             return Optional.of(TemperatureFormat.valueOf(reqSystemFormats().getTempFormat()));
528         } catch (OmniInvalidResponseException | OmniUnknownMessageTypeException | BridgeOfflineException e) {
529             logger.debug("Could not request temperature format from controller: {}", e.getMessage());
530             return Optional.empty();
531         }
532     }
533
534     public void updateChannels() {
535         try {
536             getSystemStatus();
537             updateState(CHANNEL_CONSOLE_ENABLE_DISABLE_BEEPER, UnDefType.UNDEF);
538             updateState(CHANNEL_CONSOLE_BEEP, UnDefType.UNDEF);
539         } catch (IOException | OmniNotConnectedException | OmniInvalidResponseException
540                 | OmniUnknownMessageTypeException e) {
541             logger.warn("Unable to update bridge channels: {}", e.getMessage());
542         }
543     }
544
545     @Override
546     public void dispose() {
547         cancelReconnectJob(true);
548         cancelEventPolling();
549         final Connection connection = omniConnection;
550         if (connection != null) {
551             connection.removeDisconnectListener(this);
552             connection.disconnect();
553         }
554     }
555
556     private Optional<Thing> getChildThing(ThingTypeUID type, int number) {
557         Bridge bridge = getThing();
558         return bridge.getThings().stream().filter(t -> t.getThingTypeUID().equals(type))
559                 .filter(t -> ((Number) t.getConfiguration().get(THING_PROPERTIES_NUMBER)).intValue() == number)
560                 .findFirst();
561     }
562
563     private Optional<Thing> getUnitThing(int unitId) {
564         return SUPPORTED_UNIT_TYPES_UIDS.stream().map(uid -> getChildThing(uid, unitId)).flatMap(Optional::stream)
565                 .findFirst();
566     }
567
568     public Optional<AudioPlayer> getAudioPlayer() {
569         return audioPlayer;
570     }
571
572     public Message readEventRecord(int eventNumber, int direction)
573             throws OmniInvalidResponseException, OmniUnknownMessageTypeException, BridgeOfflineException {
574         try {
575             return getOmniConnection().readEventRecord(eventNumber, direction);
576         } catch (OmniNotConnectedException | IOException e) {
577             setOfflineAndReconnect(e.getMessage());
578             throw new BridgeOfflineException(e);
579         }
580     }
581
582     private void updateBridgeProperties() {
583         try {
584             SystemInformation systemInformation = reqSystemInformation();
585             Map<String, String> properties = editProperties();
586             properties.put(Thing.PROPERTY_MODEL_ID, Integer.toString(systemInformation.getModel()));
587             properties.put(Thing.PROPERTY_FIRMWARE_VERSION,
588                     Integer.toString(systemInformation.getMajor()) + "."
589                             + Integer.toString(systemInformation.getMinor()) + "."
590                             + Integer.toString(systemInformation.getRevision()));
591             properties.put(THING_PROPERTIES_PHONE_NUMBER, systemInformation.getPhone());
592             updateProperties(properties);
593         } catch (OmniInvalidResponseException | OmniUnknownMessageTypeException | BridgeOfflineException e) {
594             logger.debug("Could not request system information from OmniLink Controller: {}", e.getMessage());
595         }
596     }
597
598     @Override
599     public void initialize() {
600         scheduleReconnectJob();
601     }
602
603     private void scheduleReconnectJob() {
604         ScheduledFuture<?> currentReconnectJob = connectJob;
605         if (currentReconnectJob == null || currentReconnectJob.isDone()) {
606             connectJob = super.scheduler.scheduleWithFixedDelay(this::makeOmnilinkConnection, 0, autoReconnectPeriod,
607                     TimeUnit.SECONDS);
608         }
609     }
610
611     private void cancelReconnectJob(boolean kill) {
612         ScheduledFuture<?> currentReconnectJob = connectJob;
613         if (currentReconnectJob != null) {
614             currentReconnectJob.cancel(kill);
615         }
616     }
617
618     private void setOfflineAndReconnect(@Nullable String message) {
619         updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, message);
620         cancelEventPolling();
621         final Connection connection = omniConnection;
622         if (connection != null) {
623             connection.removeDisconnectListener(this);
624         }
625         scheduleReconnectJob();
626     }
627
628     private void startEventPolling(int interval) {
629         ScheduledFuture<?> eventPollingJobFuture = eventPollingJob;
630         if (eventPollingJobFuture == null || eventPollingJobFuture.isDone()) {
631             eventLogNumber = 0;
632             eventPollingJob = super.scheduler.scheduleWithFixedDelay(this::pollEvents, 0, interval, TimeUnit.SECONDS);
633         }
634     }
635
636     private void cancelEventPolling() {
637         ScheduledFuture<?> eventPollingJobFuture = eventPollingJob;
638         if (eventPollingJobFuture != null) {
639             eventPollingJobFuture.cancel(true);
640         }
641     }
642
643     private void pollEvents() {
644         // On first run, direction is -1 (most recent event), after its 1 for the next log message
645         try {
646             Message message;
647             do {
648                 logger.trace("Polling for event log messages.");
649                 int direction = eventLogNumber == 0 ? -1 : 1;
650                 message = readEventRecord(eventLogNumber, direction);
651                 if (message.getMessageType() == Message.MESG_TYPE_EVENT_LOG_DATA) {
652                     EventLogData logData = (EventLogData) message;
653                     logger.debug("Processing event log message number: {}", logData.getEventNumber());
654                     eventLogNumber = logData.getEventNumber();
655                     String json = gson.toJson(logData);
656                     logger.debug("Receieved event log message: {}", json);
657                     updateState(CHANNEL_EVENT_LOG, new StringType(json));
658                 }
659             } while (message.getMessageType() != Message.MESG_TYPE_END_OF_DATA);
660
661         } catch (OmniInvalidResponseException | OmniUnknownMessageTypeException | BridgeOfflineException e) {
662             logger.debug("Exception recieved while polling for event log messages: {}", e.getMessage());
663         }
664     }
665
666     private Connection getOmniConnection() throws OmniNotConnectedException {
667         final Connection connection = omniConnection;
668         if (connection != null) {
669             return connection;
670         } else {
671             throw new OmniNotConnectedException("Connection not yet established!");
672         }
673     }
674 }