]> git.basschouten.com Git - openhab-addons.git/blob
da710cc108f6179d1db3b32605415f89a1033698
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2020 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.nuvo.internal.handler;
14
15 import static org.openhab.binding.nuvo.internal.NuvoBindingConstants.*;
16
17 import java.math.BigDecimal;
18 import java.text.SimpleDateFormat;
19 import java.util.ArrayList;
20 import java.util.Collection;
21 import java.util.Collections;
22 import java.util.Date;
23 import java.util.HashSet;
24 import java.util.List;
25 import java.util.Set;
26 import java.util.concurrent.ScheduledFuture;
27 import java.util.concurrent.TimeUnit;
28 import java.util.regex.Matcher;
29 import java.util.regex.Pattern;
30 import java.util.stream.Collectors;
31 import java.util.stream.IntStream;
32
33 import javax.measure.Unit;
34 import javax.measure.quantity.Time;
35
36 import org.eclipse.jdt.annotation.NonNullByDefault;
37 import org.eclipse.jdt.annotation.Nullable;
38 import org.openhab.binding.nuvo.internal.NuvoException;
39 import org.openhab.binding.nuvo.internal.NuvoStateDescriptionOptionProvider;
40 import org.openhab.binding.nuvo.internal.NuvoThingActions;
41 import org.openhab.binding.nuvo.internal.communication.NuvoCommand;
42 import org.openhab.binding.nuvo.internal.communication.NuvoConnector;
43 import org.openhab.binding.nuvo.internal.communication.NuvoDefaultConnector;
44 import org.openhab.binding.nuvo.internal.communication.NuvoEnum;
45 import org.openhab.binding.nuvo.internal.communication.NuvoIpConnector;
46 import org.openhab.binding.nuvo.internal.communication.NuvoMessageEvent;
47 import org.openhab.binding.nuvo.internal.communication.NuvoMessageEventListener;
48 import org.openhab.binding.nuvo.internal.communication.NuvoSerialConnector;
49 import org.openhab.binding.nuvo.internal.communication.NuvoStatusCodes;
50 import org.openhab.binding.nuvo.internal.configuration.NuvoThingConfiguration;
51 import org.openhab.core.io.transport.serial.SerialPortManager;
52 import org.openhab.core.library.types.DecimalType;
53 import org.openhab.core.library.types.NextPreviousType;
54 import org.openhab.core.library.types.OnOffType;
55 import org.openhab.core.library.types.OpenClosedType;
56 import org.openhab.core.library.types.PercentType;
57 import org.openhab.core.library.types.PlayPauseType;
58 import org.openhab.core.library.types.QuantityType;
59 import org.openhab.core.library.types.StringType;
60 import org.openhab.core.library.unit.SmartHomeUnits;
61 import org.openhab.core.thing.Channel;
62 import org.openhab.core.thing.ChannelUID;
63 import org.openhab.core.thing.Thing;
64 import org.openhab.core.thing.ThingStatus;
65 import org.openhab.core.thing.ThingStatusDetail;
66 import org.openhab.core.thing.binding.BaseThingHandler;
67 import org.openhab.core.thing.binding.ThingHandlerService;
68 import org.openhab.core.types.Command;
69 import org.openhab.core.types.State;
70 import org.openhab.core.types.StateOption;
71 import org.openhab.core.types.UnDefType;
72 import org.slf4j.Logger;
73 import org.slf4j.LoggerFactory;
74
75 /**
76  * The {@link NuvoHandler} is responsible for handling commands, which are sent to one of the channels.
77  *
78  * Based on the Rotel binding by Laurent Garnier
79  *
80  * @author Michael Lobstein - Initial contribution
81  */
82 @NonNullByDefault
83 public class NuvoHandler extends BaseThingHandler implements NuvoMessageEventListener {
84     private static final long RECON_POLLING_INTERVAL_SEC = 60;
85     private static final long POLLING_INTERVAL_SEC = 30;
86     private static final long CLOCK_SYNC_INTERVAL_SEC = 3600;
87     private static final long INITIAL_POLLING_DELAY_SEC = 30;
88     private static final long INITIAL_CLOCK_SYNC_DELAY_SEC = 10;
89     // spec says wait 50ms, min is 100
90     private static final long SLEEP_BETWEEN_CMD_MS = 100;
91     private static final Unit<Time> API_SECOND_UNIT = SmartHomeUnits.SECOND;
92
93     private static final String ZONE = "ZONE";
94     private static final String SOURCE = "SOURCE";
95     private static final String CHANNEL_DELIMIT = "#";
96     private static final String UNDEF = "UNDEF";
97     private static final String GC_STR = "NV-IG8";
98
99     private static final int MAX_ZONES = 20;
100     private static final int MAX_SRC = 6;
101     private static final int MIN_VOLUME = 0;
102     private static final int MAX_VOLUME = 79;
103     private static final int MIN_EQ = -18;
104     private static final int MAX_EQ = 18;
105
106     private static final Pattern ZONE_PATTERN = Pattern
107             .compile("^ON,SRC(\\d{1}),(MUTE|VOL\\d{1,2}),DND([0-1]),LOCK([0-1])$");
108     private static final Pattern DISP_PATTERN = Pattern.compile("^DISPLINE(\\d{1}),\"(.*)\"$");
109     private static final Pattern DISP_INFO_PATTERN = Pattern
110             .compile("^DISPINFO,DUR(\\d{1,6}),POS(\\d{1,6}),STATUS(\\d{1,2})$");
111     private static final Pattern ZONE_CFG_PATTERN = Pattern.compile("^BASS(.*),TREB(.*),BAL(.*),LOUDCMP([0-1])$");
112
113     private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy,MM,dd,HH,mm");
114
115     private final Logger logger = LoggerFactory.getLogger(NuvoHandler.class);
116     private final NuvoStateDescriptionOptionProvider stateDescriptionProvider;
117     private final SerialPortManager serialPortManager;
118
119     private @Nullable ScheduledFuture<?> reconnectJob;
120     private @Nullable ScheduledFuture<?> pollingJob;
121     private @Nullable ScheduledFuture<?> clockSyncJob;
122
123     private NuvoConnector connector = new NuvoDefaultConnector();
124     private long lastEventReceived = System.currentTimeMillis();
125     private int numZones = 1;
126     private String versionString = BLANK;
127     private boolean isGConcerto = false;
128     private Object sequenceLock = new Object();
129
130     Set<Integer> activeZones = new HashSet<>(1);
131
132     // A state option list for the source labels
133     List<StateOption> sourceLabels = new ArrayList<>();
134
135     /**
136      * Constructor
137      */
138     public NuvoHandler(Thing thing, NuvoStateDescriptionOptionProvider stateDescriptionProvider,
139             SerialPortManager serialPortManager) {
140         super(thing);
141         this.stateDescriptionProvider = stateDescriptionProvider;
142         this.serialPortManager = serialPortManager;
143     }
144
145     @Override
146     public void initialize() {
147         final String uid = this.getThing().getUID().getAsString();
148         NuvoThingConfiguration config = getConfigAs(NuvoThingConfiguration.class);
149         final String serialPort = config.serialPort;
150         final String host = config.host;
151         final Integer port = config.port;
152         final Integer numZones = config.numZones;
153
154         // Check configuration settings
155         String configError = null;
156         if ((serialPort == null || serialPort.isEmpty()) && (host == null || host.isEmpty())) {
157             configError = "undefined serialPort and host configuration settings; please set one of them";
158         } else if (serialPort != null && (host == null || host.isEmpty())) {
159             if (serialPort.toLowerCase().startsWith("rfc2217")) {
160                 configError = "use host and port configuration settings for a serial over IP connection";
161             }
162         } else {
163             if (port == null) {
164                 configError = "undefined port configuration setting";
165             } else if (port <= 0) {
166                 configError = "invalid port configuration setting";
167             }
168         }
169
170         if (configError != null) {
171             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, configError);
172             return;
173         }
174
175         if (serialPort != null) {
176             connector = new NuvoSerialConnector(serialPortManager, serialPort, uid);
177         } else if (port != null) {
178             connector = new NuvoIpConnector(host, port, uid);
179         } else {
180             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
181                     "Either Serial port or Host & Port must be specifed");
182             return;
183         }
184
185         if (numZones != null) {
186             this.numZones = numZones;
187         }
188
189         activeZones = IntStream.range((1), (this.numZones + 1)).boxed().collect(Collectors.toSet());
190
191         // remove the channels for the zones we are not using
192         if (this.numZones < MAX_ZONES) {
193             List<Channel> channels = new ArrayList<>(this.getThing().getChannels());
194
195             List<Integer> zonesToRemove = IntStream.range((this.numZones + 1), (MAX_ZONES + 1)).boxed()
196                     .collect(Collectors.toList());
197
198             zonesToRemove.forEach(zone -> channels.removeIf(c -> (c.getUID().getId().contains("zone" + zone))));
199             updateThing(editThing().withChannels(channels).build());
200         }
201
202         if (config.clockSync) {
203             scheduleClockSyncJob();
204         }
205
206         scheduleReconnectJob();
207         schedulePollingJob();
208         updateStatus(ThingStatus.UNKNOWN);
209     }
210
211     @Override
212     public void dispose() {
213         cancelReconnectJob();
214         cancelPollingJob();
215         cancelClockSyncJob();
216         closeConnection();
217         super.dispose();
218     }
219
220     @Override
221     public Collection<Class<? extends ThingHandlerService>> getServices() {
222         return Collections.singletonList(NuvoThingActions.class);
223     }
224
225     public void handleRawCommand(@Nullable String command) {
226         synchronized (sequenceLock) {
227             try {
228                 connector.sendCommand(command);
229             } catch (NuvoException e) {
230                 logger.warn("Nuvo Command: {} failed", command);
231             }
232         }
233     }
234
235     /**
236      * Handle a command the UI
237      *
238      * @param channelUID the channel sending the command
239      * @param command the command received
240      * 
241      */
242     @Override
243     public void handleCommand(ChannelUID channelUID, Command command) {
244         String channel = channelUID.getId();
245         String[] channelSplit = channel.split(CHANNEL_DELIMIT);
246         NuvoEnum target = NuvoEnum.valueOf(channelSplit[0].toUpperCase());
247
248         String channelType = channelSplit[1];
249
250         if (getThing().getStatus() != ThingStatus.ONLINE) {
251             logger.debug("Thing is not ONLINE; command {} from channel {} is ignored", command, channel);
252             return;
253         }
254
255         synchronized (sequenceLock) {
256             if (!connector.isConnected()) {
257                 logger.warn("Command {} from channel {} is ignored: connection not established", command, channel);
258                 return;
259             }
260
261             try {
262                 switch (channelType) {
263                     case CHANNEL_TYPE_POWER:
264                         if (command instanceof OnOffType) {
265                             connector.sendCommand(target, command == OnOffType.ON ? NuvoCommand.ON : NuvoCommand.OFF);
266                         }
267                         break;
268                     case CHANNEL_TYPE_SOURCE:
269                         if (command instanceof DecimalType) {
270                             int value = ((DecimalType) command).intValue();
271                             if (value >= 1 && value <= MAX_SRC) {
272                                 logger.debug("Got source command {} zone {}", value, target);
273                                 connector.sendCommand(target, NuvoCommand.SOURCE, String.valueOf(value));
274                             }
275                         }
276                         break;
277                     case CHANNEL_TYPE_VOLUME:
278                         if (command instanceof PercentType) {
279                             int value = (MAX_VOLUME
280                                     - (int) Math.round(
281                                             ((PercentType) command).doubleValue() / 100.0 * (MAX_VOLUME - MIN_VOLUME))
282                                     + MIN_VOLUME);
283                             logger.debug("Got volume command {} zone {}", value, target);
284                             connector.sendCommand(target, NuvoCommand.VOLUME, String.valueOf(value));
285                         }
286                         break;
287                     case CHANNEL_TYPE_MUTE:
288                         if (command instanceof OnOffType) {
289                             connector.sendCommand(target,
290                                     command == OnOffType.ON ? NuvoCommand.MUTE_ON : NuvoCommand.MUTE_OFF);
291                         }
292                         break;
293                     case CHANNEL_TYPE_TREBLE:
294                         if (command instanceof DecimalType) {
295                             int value = ((DecimalType) command).intValue();
296                             if (value >= MIN_EQ && value <= MAX_EQ) {
297                                 // device can only accept even values
298                                 if (value % 2 == 1)
299                                     value++;
300                                 logger.debug("Got treble command {} zone {}", value, target);
301                                 connector.sendCfgCommand(target, NuvoCommand.TREBLE, String.valueOf(value));
302                             }
303                         }
304                         break;
305                     case CHANNEL_TYPE_BASS:
306                         if (command instanceof DecimalType) {
307                             int value = ((DecimalType) command).intValue();
308                             if (value >= MIN_EQ && value <= MAX_EQ) {
309                                 if (value % 2 == 1)
310                                     value++;
311                                 logger.debug("Got bass command {} zone {}", value, target);
312                                 connector.sendCfgCommand(target, NuvoCommand.BASS, String.valueOf(value));
313                             }
314                         }
315                         break;
316                     case CHANNEL_TYPE_BALANCE:
317                         if (command instanceof DecimalType) {
318                             int value = ((DecimalType) command).intValue();
319                             if (value >= MIN_EQ && value <= MAX_EQ) {
320                                 if (value % 2 == 1)
321                                     value++;
322                                 logger.debug("Got balance command {} zone {}", value, target);
323                                 connector.sendCfgCommand(target, NuvoCommand.BALANCE,
324                                         NuvoStatusCodes.getBalanceFromInt(value));
325                             }
326                         }
327                         break;
328                     case CHANNEL_TYPE_LOUDNESS:
329                         if (command instanceof OnOffType) {
330                             connector.sendCfgCommand(target, NuvoCommand.LOUDNESS,
331                                     command == OnOffType.ON ? ONE : ZERO);
332                         }
333                         break;
334                     case CHANNEL_TYPE_CONTROL:
335                         handleControlCommand(target, command);
336                         break;
337                     case CHANNEL_TYPE_DND:
338                         if (command instanceof OnOffType) {
339                             connector.sendCommand(target,
340                                     command == OnOffType.ON ? NuvoCommand.DND_ON : NuvoCommand.DND_OFF);
341                         }
342                         break;
343                     case CHANNEL_TYPE_PARTY:
344                         if (command instanceof OnOffType) {
345                             connector.sendCommand(target,
346                                     command == OnOffType.ON ? NuvoCommand.PARTY_ON : NuvoCommand.PARTY_OFF);
347                         }
348                         break;
349                     case CHANNEL_DISPLAY_LINE1:
350                         if (command instanceof StringType) {
351                             connector.sendCommand(target, NuvoCommand.DISPLINE1, "\"" + command + "\"");
352                         }
353                         break;
354                     case CHANNEL_DISPLAY_LINE2:
355                         if (command instanceof StringType) {
356                             connector.sendCommand(target, NuvoCommand.DISPLINE2, "\"" + command + "\"");
357                         }
358                         break;
359                     case CHANNEL_DISPLAY_LINE3:
360                         if (command instanceof StringType) {
361                             connector.sendCommand(target, NuvoCommand.DISPLINE3, "\"" + command + "\"");
362                         }
363                         break;
364                     case CHANNEL_DISPLAY_LINE4:
365                         if (command instanceof StringType) {
366                             connector.sendCommand(target, NuvoCommand.DISPLINE4, "\"" + command + "\"");
367                         }
368                         break;
369                     case CHANNEL_TYPE_ALLOFF:
370                         if (command instanceof OnOffType) {
371                             connector.sendCommand(NuvoCommand.ALLOFF);
372                         }
373                         break;
374                     case CHANNEL_TYPE_ALLMUTE:
375                         if (command instanceof OnOffType) {
376                             connector.sendCommand(target,
377                                     command == OnOffType.ON ? NuvoCommand.ALLMUTE_ON : NuvoCommand.ALLMUTE_OFF);
378                         }
379                         break;
380                     case CHANNEL_TYPE_PAGE:
381                         if (command instanceof OnOffType) {
382                             connector.sendCommand(target,
383                                     command == OnOffType.ON ? NuvoCommand.PAGE_ON : NuvoCommand.PAGE_OFF);
384                         }
385                         break;
386                 }
387             } catch (NuvoException e) {
388                 logger.warn("Command {} from channel {} failed: {}", command, channel, e.getMessage());
389                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Sending command failed");
390                 closeConnection();
391                 scheduleReconnectJob();
392             }
393         }
394     }
395
396     /**
397      * Open the connection with the Nuvo device
398      *
399      * @return true if the connection is opened successfully or false if not
400      */
401     private synchronized boolean openConnection() {
402         connector.addEventListener(this);
403         try {
404             connector.open();
405         } catch (NuvoException e) {
406             logger.debug("openConnection() failed: {}", e.getMessage());
407         }
408         logger.debug("openConnection(): {}", connector.isConnected() ? "connected" : "disconnected");
409         return connector.isConnected();
410     }
411
412     /**
413      * Close the connection with the Nuvo device
414      */
415     private synchronized void closeConnection() {
416         if (connector.isConnected()) {
417             connector.close();
418             connector.removeEventListener(this);
419             logger.debug("closeConnection(): disconnected");
420         }
421     }
422
423     /**
424      * Handle an event received from the Nuvo device
425      *
426      * @param event the event to process
427      */
428     @Override
429     public void onNewMessageEvent(NuvoMessageEvent evt) {
430         logger.debug("onNewMessageEvent: key {} = {}", evt.getKey(), evt.getValue());
431         lastEventReceived = System.currentTimeMillis();
432
433         String type = evt.getType();
434         String key = evt.getKey();
435         String updateData = evt.getValue().trim();
436         if (this.getThing().getStatus() == ThingStatus.OFFLINE) {
437             updateStatus(ThingStatus.ONLINE, ThingStatusDetail.NONE, this.versionString);
438         }
439
440         switch (type) {
441             case TYPE_VERSION:
442                 this.versionString = updateData;
443                 // Determine if we are a Grand Concerto or not
444                 if (this.versionString.contains(GC_STR)) {
445                     this.isGConcerto = true;
446                     connector.setEssentia(false);
447                 }
448                 break;
449             case TYPE_ALLOFF:
450                 activeZones.forEach(zoneNum -> {
451                     updateChannelState(NuvoEnum.valueOf(ZONE + zoneNum), CHANNEL_TYPE_POWER, OFF);
452                 });
453                 break;
454             case TYPE_ALLMUTE:
455                 updateChannelState(NuvoEnum.SYSTEM, CHANNEL_TYPE_ALLMUTE, ONE.equals(updateData) ? ON : OFF);
456                 activeZones.forEach(zoneNum -> {
457                     updateChannelState(NuvoEnum.valueOf(ZONE + zoneNum), CHANNEL_TYPE_MUTE,
458                             ONE.equals(updateData) ? ON : OFF);
459                 });
460                 break;
461             case TYPE_PAGE:
462                 updateChannelState(NuvoEnum.SYSTEM, CHANNEL_TYPE_PAGE, ONE.equals(updateData) ? ON : OFF);
463                 break;
464             case TYPE_SOURCE_UPDATE:
465                 logger.debug("Source update: Source: {} - Value: {}", key, updateData);
466                 NuvoEnum targetSource = NuvoEnum.valueOf(SOURCE + key);
467
468                 if (updateData.contains(DISPLINE)) {
469                     // example: DISPLINE2,"Play My Song (Featuring Dee Ajayi)"
470                     Matcher matcher = DISP_PATTERN.matcher(updateData);
471                     if (matcher.find()) {
472                         updateChannelState(targetSource, CHANNEL_DISPLAY_LINE + matcher.group(1), matcher.group(2));
473                     } else {
474                         logger.debug("no match on message: {}", updateData);
475                     }
476                 } else if (updateData.contains(DISPINFO)) {
477                     // example: DISPINFO,DUR0,POS70,STATUS2 (DUR and POS are expressed in tenths of a second)
478                     // 6 places(tenths of a second)-> max 999,999 /10/60/60/24 = 1.15 days
479                     Matcher matcher = DISP_INFO_PATTERN.matcher(updateData);
480                     if (matcher.find()) {
481                         updateChannelState(targetSource, CHANNEL_TRACK_LENGTH, matcher.group(1));
482                         updateChannelState(targetSource, CHANNEL_TRACK_POSITION, matcher.group(2));
483                         updateChannelState(targetSource, CHANNEL_PLAY_MODE, matcher.group(3));
484                     } else {
485                         logger.debug("no match on message: {}", updateData);
486                     }
487                 } else if (updateData.contains(NAME_QUOTE) && sourceLabels.size() <= MAX_SRC) {
488                     // example: NAME"Ipod"
489                     String name = updateData.split("\"")[1];
490                     sourceLabels.add(new StateOption(key, name));
491                 }
492                 break;
493             case TYPE_ZONE_UPDATE:
494                 logger.debug("Zone update: Zone: {} - Value: {}", key, updateData);
495                 // example : OFF
496                 // or: ON,SRC3,VOL63,DND0,LOCK0
497                 // or: ON,SRC3,MUTE,DND0,LOCK0
498
499                 NuvoEnum targetZone = NuvoEnum.valueOf(ZONE + key);
500
501                 if (OFF.equals(updateData)) {
502                     updateChannelState(targetZone, CHANNEL_TYPE_POWER, OFF);
503                     updateChannelState(targetZone, CHANNEL_TYPE_SOURCE, UNDEF);
504                 } else {
505                     Matcher matcher = ZONE_PATTERN.matcher(updateData);
506                     if (matcher.find()) {
507                         updateChannelState(targetZone, CHANNEL_TYPE_POWER, ON);
508                         updateChannelState(targetZone, CHANNEL_TYPE_SOURCE, matcher.group(1));
509
510                         if (MUTE.equals(matcher.group(2))) {
511                             updateChannelState(targetZone, CHANNEL_TYPE_MUTE, ON);
512                         } else {
513                             updateChannelState(targetZone, CHANNEL_TYPE_MUTE, NuvoCommand.OFF.getValue());
514                             updateChannelState(targetZone, CHANNEL_TYPE_VOLUME, matcher.group(2).replace(VOL, BLANK));
515                         }
516
517                         updateChannelState(targetZone, CHANNEL_TYPE_DND, ONE.equals(matcher.group(3)) ? ON : OFF);
518                         updateChannelState(targetZone, CHANNEL_TYPE_LOCK, ONE.equals(matcher.group(4)) ? ON : OFF);
519                     } else {
520                         logger.debug("no match on message: {}", updateData);
521                     }
522                 }
523                 break;
524             case TYPE_ZONE_BUTTON:
525                 logger.debug("Zone Button pressed: Source: {} - Button: {}", key, updateData);
526                 updateChannelState(NuvoEnum.valueOf(SOURCE + key), CHANNEL_BUTTON_PRESS, updateData);
527                 break;
528             case TYPE_ZONE_CONFIG:
529                 logger.debug("Zone Configuration: Zone: {} - Value: {}", key, updateData);
530                 // example: BASS1,TREB-2,BALR2,LOUDCMP1
531                 Matcher matcher = ZONE_CFG_PATTERN.matcher(updateData);
532                 if (matcher.find()) {
533                     updateChannelState(NuvoEnum.valueOf(ZONE + key), CHANNEL_TYPE_BASS, matcher.group(1));
534                     updateChannelState(NuvoEnum.valueOf(ZONE + key), CHANNEL_TYPE_TREBLE, matcher.group(2));
535                     updateChannelState(NuvoEnum.valueOf(ZONE + key), CHANNEL_TYPE_BALANCE,
536                             NuvoStatusCodes.getBalanceFromStr(matcher.group(3)));
537                     updateChannelState(NuvoEnum.valueOf(ZONE + key), CHANNEL_TYPE_LOUDNESS,
538                             ONE.equals(matcher.group(4)) ? ON : OFF);
539                 } else {
540                     logger.debug("no match on message: {}", updateData);
541                 }
542                 break;
543             default:
544                 logger.debug("onNewMessageEvent: unhandled key {}", key);
545                 break;
546         }
547     }
548
549     /**
550      * Schedule the reconnection job
551      */
552     private void scheduleReconnectJob() {
553         logger.debug("Schedule reconnect job");
554         cancelReconnectJob();
555         reconnectJob = scheduler.scheduleWithFixedDelay(() -> {
556             if (!connector.isConnected()) {
557                 logger.debug("Trying to reconnect...");
558                 closeConnection();
559                 String error = null;
560                 if (openConnection()) {
561                     synchronized (sequenceLock) {
562                         try {
563                             long prevUpdateTime = lastEventReceived;
564
565                             connector.sendCommand(NuvoCommand.GET_CONTROLLER_VERSION);
566
567                             NuvoEnum.VALID_SOURCES.forEach(source -> {
568                                 try {
569                                     connector.sendQuery(NuvoEnum.valueOf(source), NuvoCommand.NAME);
570                                     Thread.sleep(SLEEP_BETWEEN_CMD_MS);
571                                     connector.sendQuery(NuvoEnum.valueOf(source), NuvoCommand.DISPINFO);
572                                     Thread.sleep(SLEEP_BETWEEN_CMD_MS);
573                                     connector.sendQuery(NuvoEnum.valueOf(source), NuvoCommand.DISPLINE);
574                                     Thread.sleep(SLEEP_BETWEEN_CMD_MS);
575                                 } catch (NuvoException | InterruptedException e) {
576                                     logger.debug("Error Querying Source data: {}", e.getMessage());
577                                 }
578                             });
579
580                             // Query all active zones to get their current status and eq configuration
581                             activeZones.forEach(zoneNum -> {
582                                 try {
583                                     connector.sendQuery(NuvoEnum.valueOf(ZONE + zoneNum), NuvoCommand.STATUS);
584                                     Thread.sleep(SLEEP_BETWEEN_CMD_MS);
585                                     connector.sendCfgCommand(NuvoEnum.valueOf(ZONE + zoneNum), NuvoCommand.EQ_QUERY,
586                                             BLANK);
587                                     Thread.sleep(SLEEP_BETWEEN_CMD_MS);
588                                 } catch (NuvoException | InterruptedException e) {
589                                     logger.debug("Error Querying Zone data: {}", e.getMessage());
590                                 }
591                             });
592
593                             // prevUpdateTime should have changed if a zone update was received
594                             if (prevUpdateTime == lastEventReceived) {
595                                 error = "Controller not responding to status requests";
596                             } else {
597                                 // Put the source labels on all active zones
598                                 activeZones.forEach(zoneNum -> {
599                                     stateDescriptionProvider.setStateOptions(new ChannelUID(getThing().getUID(),
600                                             ZONE.toLowerCase() + zoneNum + CHANNEL_DELIMIT + CHANNEL_TYPE_SOURCE),
601                                             sourceLabels);
602                                 });
603                             }
604                         } catch (NuvoException e) {
605                             error = "First command after connection failed";
606                             logger.debug("{}: {}", error, e.getMessage());
607                         }
608                     }
609                 } else {
610                     error = "Reconnection failed";
611                 }
612                 if (error != null) {
613                     updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, error);
614                     closeConnection();
615                 } else {
616                     updateStatus(ThingStatus.ONLINE, ThingStatusDetail.NONE, this.versionString);
617                 }
618             }
619         }, 1, RECON_POLLING_INTERVAL_SEC, TimeUnit.SECONDS);
620     }
621
622     /**
623      * Cancel the reconnection job
624      */
625     private void cancelReconnectJob() {
626         ScheduledFuture<?> reconnectJob = this.reconnectJob;
627         if (reconnectJob != null) {
628             reconnectJob.cancel(true);
629             this.reconnectJob = null;
630         }
631     }
632
633     /**
634      * Schedule the polling job
635      */
636     private void schedulePollingJob() {
637         logger.debug("Schedule polling job");
638         cancelPollingJob();
639
640         // when the Nuvo amp is off, this will keep the connection (esp Serial over IP) alive and detect if the
641         // connection goes down
642         pollingJob = scheduler.scheduleWithFixedDelay(() -> {
643             if (connector.isConnected()) {
644                 logger.debug("Polling the component for updated status...");
645
646                 synchronized (sequenceLock) {
647                     try {
648                         connector.sendCommand(NuvoCommand.GET_CONTROLLER_VERSION);
649                     } catch (NuvoException e) {
650                         logger.debug("Polling error: {}", e.getMessage());
651                     }
652
653                     // if the last event received was more than 1.25 intervals ago,
654                     // the component is not responding even though the connection is still good
655                     if ((System.currentTimeMillis() - lastEventReceived) > (POLLING_INTERVAL_SEC * 1.25 * 1000)) {
656                         logger.debug("Component not responding to status requests");
657                         updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
658                                 "Component not responding to status requests");
659                         closeConnection();
660                         scheduleReconnectJob();
661                     }
662                 }
663             }
664         }, INITIAL_POLLING_DELAY_SEC, POLLING_INTERVAL_SEC, TimeUnit.SECONDS);
665     }
666
667     /**
668      * Cancel the polling job
669      */
670     private void cancelPollingJob() {
671         ScheduledFuture<?> pollingJob = this.pollingJob;
672         if (pollingJob != null) {
673             pollingJob.cancel(true);
674             this.pollingJob = null;
675         }
676     }
677
678     /**
679      * Schedule the clock sync job
680      */
681     private void scheduleClockSyncJob() {
682         logger.debug("Schedule clock sync job");
683         cancelClockSyncJob();
684         clockSyncJob = scheduler.scheduleWithFixedDelay(() -> {
685             if (this.isGConcerto) {
686                 try {
687                     connector.sendCommand(NuvoCommand.CFGTIME.getValue() + DATE_FORMAT.format(new Date()));
688                 } catch (NuvoException e) {
689                     logger.debug("Error syncing clock: {}", e.getMessage());
690                 }
691             } else {
692                 this.cancelClockSyncJob();
693             }
694         }, INITIAL_CLOCK_SYNC_DELAY_SEC, CLOCK_SYNC_INTERVAL_SEC, TimeUnit.SECONDS);
695     }
696
697     /**
698      * Cancel the clock sync job
699      */
700     private void cancelClockSyncJob() {
701         ScheduledFuture<?> clockSyncJob = this.clockSyncJob;
702         if (clockSyncJob != null) {
703             clockSyncJob.cancel(true);
704             this.clockSyncJob = null;
705         }
706     }
707
708     /**
709      * Update the state of a channel
710      *
711      * @param target the channel group
712      * @param channelType the channel group item
713      * @param value the value to be updated
714      */
715     private void updateChannelState(NuvoEnum target, String channelType, String value) {
716         String channel = target.name().toLowerCase() + CHANNEL_DELIMIT + channelType;
717
718         if (!isLinked(channel)) {
719             return;
720         }
721
722         State state = UnDefType.UNDEF;
723
724         if (UNDEF.equals(value)) {
725             updateState(channel, state);
726             return;
727         }
728
729         switch (channelType) {
730             case CHANNEL_TYPE_POWER:
731             case CHANNEL_TYPE_MUTE:
732             case CHANNEL_TYPE_DND:
733             case CHANNEL_TYPE_PARTY:
734             case CHANNEL_TYPE_ALLMUTE:
735             case CHANNEL_TYPE_PAGE:
736             case CHANNEL_TYPE_LOUDNESS:
737                 state = ON.equals(value) ? OnOffType.ON : OnOffType.OFF;
738                 break;
739             case CHANNEL_TYPE_LOCK:
740                 state = ON.equals(value) ? OpenClosedType.OPEN : OpenClosedType.CLOSED;
741                 break;
742             case CHANNEL_TYPE_SOURCE:
743             case CHANNEL_TYPE_TREBLE:
744             case CHANNEL_TYPE_BASS:
745             case CHANNEL_TYPE_BALANCE:
746                 state = new DecimalType(value);
747                 break;
748             case CHANNEL_TYPE_VOLUME:
749                 int volume = Integer.parseInt(value);
750                 long volumePct = Math
751                         .round((double) (MAX_VOLUME - volume) / (double) (MAX_VOLUME - MIN_VOLUME) * 100.0);
752                 state = new PercentType(BigDecimal.valueOf(volumePct));
753                 break;
754             case CHANNEL_DISPLAY_LINE1:
755             case CHANNEL_DISPLAY_LINE2:
756             case CHANNEL_DISPLAY_LINE3:
757             case CHANNEL_DISPLAY_LINE4:
758             case CHANNEL_BUTTON_PRESS:
759                 state = new StringType(value);
760                 break;
761             case CHANNEL_PLAY_MODE:
762                 state = new StringType(NuvoStatusCodes.PLAY_MODE.get(value));
763                 break;
764             case CHANNEL_TRACK_LENGTH:
765             case CHANNEL_TRACK_POSITION:
766                 state = new QuantityType<Time>(Integer.parseInt(value) / 10, NuvoHandler.API_SECOND_UNIT);
767                 break;
768             default:
769                 break;
770         }
771         updateState(channel, state);
772     }
773
774     /**
775      * Handle a button press from a UI Player item
776      *
777      * @param target the nuvo zone to receive the command
778      * @param command the button press command to send to the zone
779      */
780     private void handleControlCommand(NuvoEnum target, Command command) throws NuvoException {
781         if (command instanceof PlayPauseType) {
782             connector.sendCommand(target, NuvoCommand.PLAYPAUSE);
783         } else if (command instanceof NextPreviousType) {
784             if (command == NextPreviousType.NEXT) {
785                 connector.sendCommand(target, NuvoCommand.NEXT);
786             } else if (command == NextPreviousType.PREVIOUS) {
787                 connector.sendCommand(target, NuvoCommand.PREV);
788             }
789         } else {
790             logger.warn("Unknown control command: {}", command);
791         }
792     }
793 }