]> git.basschouten.com Git - openhab-addons.git/blob
890b583e42004ef16bff6dc1eb00f5fdc84573dc
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2024 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 stringCommand) {
195                     try {
196                         sendOmnilinkCommand(CommandMessage.CMD_CONSOLE_ENABLE_DISABLE_BEEPER,
197                                 stringCommand.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 decimalCommand) {
209                     try {
210                         sendOmnilinkCommand(CommandMessage.CMD_CONSOLE_BEEP, decimalCommand.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 unitStatus) {
291                     int unitNumber = unitStatus.getNumber();
292
293                     logger.debug("Received status update for Unit: {}, status: {}", unitNumber, unitStatus);
294                     Optional<Thing> theThing = getUnitThing(unitNumber);
295                     theThing.map(Thing::getHandler)
296                             .ifPresent(theHandler -> ((UnitHandler) theHandler).handleStatus(unitStatus));
297                 } else if (status instanceof ExtendedZoneStatus zoneStatus) {
298                     int zoneNumber = zoneStatus.getNumber();
299
300                     logger.debug("Received status update for Zone: {}, status: {}", zoneNumber, zoneStatus);
301                     Optional<Thing> theThing = getChildThing(THING_TYPE_ZONE, zoneNumber);
302                     theThing.map(Thing::getHandler)
303                             .ifPresent(theHandler -> ((ZoneHandler) theHandler).handleStatus(zoneStatus));
304                 } else if (status instanceof ExtendedAreaStatus areaStatus) {
305                     int areaNumber = areaStatus.getNumber();
306
307                     logger.debug("Received status update for Area: {}, status: {}", areaNumber, areaStatus);
308                     systemType.ifPresent(t -> {
309                         Optional<Thing> theThing = Optional.empty();
310                         switch (t) {
311                             case LUMINA:
312                                 theThing = getChildThing(THING_TYPE_LUMINA_AREA, areaNumber);
313                                 break;
314                             case OMNI:
315                                 theThing = getChildThing(THING_TYPE_OMNI_AREA, areaNumber);
316                                 break;
317                         }
318                         theThing.map(Thing::getHandler)
319                                 .ifPresent(theHandler -> ((AbstractAreaHandler) theHandler).handleStatus(areaStatus));
320                     });
321                 } else if (status instanceof ExtendedAccessControlReaderLockStatus lockStatus) {
322                     int lockNumber = lockStatus.getNumber();
323
324                     logger.debug("Received status update for Lock: {}, status: {}", lockNumber, lockStatus);
325                     Optional<Thing> theThing = getChildThing(THING_TYPE_LOCK, lockNumber);
326                     theThing.map(Thing::getHandler)
327                             .ifPresent(theHandler -> ((LockHandler) theHandler).handleStatus(lockStatus));
328                 } else if (status instanceof ExtendedThermostatStatus thermostatStatus) {
329                     int thermostatNumber = thermostatStatus.getNumber();
330
331                     logger.debug("Received status update for Thermostat: {}, status: {}", thermostatNumber,
332                             thermostatStatus);
333                     Optional<Thing> theThing = getChildThing(THING_TYPE_THERMOSTAT, thermostatNumber);
334                     theThing.map(Thing::getHandler)
335                             .ifPresent(theHandler -> ((ThermostatHandler) theHandler).handleStatus(thermostatStatus));
336                 } else if (status instanceof ExtendedAudioZoneStatus audioZoneStatus) {
337                     int audioZoneNumber = audioZoneStatus.getNumber();
338
339                     logger.debug("Received status update for Audio Zone: {}, status: {}", audioZoneNumber,
340                             audioZoneStatus);
341                     Optional<Thing> theThing = getChildThing(THING_TYPE_AUDIO_ZONE, audioZoneNumber);
342                     theThing.map(Thing::getHandler)
343                             .ifPresent(theHandler -> ((AudioZoneHandler) theHandler).handleStatus(audioZoneStatus));
344                 } else if (status instanceof ExtendedAuxSensorStatus auxSensorStatus) {
345                     int auxSensorNumber = auxSensorStatus.getNumber();
346
347                     // Aux Sensors can be either temperature or humidity, need to check both.
348                     Optional<Thing> tempThing = getChildThing(THING_TYPE_TEMP_SENSOR, auxSensorNumber);
349                     Optional<Thing> humidityThing = getChildThing(THING_TYPE_HUMIDITY_SENSOR, auxSensorNumber);
350                     if (tempThing.isPresent()) {
351                         logger.debug("Received status update for Temperature Sensor: {}, status: {}", auxSensorNumber,
352                                 auxSensorStatus);
353                         tempThing.map(Thing::getHandler).ifPresent(
354                                 theHandler -> ((TempSensorHandler) theHandler).handleStatus(auxSensorStatus));
355                     }
356                     if (humidityThing.isPresent()) {
357                         logger.debug("Received status update for Humidity Sensor: {}, status: {}", auxSensorNumber,
358                                 auxSensorStatus);
359                         humidityThing.map(Thing::getHandler).ifPresent(
360                                 theHandler -> ((HumiditySensorHandler) theHandler).handleStatus(auxSensorStatus));
361                     }
362                 } else {
363                     logger.debug("Received Object Status Notification that was not processed: {}", objectStatus);
364                 }
365             }
366         } else {
367             logger.debug("Received null Object Status Notification!");
368         }
369     }
370
371     @Override
372     public void systemEventNotification(@Nullable SystemEvent event) {
373         if (event != null) {
374             logger.debug("Received System Event Notification of type: {}", event.getType());
375             switch (event.getType()) {
376                 case PHONE_LINE_DEAD:
377                 case PHONE_LINE_OFF_HOOK:
378                 case PHONE_LINE_ON_HOOK:
379                 case PHONE_LINE_RING:
380                     ChannelUID channel = new ChannelUID(getThing().getUID(), TRIGGER_CHANNEL_PHONE_LINE_EVENT);
381                     triggerChannel(channel, event.getType().toString().replaceAll("^PHONE_LINE_", ""));
382                     break;
383                 case AC_POWER_OFF:
384                 case AC_POWER_RESTORED:
385                     ChannelUID acChannel = new ChannelUID(getThing().getUID(), TRIGGER_CHANNEL_AC_POWER_EVENT);
386                     triggerChannel(acChannel, event.getType().toString().replaceAll("^AC_POWER_", ""));
387                     break;
388                 case BATTERY_LOW:
389                 case BATTERY_OK:
390                     ChannelUID batteryChannel = new ChannelUID(getThing().getUID(), TRIGGER_CHANNEL_BATTERY_EVENT);
391                     triggerChannel(batteryChannel, event.getType().toString().replaceAll("^BATTERY_", ""));
392                     break;
393                 case DCM_OK:
394                 case DCM_TROUBLE:
395                     ChannelUID dcmChannel = new ChannelUID(getThing().getUID(), TRIGGER_CHANNEL_DCM_EVENT);
396                     triggerChannel(dcmChannel, event.getType().toString().replaceAll("^DCM_", ""));
397                     break;
398                 case ENERGY_COST_CRITICAL:
399                 case ENERGY_COST_HIGH:
400                 case ENERGY_COST_LOW:
401                 case ENERGY_COST_MID:
402                     ChannelUID energyChannel = new ChannelUID(getThing().getUID(), TRIGGER_CHANNEL_ENERGY_COST_EVENT);
403                     triggerChannel(energyChannel, event.getType().toString().replaceAll("^ENERGY_COST_", ""));
404                     break;
405                 case CAMERA_1_TRIGGER:
406                 case CAMERA_2_TRIGGER:
407                 case CAMERA_3_TRIGGER:
408                 case CAMERA_4_TRIGGER:
409                 case CAMERA_5_TRIGGER:
410                 case CAMERA_6_TRIGGER:
411                     ChannelUID cameraChannel = new ChannelUID(getThing().getUID(),
412                             TRIGGER_CHANNEL_CAMERA_TRIGGER_EVENT);
413                     triggerChannel(cameraChannel, String.valueOf(event.getType().toString().charAt(8)));
414                     break;
415                 case BUTTON:
416                     Optional<Thing> buttonThing = getChildThing(THING_TYPE_BUTTON,
417                             ((ButtonEvent) event).getButtonNumber());
418                     buttonThing.map(Thing::getHandler)
419                             .ifPresent(theHandler -> ((ButtonHandler) theHandler).buttonActivated());
420                     break;
421                 case ALL_ON_OFF:
422                     Optional<Thing> areaThing = getChildThing(THING_TYPE_OMNI_AREA, ((AllOnOffEvent) event).getArea());
423                     if (areaThing.isPresent()) {
424                         logger.debug("Thing for allOnOff event: {}", areaThing.get().getUID());
425                         areaThing.map(Thing::getHandler).ifPresent(theHandler -> ((AbstractAreaHandler) theHandler)
426                                 .handleAllOnOffEvent((AllOnOffEvent) event));
427                     }
428                     break;
429                 case UPB_LINK:
430                     UPBLinkEvent linkEvent = (UPBLinkEvent) event;
431                     UPBLinkEvent.Command command = linkEvent.getLinkCommand();
432                     int link = linkEvent.getLinkNumber();
433                     handleUPBLink(link, command);
434                     break;
435                 case ALC_UPB_RADIORA_STARLITE_SWITCH_PRESS:
436                     SwitchPressEvent switchPressEvent = (SwitchPressEvent) event;
437                     int unitNumber = switchPressEvent.getUnitNumber();
438
439                     Optional<Thing> unitThing = getUnitThing(unitNumber);
440                     unitThing.map(Thing::getHandler).ifPresent(
441                             theHandler -> ((UnitHandler) theHandler).handleSwitchPressEvent(switchPressEvent));
442                     break;
443                 default:
444                     logger.warn("Ignoring System Event Notification of type: {}", event.getType());
445             }
446         } else {
447             logger.debug("Received null System Event Notification!");
448         }
449     }
450
451     private void handleUPBLink(int link, UPBLinkEvent.Command command) {
452         final ChannelUID activateChannel;
453
454         if (command == UPBLinkEvent.Command.ACTIVATED) {
455             activateChannel = new ChannelUID(getThing().getUID(), TRIGGER_CHANNEL_UPB_LINK_ACTIVATED_EVENT);
456         } else if (command == UPBLinkEvent.Command.DEACTIVATED) {
457             activateChannel = new ChannelUID(getThing().getUID(), TRIGGER_CHANNEL_UPB_LINK_DEACTIVATED_EVENT);
458         } else {
459             logger.debug("Received unsupported UPB link event: {}", command);
460             return;
461         }
462         triggerChannel(activateChannel, Integer.toString(link));
463     }
464
465     @Override
466     public void notConnectedEvent(@Nullable Exception e) {
467         if (e != null) {
468             logger.debug("Received an OmniLink Controller not connected event: {}", e.getMessage());
469             setOfflineAndReconnect(e.getMessage());
470         }
471     }
472
473     private void getSystemStatus() throws IOException, OmniNotConnectedException, OmniInvalidResponseException,
474             OmniUnknownMessageTypeException {
475         SystemStatus status = getOmniConnection().reqSystemStatus();
476         logger.debug("Received system status: {}", status);
477         // Update controller's reported time
478         String dateString = new StringBuilder().append(2000 + status.getYear()).append("-")
479                 .append(String.format("%02d", status.getMonth())).append("-")
480                 .append(String.format("%02d", status.getDay())).append("T")
481                 .append(String.format("%02d", status.getHour())).append(":")
482                 .append(String.format("%02d", status.getMinute())).append(":")
483                 .append(String.format("%02d", status.getSecond())).toString();
484         updateState(CHANNEL_SYSTEM_DATE, new DateTimeType(dateString));
485     }
486
487     public Message reqObjectProperties(int objectType, int objectNum, int direction, int filter1, int filter2,
488             int filter3) throws OmniInvalidResponseException, OmniUnknownMessageTypeException, BridgeOfflineException {
489         try {
490             return getOmniConnection().reqObjectProperties(objectType, objectNum, direction, filter1, filter2, filter3);
491         } catch (OmniNotConnectedException | IOException e) {
492             setOfflineAndReconnect(e.getMessage());
493             throw new BridgeOfflineException(e);
494         }
495     }
496
497     public Message requestAudioSourceStatus(final int source, final int position)
498             throws OmniInvalidResponseException, OmniUnknownMessageTypeException, BridgeOfflineException {
499         try {
500             return getOmniConnection().reqAudioSourceStatus(source, position);
501         } catch (OmniNotConnectedException | IOException e) {
502             setOfflineAndReconnect(e.getMessage());
503             throw new BridgeOfflineException(e);
504         }
505     }
506
507     public ObjectStatus requestObjectStatus(final int objType, final int startObject, final int endObject,
508             boolean extended)
509             throws OmniInvalidResponseException, OmniUnknownMessageTypeException, BridgeOfflineException {
510         try {
511             return getOmniConnection().reqObjectStatus(objType, startObject, endObject, extended);
512         } catch (OmniNotConnectedException | IOException e) {
513             setOfflineAndReconnect(e.getMessage());
514             throw new BridgeOfflineException(e);
515         }
516     }
517
518     public Optional<TemperatureFormat> getTemperatureFormat() {
519         try {
520             return Optional.of(TemperatureFormat.valueOf(reqSystemFormats().getTempFormat()));
521         } catch (OmniInvalidResponseException | OmniUnknownMessageTypeException | BridgeOfflineException e) {
522             logger.debug("Could not request temperature format from controller: {}", e.getMessage());
523             return Optional.empty();
524         }
525     }
526
527     public void updateChannels() {
528         try {
529             getSystemStatus();
530             updateState(CHANNEL_CONSOLE_ENABLE_DISABLE_BEEPER, UnDefType.UNDEF);
531             updateState(CHANNEL_CONSOLE_BEEP, UnDefType.UNDEF);
532         } catch (IOException | OmniNotConnectedException | OmniInvalidResponseException
533                 | OmniUnknownMessageTypeException e) {
534             logger.warn("Unable to update bridge channels: {}", e.getMessage());
535         }
536     }
537
538     @Override
539     public void dispose() {
540         cancelReconnectJob(true);
541         cancelEventPolling();
542         final Connection connection = omniConnection;
543         if (connection != null) {
544             connection.removeDisconnectListener(this);
545             connection.disconnect();
546         }
547     }
548
549     private Optional<Thing> getChildThing(ThingTypeUID type, int number) {
550         Bridge bridge = getThing();
551         return bridge.getThings().stream().filter(t -> t.getThingTypeUID().equals(type))
552                 .filter(t -> ((Number) t.getConfiguration().get(THING_PROPERTIES_NUMBER)).intValue() == number)
553                 .findFirst();
554     }
555
556     private Optional<Thing> getUnitThing(int unitId) {
557         return SUPPORTED_UNIT_TYPES_UIDS.stream().map(uid -> getChildThing(uid, unitId)).flatMap(Optional::stream)
558                 .findFirst();
559     }
560
561     public Optional<AudioPlayer> getAudioPlayer() {
562         return audioPlayer;
563     }
564
565     public Message readEventRecord(int eventNumber, int direction)
566             throws OmniInvalidResponseException, OmniUnknownMessageTypeException, BridgeOfflineException {
567         try {
568             return getOmniConnection().readEventRecord(eventNumber, direction);
569         } catch (OmniNotConnectedException | IOException e) {
570             setOfflineAndReconnect(e.getMessage());
571             throw new BridgeOfflineException(e);
572         }
573     }
574
575     private void updateBridgeProperties() {
576         try {
577             SystemInformation systemInformation = reqSystemInformation();
578             Map<String, String> properties = editProperties();
579             properties.put(Thing.PROPERTY_MODEL_ID, Integer.toString(systemInformation.getModel()));
580             properties.put(Thing.PROPERTY_FIRMWARE_VERSION,
581                     Integer.toString(systemInformation.getMajor()) + "."
582                             + Integer.toString(systemInformation.getMinor()) + "."
583                             + Integer.toString(systemInformation.getRevision()));
584             properties.put(THING_PROPERTIES_PHONE_NUMBER, systemInformation.getPhone());
585             updateProperties(properties);
586         } catch (OmniInvalidResponseException | OmniUnknownMessageTypeException | BridgeOfflineException e) {
587             logger.debug("Could not request system information from OmniLink Controller: {}", e.getMessage());
588         }
589     }
590
591     @Override
592     public void initialize() {
593         scheduleReconnectJob();
594     }
595
596     private void scheduleReconnectJob() {
597         ScheduledFuture<?> currentReconnectJob = connectJob;
598         if (currentReconnectJob == null || currentReconnectJob.isDone()) {
599             connectJob = super.scheduler.scheduleWithFixedDelay(this::makeOmnilinkConnection, 0, autoReconnectPeriod,
600                     TimeUnit.SECONDS);
601         }
602     }
603
604     private void cancelReconnectJob(boolean kill) {
605         ScheduledFuture<?> currentReconnectJob = connectJob;
606         if (currentReconnectJob != null) {
607             currentReconnectJob.cancel(kill);
608         }
609     }
610
611     private void setOfflineAndReconnect(@Nullable String message) {
612         updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, message);
613         cancelEventPolling();
614         final Connection connection = omniConnection;
615         if (connection != null) {
616             connection.removeDisconnectListener(this);
617         }
618         scheduleReconnectJob();
619     }
620
621     private void startEventPolling(int interval) {
622         ScheduledFuture<?> eventPollingJobFuture = eventPollingJob;
623         if (eventPollingJobFuture == null || eventPollingJobFuture.isDone()) {
624             eventLogNumber = 0;
625             eventPollingJob = super.scheduler.scheduleWithFixedDelay(this::pollEvents, 0, interval, TimeUnit.SECONDS);
626         }
627     }
628
629     private void cancelEventPolling() {
630         ScheduledFuture<?> eventPollingJobFuture = eventPollingJob;
631         if (eventPollingJobFuture != null) {
632             eventPollingJobFuture.cancel(true);
633         }
634     }
635
636     private void pollEvents() {
637         // On first run, direction is -1 (most recent event), after its 1 for the next log message
638         try {
639             Message message;
640             do {
641                 logger.trace("Polling for event log messages.");
642                 int direction = eventLogNumber == 0 ? -1 : 1;
643                 message = readEventRecord(eventLogNumber, direction);
644                 if (message.getMessageType() == Message.MESG_TYPE_EVENT_LOG_DATA) {
645                     EventLogData logData = (EventLogData) message;
646                     logger.debug("Processing event log message number: {}", logData.getEventNumber());
647                     eventLogNumber = logData.getEventNumber();
648                     String json = gson.toJson(logData);
649                     logger.debug("Receieved event log message: {}", json);
650                     updateState(CHANNEL_EVENT_LOG, new StringType(json));
651                 }
652             } while (message.getMessageType() != Message.MESG_TYPE_END_OF_DATA);
653
654         } catch (OmniInvalidResponseException | OmniUnknownMessageTypeException | BridgeOfflineException e) {
655             logger.debug("Exception recieved while polling for event log messages: {}", e.getMessage());
656         }
657     }
658
659     private Connection getOmniConnection() throws OmniNotConnectedException {
660         final Connection connection = omniConnection;
661         if (connection != null) {
662             return connection;
663         } else {
664             throw new OmniNotConnectedException("Connection not yet established!");
665         }
666     }
667 }