2 * Copyright (c) 2010-2020 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
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
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.nuvo.internal.handler;
15 import static org.openhab.binding.nuvo.internal.NuvoBindingConstants.*;
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;
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;
33 import javax.measure.Unit;
34 import javax.measure.quantity.Time;
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;
76 * The {@link NuvoHandler} is responsible for handling commands, which are sent to one of the channels.
78 * Based on the Rotel binding by Laurent Garnier
80 * @author Michael Lobstein - Initial contribution
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;
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";
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;
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])$");
113 private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy,MM,dd,HH,mm");
115 private final Logger logger = LoggerFactory.getLogger(NuvoHandler.class);
116 private final NuvoStateDescriptionOptionProvider stateDescriptionProvider;
117 private final SerialPortManager serialPortManager;
119 private @Nullable ScheduledFuture<?> reconnectJob;
120 private @Nullable ScheduledFuture<?> pollingJob;
121 private @Nullable ScheduledFuture<?> clockSyncJob;
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();
130 Set<Integer> activeZones = new HashSet<>(1);
132 // A state option list for the source labels
133 List<StateOption> sourceLabels = new ArrayList<>();
138 public NuvoHandler(Thing thing, NuvoStateDescriptionOptionProvider stateDescriptionProvider,
139 SerialPortManager serialPortManager) {
141 this.stateDescriptionProvider = stateDescriptionProvider;
142 this.serialPortManager = serialPortManager;
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;
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";
164 configError = "undefined port configuration setting";
165 } else if (port <= 0) {
166 configError = "invalid port configuration setting";
170 if (configError != null) {
171 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, configError);
175 if (serialPort != null) {
176 connector = new NuvoSerialConnector(serialPortManager, serialPort, uid);
177 } else if (port != null) {
178 connector = new NuvoIpConnector(host, port, uid);
180 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
181 "Either Serial port or Host & Port must be specifed");
185 if (numZones != null) {
186 this.numZones = numZones;
189 activeZones = IntStream.range((1), (this.numZones + 1)).boxed().collect(Collectors.toSet());
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());
195 List<Integer> zonesToRemove = IntStream.range((this.numZones + 1), (MAX_ZONES + 1)).boxed()
196 .collect(Collectors.toList());
198 zonesToRemove.forEach(zone -> channels.removeIf(c -> (c.getUID().getId().contains("zone" + zone))));
199 updateThing(editThing().withChannels(channels).build());
202 if (config.clockSync) {
203 scheduleClockSyncJob();
206 scheduleReconnectJob();
207 schedulePollingJob();
208 updateStatus(ThingStatus.UNKNOWN);
212 public void dispose() {
213 cancelReconnectJob();
215 cancelClockSyncJob();
221 public Collection<Class<? extends ThingHandlerService>> getServices() {
222 return Collections.singletonList(NuvoThingActions.class);
225 public void handleRawCommand(@Nullable String command) {
226 synchronized (sequenceLock) {
228 connector.sendCommand(command);
229 } catch (NuvoException e) {
230 logger.warn("Nuvo Command: {} failed", command);
236 * Handle a command the UI
238 * @param channelUID the channel sending the command
239 * @param command the command received
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());
248 String channelType = channelSplit[1];
250 if (getThing().getStatus() != ThingStatus.ONLINE) {
251 logger.debug("Thing is not ONLINE; command {} from channel {} is ignored", command, channel);
255 synchronized (sequenceLock) {
256 if (!connector.isConnected()) {
257 logger.warn("Command {} from channel {} is ignored: connection not established", command, channel);
262 switch (channelType) {
263 case CHANNEL_TYPE_POWER:
264 if (command instanceof OnOffType) {
265 connector.sendCommand(target, command == OnOffType.ON ? NuvoCommand.ON : NuvoCommand.OFF);
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));
277 case CHANNEL_TYPE_VOLUME:
278 if (command instanceof PercentType) {
279 int value = (MAX_VOLUME
281 ((PercentType) command).doubleValue() / 100.0 * (MAX_VOLUME - MIN_VOLUME))
283 logger.debug("Got volume command {} zone {}", value, target);
284 connector.sendCommand(target, NuvoCommand.VOLUME, String.valueOf(value));
287 case CHANNEL_TYPE_MUTE:
288 if (command instanceof OnOffType) {
289 connector.sendCommand(target,
290 command == OnOffType.ON ? NuvoCommand.MUTE_ON : NuvoCommand.MUTE_OFF);
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
300 logger.debug("Got treble command {} zone {}", value, target);
301 connector.sendCfgCommand(target, NuvoCommand.TREBLE, String.valueOf(value));
305 case CHANNEL_TYPE_BASS:
306 if (command instanceof DecimalType) {
307 int value = ((DecimalType) command).intValue();
308 if (value >= MIN_EQ && value <= MAX_EQ) {
311 logger.debug("Got bass command {} zone {}", value, target);
312 connector.sendCfgCommand(target, NuvoCommand.BASS, String.valueOf(value));
316 case CHANNEL_TYPE_BALANCE:
317 if (command instanceof DecimalType) {
318 int value = ((DecimalType) command).intValue();
319 if (value >= MIN_EQ && value <= MAX_EQ) {
322 logger.debug("Got balance command {} zone {}", value, target);
323 connector.sendCfgCommand(target, NuvoCommand.BALANCE,
324 NuvoStatusCodes.getBalanceFromInt(value));
328 case CHANNEL_TYPE_LOUDNESS:
329 if (command instanceof OnOffType) {
330 connector.sendCfgCommand(target, NuvoCommand.LOUDNESS,
331 command == OnOffType.ON ? ONE : ZERO);
334 case CHANNEL_TYPE_CONTROL:
335 handleControlCommand(target, command);
337 case CHANNEL_TYPE_DND:
338 if (command instanceof OnOffType) {
339 connector.sendCommand(target,
340 command == OnOffType.ON ? NuvoCommand.DND_ON : NuvoCommand.DND_OFF);
343 case CHANNEL_TYPE_PARTY:
344 if (command instanceof OnOffType) {
345 connector.sendCommand(target,
346 command == OnOffType.ON ? NuvoCommand.PARTY_ON : NuvoCommand.PARTY_OFF);
349 case CHANNEL_DISPLAY_LINE1:
350 if (command instanceof StringType) {
351 connector.sendCommand(target, NuvoCommand.DISPLINE1, "\"" + command + "\"");
354 case CHANNEL_DISPLAY_LINE2:
355 if (command instanceof StringType) {
356 connector.sendCommand(target, NuvoCommand.DISPLINE2, "\"" + command + "\"");
359 case CHANNEL_DISPLAY_LINE3:
360 if (command instanceof StringType) {
361 connector.sendCommand(target, NuvoCommand.DISPLINE3, "\"" + command + "\"");
364 case CHANNEL_DISPLAY_LINE4:
365 if (command instanceof StringType) {
366 connector.sendCommand(target, NuvoCommand.DISPLINE4, "\"" + command + "\"");
369 case CHANNEL_TYPE_ALLOFF:
370 if (command instanceof OnOffType) {
371 connector.sendCommand(NuvoCommand.ALLOFF);
374 case CHANNEL_TYPE_ALLMUTE:
375 if (command instanceof OnOffType) {
376 connector.sendCommand(target,
377 command == OnOffType.ON ? NuvoCommand.ALLMUTE_ON : NuvoCommand.ALLMUTE_OFF);
380 case CHANNEL_TYPE_PAGE:
381 if (command instanceof OnOffType) {
382 connector.sendCommand(target,
383 command == OnOffType.ON ? NuvoCommand.PAGE_ON : NuvoCommand.PAGE_OFF);
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");
391 scheduleReconnectJob();
397 * Open the connection with the Nuvo device
399 * @return true if the connection is opened successfully or false if not
401 private synchronized boolean openConnection() {
402 connector.addEventListener(this);
405 } catch (NuvoException e) {
406 logger.debug("openConnection() failed: {}", e.getMessage());
408 logger.debug("openConnection(): {}", connector.isConnected() ? "connected" : "disconnected");
409 return connector.isConnected();
413 * Close the connection with the Nuvo device
415 private synchronized void closeConnection() {
416 if (connector.isConnected()) {
418 connector.removeEventListener(this);
419 logger.debug("closeConnection(): disconnected");
424 * Handle an event received from the Nuvo device
426 * @param event the event to process
429 public void onNewMessageEvent(NuvoMessageEvent evt) {
430 logger.debug("onNewMessageEvent: key {} = {}", evt.getKey(), evt.getValue());
431 lastEventReceived = System.currentTimeMillis();
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);
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);
450 activeZones.forEach(zoneNum -> {
451 updateChannelState(NuvoEnum.valueOf(ZONE + zoneNum), CHANNEL_TYPE_POWER, OFF);
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);
462 updateChannelState(NuvoEnum.SYSTEM, CHANNEL_TYPE_PAGE, ONE.equals(updateData) ? ON : OFF);
464 case TYPE_SOURCE_UPDATE:
465 logger.debug("Source update: Source: {} - Value: {}", key, updateData);
466 NuvoEnum targetSource = NuvoEnum.valueOf(SOURCE + key);
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));
474 logger.debug("no match on message: {}", updateData);
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));
485 logger.debug("no match on message: {}", updateData);
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));
493 case TYPE_ZONE_UPDATE:
494 logger.debug("Zone update: Zone: {} - Value: {}", key, updateData);
496 // or: ON,SRC3,VOL63,DND0,LOCK0
497 // or: ON,SRC3,MUTE,DND0,LOCK0
499 NuvoEnum targetZone = NuvoEnum.valueOf(ZONE + key);
501 if (OFF.equals(updateData)) {
502 updateChannelState(targetZone, CHANNEL_TYPE_POWER, OFF);
503 updateChannelState(targetZone, CHANNEL_TYPE_SOURCE, UNDEF);
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));
510 if (MUTE.equals(matcher.group(2))) {
511 updateChannelState(targetZone, CHANNEL_TYPE_MUTE, ON);
513 updateChannelState(targetZone, CHANNEL_TYPE_MUTE, NuvoCommand.OFF.getValue());
514 updateChannelState(targetZone, CHANNEL_TYPE_VOLUME, matcher.group(2).replace(VOL, BLANK));
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);
520 logger.debug("no match on message: {}", updateData);
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);
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);
540 logger.debug("no match on message: {}", updateData);
544 logger.debug("onNewMessageEvent: unhandled key {}", key);
550 * Schedule the reconnection job
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...");
560 if (openConnection()) {
561 synchronized (sequenceLock) {
563 long prevUpdateTime = lastEventReceived;
565 connector.sendCommand(NuvoCommand.GET_CONTROLLER_VERSION);
567 NuvoEnum.VALID_SOURCES.forEach(source -> {
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());
580 // Query all active zones to get their current status and eq configuration
581 activeZones.forEach(zoneNum -> {
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,
587 Thread.sleep(SLEEP_BETWEEN_CMD_MS);
588 } catch (NuvoException | InterruptedException e) {
589 logger.debug("Error Querying Zone data: {}", e.getMessage());
593 // prevUpdateTime should have changed if a zone update was received
594 if (prevUpdateTime == lastEventReceived) {
595 error = "Controller not responding to status requests";
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),
604 } catch (NuvoException e) {
605 error = "First command after connection failed";
606 logger.debug("{}: {}", error, e.getMessage());
610 error = "Reconnection failed";
613 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, error);
616 updateStatus(ThingStatus.ONLINE, ThingStatusDetail.NONE, this.versionString);
619 }, 1, RECON_POLLING_INTERVAL_SEC, TimeUnit.SECONDS);
623 * Cancel the reconnection job
625 private void cancelReconnectJob() {
626 ScheduledFuture<?> reconnectJob = this.reconnectJob;
627 if (reconnectJob != null) {
628 reconnectJob.cancel(true);
629 this.reconnectJob = null;
634 * Schedule the polling job
636 private void schedulePollingJob() {
637 logger.debug("Schedule polling job");
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...");
646 synchronized (sequenceLock) {
648 connector.sendCommand(NuvoCommand.GET_CONTROLLER_VERSION);
649 } catch (NuvoException e) {
650 logger.debug("Polling error: {}", e.getMessage());
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");
660 scheduleReconnectJob();
664 }, INITIAL_POLLING_DELAY_SEC, POLLING_INTERVAL_SEC, TimeUnit.SECONDS);
668 * Cancel the polling job
670 private void cancelPollingJob() {
671 ScheduledFuture<?> pollingJob = this.pollingJob;
672 if (pollingJob != null) {
673 pollingJob.cancel(true);
674 this.pollingJob = null;
679 * Schedule the clock sync job
681 private void scheduleClockSyncJob() {
682 logger.debug("Schedule clock sync job");
683 cancelClockSyncJob();
684 clockSyncJob = scheduler.scheduleWithFixedDelay(() -> {
685 if (this.isGConcerto) {
687 connector.sendCommand(NuvoCommand.CFGTIME.getValue() + DATE_FORMAT.format(new Date()));
688 } catch (NuvoException e) {
689 logger.debug("Error syncing clock: {}", e.getMessage());
692 this.cancelClockSyncJob();
694 }, INITIAL_CLOCK_SYNC_DELAY_SEC, CLOCK_SYNC_INTERVAL_SEC, TimeUnit.SECONDS);
698 * Cancel the clock sync job
700 private void cancelClockSyncJob() {
701 ScheduledFuture<?> clockSyncJob = this.clockSyncJob;
702 if (clockSyncJob != null) {
703 clockSyncJob.cancel(true);
704 this.clockSyncJob = null;
709 * Update the state of a channel
711 * @param target the channel group
712 * @param channelType the channel group item
713 * @param value the value to be updated
715 private void updateChannelState(NuvoEnum target, String channelType, String value) {
716 String channel = target.name().toLowerCase() + CHANNEL_DELIMIT + channelType;
718 if (!isLinked(channel)) {
722 State state = UnDefType.UNDEF;
724 if (UNDEF.equals(value)) {
725 updateState(channel, state);
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;
739 case CHANNEL_TYPE_LOCK:
740 state = ON.equals(value) ? OpenClosedType.OPEN : OpenClosedType.CLOSED;
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);
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));
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);
761 case CHANNEL_PLAY_MODE:
762 state = new StringType(NuvoStatusCodes.PLAY_MODE.get(value));
764 case CHANNEL_TRACK_LENGTH:
765 case CHANNEL_TRACK_POSITION:
766 state = new QuantityType<Time>(Integer.parseInt(value) / 10, NuvoHandler.API_SECOND_UNIT);
771 updateState(channel, state);
775 * Handle a button press from a UI Player item
777 * @param target the nuvo zone to receive the command
778 * @param command the button press command to send to the zone
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);
790 logger.warn("Unknown control command: {}", command);