2 * Copyright (c) 2010-2023 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.monopriceaudio.internal.handler;
15 import static org.openhab.binding.monopriceaudio.internal.MonopriceAudioBindingConstants.*;
17 import java.math.BigDecimal;
18 import java.util.ArrayList;
19 import java.util.HashSet;
20 import java.util.List;
23 import java.util.concurrent.ScheduledFuture;
24 import java.util.concurrent.TimeUnit;
25 import java.util.stream.Collectors;
26 import java.util.stream.IntStream;
27 import java.util.stream.Stream;
29 import org.eclipse.jdt.annotation.NonNullByDefault;
30 import org.eclipse.jdt.annotation.Nullable;
31 import org.openhab.binding.monopriceaudio.internal.MonopriceAudioException;
32 import org.openhab.binding.monopriceaudio.internal.MonopriceAudioStateDescriptionOptionProvider;
33 import org.openhab.binding.monopriceaudio.internal.communication.AmplifierModel;
34 import org.openhab.binding.monopriceaudio.internal.communication.MonopriceAudioConnector;
35 import org.openhab.binding.monopriceaudio.internal.communication.MonopriceAudioDefaultConnector;
36 import org.openhab.binding.monopriceaudio.internal.communication.MonopriceAudioIpConnector;
37 import org.openhab.binding.monopriceaudio.internal.communication.MonopriceAudioMessageEvent;
38 import org.openhab.binding.monopriceaudio.internal.communication.MonopriceAudioMessageEventListener;
39 import org.openhab.binding.monopriceaudio.internal.communication.MonopriceAudioSerialConnector;
40 import org.openhab.binding.monopriceaudio.internal.configuration.MonopriceAudioThingConfiguration;
41 import org.openhab.binding.monopriceaudio.internal.dto.MonopriceAudioZoneDTO;
42 import org.openhab.core.io.transport.serial.SerialPortManager;
43 import org.openhab.core.library.types.DecimalType;
44 import org.openhab.core.library.types.OnOffType;
45 import org.openhab.core.library.types.OpenClosedType;
46 import org.openhab.core.library.types.PercentType;
47 import org.openhab.core.thing.Channel;
48 import org.openhab.core.thing.ChannelUID;
49 import org.openhab.core.thing.Thing;
50 import org.openhab.core.thing.ThingStatus;
51 import org.openhab.core.thing.ThingStatusDetail;
52 import org.openhab.core.thing.binding.BaseThingHandler;
53 import org.openhab.core.types.Command;
54 import org.openhab.core.types.RefreshType;
55 import org.openhab.core.types.State;
56 import org.openhab.core.types.StateOption;
57 import org.openhab.core.types.UnDefType;
58 import org.slf4j.Logger;
59 import org.slf4j.LoggerFactory;
62 * The {@link MonopriceAudioHandler} is responsible for handling commands, which are sent to one of the channels.
64 * Based on the Rotel binding by Laurent Garnier
66 * @author Michael Lobstein - Initial contribution
67 * @author Michael Lobstein - Add support for additional amplifier types
70 public class MonopriceAudioHandler extends BaseThingHandler implements MonopriceAudioMessageEventListener {
71 private static final long RECON_POLLING_INTERVAL_SEC = 60;
72 private static final long INITIAL_POLLING_DELAY_SEC = 10;
74 private static final String ZONE = "zone";
75 private static final String ALL = "all";
76 private static final String CHANNEL_DELIMIT = "#";
78 private static final int ZERO = 0;
79 private static final int ONE = 1;
80 private static final int MIN_VOLUME = 0;
82 private final Logger logger = LoggerFactory.getLogger(MonopriceAudioHandler.class);
83 private final AmplifierModel amp;
84 private final MonopriceAudioStateDescriptionOptionProvider stateDescriptionProvider;
85 private final SerialPortManager serialPortManager;
87 private @Nullable ScheduledFuture<?> reconnectJob;
88 private @Nullable ScheduledFuture<?> pollingJob;
90 private MonopriceAudioConnector connector = new MonopriceAudioDefaultConnector();
92 private Map<String, MonopriceAudioZoneDTO> zoneDataMap = Map.of(ZONE, new MonopriceAudioZoneDTO());
93 private Set<String> ignoreZones = new HashSet<>();
94 private long lastPollingUpdate = System.currentTimeMillis();
95 private long pollingInterval = ZERO;
96 private int numZones = ZERO;
97 private int allVolume = ONE;
98 private int initialAllVolume = ZERO;
99 private boolean disableKeypadPolling = false;
100 private Object sequenceLock = new Object();
102 public MonopriceAudioHandler(Thing thing, AmplifierModel amp,
103 MonopriceAudioStateDescriptionOptionProvider stateDescriptionProvider,
104 SerialPortManager serialPortManager) {
107 this.stateDescriptionProvider = stateDescriptionProvider;
108 this.serialPortManager = serialPortManager;
112 public void initialize() {
113 final String uid = this.getThing().getUID().getAsString();
114 MonopriceAudioThingConfiguration config = getConfigAs(MonopriceAudioThingConfiguration.class);
115 final String serialPort = config.serialPort;
116 final String host = config.host;
117 final Integer port = config.port;
118 numZones = config.numZones;
119 final String ignoreZonesConfig = config.ignoreZones;
120 disableKeypadPolling = config.disableKeypadPolling || amp == AmplifierModel.MONOPRICE70;
122 // build a Map with a MonopriceAudioZoneDTO for each zoneId
123 zoneDataMap = amp.getZoneIds().stream().limit(numZones)
124 .collect(Collectors.toMap(s -> s, s -> new MonopriceAudioZoneDTO(s)));
126 // Check configuration settings
127 if (serialPort != null && host == null && port == null) {
128 if (serialPort.toLowerCase().startsWith("rfc2217")) {
129 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
130 "@text/offline.configuration-error-rfc2217");
133 } else if (serialPort != null && (host != null || port != null)) {
134 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
135 "@text/offline.configuration-error-conflict");
139 if (serialPort != null) {
140 connector = new MonopriceAudioSerialConnector(serialPortManager, serialPort, uid, amp);
141 } else if (host != null && (port != null && port > ZERO)) {
142 connector = new MonopriceAudioIpConnector(host, port, uid, amp);
144 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
145 "@text/offline.configuration-error-missing");
149 pollingInterval = config.pollingInterval;
150 initialAllVolume = config.initialAllVolume;
152 // If zones were specified to be ignored by the 'all*' commands, use the specified binding
153 // zone ids to get the amplifier's internal zone ids and save those to a list
154 if (ignoreZonesConfig != null) {
155 for (String zone : ignoreZonesConfig.split(",")) {
157 int zoneInt = Integer.parseInt(zone);
158 if (zoneInt >= ONE && zoneInt <= amp.getMaxZones()) {
159 ignoreZones.add(ZONE + zoneInt);
161 logger.debug("Invalid ignore zone value: {}, value must be between {} and {}", zone, ONE,
164 } catch (NumberFormatException nfe) {
165 logger.debug("Invalid ignore zone value: {}", zone);
170 // Put the source labels on all active zones
171 List<Integer> activeZones = IntStream.range(1, numZones + 1).boxed().collect(Collectors.toList());
173 List<StateOption> sourceLabels = amp.getSourceLabels(config);
174 stateDescriptionProvider.setStateOptions(
175 new ChannelUID(getThing().getUID(), ALL + CHANNEL_DELIMIT + CHANNEL_TYPE_ALLSOURCE), sourceLabels);
176 activeZones.forEach(zoneNum -> {
177 stateDescriptionProvider.setStateOptions(
178 new ChannelUID(getThing().getUID(), ZONE + zoneNum + CHANNEL_DELIMIT + CHANNEL_TYPE_SOURCE),
182 // remove the channels for the zones we are not using
183 if (numZones < amp.getMaxZones()) {
184 List<Channel> channels = new ArrayList<>(this.getThing().getChannels());
186 List<Integer> zonesToRemove = IntStream.range(numZones + 1, amp.getMaxZones() + 1).boxed()
187 .collect(Collectors.toList());
189 zonesToRemove.forEach(zone -> {
190 channels.removeIf(c -> (c.getUID().getId().contains(ZONE + zone)));
192 updateThing(editThing().withChannels(channels).build());
195 // initialize the all volume state
196 allVolume = initialAllVolume;
197 long allVolumePct = Math
198 .round((initialAllVolume - MIN_VOLUME) / (double) (amp.getMaxVol() - MIN_VOLUME) * 100.0);
199 updateState(ALL + CHANNEL_DELIMIT + CHANNEL_TYPE_ALLVOLUME, new PercentType(BigDecimal.valueOf(allVolumePct)));
201 scheduleReconnectJob();
202 schedulePollingJob();
204 updateStatus(ThingStatus.UNKNOWN);
208 public void dispose() {
209 cancelReconnectJob();
216 public void handleCommand(ChannelUID channelUID, Command command) {
217 String channel = channelUID.getId();
218 String[] channelSplit = channel.split(CHANNEL_DELIMIT);
219 String channelType = channelSplit[1];
220 String zoneName = channelSplit[0];
221 String zoneId = amp.getZoneIdFromZoneName(zoneName);
223 if (getThing().getStatus() != ThingStatus.ONLINE) {
224 logger.debug("Thing is not ONLINE; command {} from channel {} is ignored", command, channel);
228 boolean success = true;
229 synchronized (sequenceLock) {
230 if (!connector.isConnected()) {
231 logger.debug("Command {} from channel {} is ignored: connection not established", command, channel);
235 if (command instanceof RefreshType) {
236 updateChannelState(zoneId, channelType);
240 Stream<String> zoneStream = amp.getZoneIds().stream().limit(numZones);
242 switch (channelType) {
243 case CHANNEL_TYPE_POWER:
244 if (command instanceof OnOffType) {
245 connector.sendCommand(zoneId, amp.getPowerCmd(), command == OnOffType.ON ? ONE : ZERO);
246 zoneDataMap.get(zoneId)
247 .setPower(command == OnOffType.ON ? amp.getOnStr() : amp.getOffStr());
250 case CHANNEL_TYPE_SOURCE:
251 if (command instanceof DecimalType) {
252 final int value = ((DecimalType) command).intValue();
253 if (value >= ONE && value <= amp.getNumSources()) {
254 logger.debug("Got source command {} zone {}", value, zoneId);
255 connector.sendCommand(zoneId, amp.getSourceCmd(), value);
256 zoneDataMap.get(zoneId).setSource(amp.getFormattedValue(value));
260 case CHANNEL_TYPE_VOLUME:
261 if (command instanceof PercentType) {
262 final int value = (int) Math.round(
263 ((PercentType) command).doubleValue() / 100.0 * (amp.getMaxVol() - MIN_VOLUME))
265 logger.debug("Got volume command {} zone {}", value, zoneId);
266 connector.sendCommand(zoneId, amp.getVolumeCmd(), value);
267 zoneDataMap.get(zoneId).setVolume(value);
270 case CHANNEL_TYPE_MUTE:
271 if (command instanceof OnOffType) {
272 connector.sendCommand(zoneId, amp.getMuteCmd(), command == OnOffType.ON ? ONE : ZERO);
273 zoneDataMap.get(zoneId).setMute(command == OnOffType.ON ? amp.getOnStr() : amp.getOffStr());
276 case CHANNEL_TYPE_TREBLE:
277 if (command instanceof DecimalType) {
278 final int value = ((DecimalType) command).intValue();
279 if (value >= amp.getMinTone() && value <= amp.getMaxTone()) {
280 logger.debug("Got treble command {} zone {}", value, zoneId);
281 connector.sendCommand(zoneId, amp.getTrebleCmd(), value + amp.getToneOffset());
282 zoneDataMap.get(zoneId).setTreble(value + amp.getToneOffset());
286 case CHANNEL_TYPE_BASS:
287 if (command instanceof DecimalType) {
288 final int value = ((DecimalType) command).intValue();
289 if (value >= amp.getMinTone() && value <= amp.getMaxTone()) {
290 logger.debug("Got bass command {} zone {}", value, zoneId);
291 connector.sendCommand(zoneId, amp.getBassCmd(), value + amp.getToneOffset());
292 zoneDataMap.get(zoneId).setBass(value + amp.getToneOffset());
296 case CHANNEL_TYPE_BALANCE:
297 if (command instanceof DecimalType) {
298 final int value = ((DecimalType) command).intValue();
299 if (value >= amp.getMinBal() && value <= amp.getMaxBal()) {
300 logger.debug("Got balance command {} zone {}", value, zoneId);
301 connector.sendCommand(zoneId, amp.getBalanceCmd(), value + amp.getBalOffset());
302 zoneDataMap.get(zoneId).setBalance(value + amp.getBalOffset());
306 case CHANNEL_TYPE_DND:
307 if (command instanceof OnOffType) {
308 connector.sendCommand(zoneId, amp.getDndCmd(), command == OnOffType.ON ? ONE : ZERO);
309 zoneDataMap.get(zoneId).setDnd(command == OnOffType.ON ? amp.getOnStr() : amp.getOffStr());
312 case CHANNEL_TYPE_ALLPOWER:
313 if (command instanceof OnOffType) {
314 final int cmd = command == OnOffType.ON ? ONE : ZERO;
315 zoneStream.forEach((streamZoneId) -> {
316 if (command == OnOffType.OFF || !ignoreZones.contains(amp.getZoneName(streamZoneId))) {
318 connector.sendCommand(streamZoneId, amp.getPowerCmd(), cmd);
319 zoneDataMap.get(streamZoneId).setPower(amp.getFormattedValue(cmd));
320 updateChannelState(streamZoneId, CHANNEL_TYPE_POWER);
322 if (command == OnOffType.ON) {
323 // reset the volume of each zone to allVolume
324 connector.sendCommand(streamZoneId, amp.getVolumeCmd(), allVolume);
325 zoneDataMap.get(streamZoneId).setVolume(allVolume);
326 updateChannelState(streamZoneId, CHANNEL_TYPE_VOLUME);
328 } catch (MonopriceAudioException e) {
329 logger.debug("Error Turning All Zones On: {}", e.getMessage());
336 case CHANNEL_TYPE_ALLSOURCE:
337 if (command instanceof DecimalType) {
338 final int value = ((DecimalType) command).intValue();
339 if (value >= ONE && value <= amp.getNumSources()) {
340 zoneStream.forEach((streamZoneId) -> {
341 if (!ignoreZones.contains(amp.getZoneName(streamZoneId))) {
343 connector.sendCommand(streamZoneId, amp.getSourceCmd(), value);
344 if (zoneDataMap.get(streamZoneId).isPowerOn()
345 && !zoneDataMap.get(streamZoneId).isMuted()) {
346 zoneDataMap.get(streamZoneId).setSource(amp.getFormattedValue(value));
347 updateChannelState(streamZoneId, CHANNEL_TYPE_SOURCE);
349 } catch (MonopriceAudioException e) {
350 logger.debug("Error Setting Source for All Zones: {}", e.getMessage());
357 case CHANNEL_TYPE_ALLVOLUME:
358 if (command instanceof PercentType) {
359 allVolume = (int) Math.round(
360 ((PercentType) command).doubleValue() / 100.0 * (amp.getMaxVol() - MIN_VOLUME))
362 zoneStream.forEach((streamZoneId) -> {
363 if (!ignoreZones.contains(amp.getZoneName(streamZoneId))) {
365 connector.sendCommand(streamZoneId, amp.getVolumeCmd(), allVolume);
366 if (zoneDataMap.get(streamZoneId).isPowerOn()
367 && !zoneDataMap.get(streamZoneId).isMuted()) {
368 zoneDataMap.get(streamZoneId).setVolume(allVolume);
369 updateChannelState(streamZoneId, CHANNEL_TYPE_VOLUME);
371 } catch (MonopriceAudioException e) {
372 logger.debug("Error Setting Volume for All Zones: {}", e.getMessage());
378 case CHANNEL_TYPE_ALLMUTE:
379 if (command instanceof OnOffType) {
380 final int cmd = command == OnOffType.ON ? ONE : ZERO;
381 zoneStream.forEach((streamZoneId) -> {
382 if (!ignoreZones.contains(amp.getZoneName(streamZoneId))) {
384 connector.sendCommand(streamZoneId, amp.getMuteCmd(), cmd);
385 if (zoneDataMap.get(streamZoneId).isPowerOn()) {
386 zoneDataMap.get(streamZoneId).setMute(amp.getFormattedValue(cmd));
387 updateChannelState(streamZoneId, CHANNEL_TYPE_MUTE);
389 } catch (MonopriceAudioException e) {
390 logger.debug("Error Setting Mute for All Zones: {}", e.getMessage());
398 logger.debug("Command {} from channel {} failed: unexpected command", command, channel);
403 logger.trace("Command {} from channel {} succeeded", command, channel);
405 } catch (MonopriceAudioException e) {
406 logger.debug("Command {} from channel {} failed: {}", command, channel, e.getMessage());
407 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
408 "@text/offline.communication-error-failed");
410 scheduleReconnectJob();
416 * Open the connection to the amplifier
418 * @return true if the connection is opened successfully or false if not
420 private synchronized boolean openConnection() {
421 connector.addEventListener(this);
424 } catch (MonopriceAudioException e) {
425 logger.debug("openConnection() failed: {}", e.getMessage());
427 logger.debug("openConnection(): {}", connector.isConnected() ? "connected" : "disconnected");
428 return connector.isConnected();
432 * Close the connection to the amplifier
434 private synchronized void closeConnection() {
435 if (connector.isConnected()) {
437 connector.removeEventListener(this);
438 logger.debug("closeConnection(): disconnected");
443 public void onNewMessageEvent(MonopriceAudioMessageEvent evt) {
444 String key = evt.getKey();
447 case MonopriceAudioConnector.KEY_ZONE_UPDATE:
448 MonopriceAudioZoneDTO newZoneData = amp.getZoneData(evt.getValue());
449 MonopriceAudioZoneDTO zoneData = zoneDataMap.get(newZoneData.getZone());
450 if (amp.getZoneIds().contains(newZoneData.getZone()) && zoneData != null) {
451 if (amp == AmplifierModel.MONOPRICE70) {
452 processMonoprice70Update(zoneData, newZoneData);
454 processZoneUpdate(zoneData, newZoneData);
457 logger.debug("invalid event: {} for key: {} or zone data null", evt.getValue(), key);
461 case MonopriceAudioConnector.KEY_PING:
462 lastPollingUpdate = System.currentTimeMillis();
466 logger.debug("onNewMessageEvent: unhandled key {}", key);
472 * Schedule the reconnection job
474 private void scheduleReconnectJob() {
475 logger.debug("Schedule reconnect job");
476 cancelReconnectJob();
477 reconnectJob = scheduler.scheduleWithFixedDelay(() -> {
478 synchronized (sequenceLock) {
479 if (!connector.isConnected()) {
480 logger.debug("Trying to reconnect...");
484 if (openConnection()) {
485 long prevUpdateTime = lastPollingUpdate;
486 // poll all zones on the amplifier to get current state
487 amp.getZoneIds().stream().limit(numZones).forEach((streamZoneId) -> {
489 connector.queryZone(streamZoneId);
491 if (amp == AmplifierModel.MONOPRICE70) {
492 connector.queryTrebBassBalance(streamZoneId);
494 } catch (MonopriceAudioException e) {
495 logger.debug("Polling error: {}", e.getMessage());
499 if (amp == AmplifierModel.XANTECH) {
501 // for xantech send the commands to enable unsolicited updates
502 connector.sendCommand("!ZA1");
503 connector.sendCommand("!ZP10"); // Zone Periodic Auto Update set to 10 secs
504 } catch (MonopriceAudioException e) {
505 logger.debug("Error sending Xantech periodic update commands: {}", e.getMessage());
509 // prevUpdateTime should have changed if a zone update was received
510 if (lastPollingUpdate == prevUpdateTime) {
511 error = "@text/offline.communication-error-polling";
514 error = "@text/offline.communication-error-reconnection";
518 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, error);
520 updateStatus(ThingStatus.ONLINE);
521 lastPollingUpdate = System.currentTimeMillis();
525 }, 1, RECON_POLLING_INTERVAL_SEC, TimeUnit.SECONDS);
529 * Cancel the reconnection job
531 private void cancelReconnectJob() {
532 ScheduledFuture<?> reconnectJob = this.reconnectJob;
533 if (reconnectJob != null) {
534 reconnectJob.cancel(true);
535 this.reconnectJob = null;
540 * Schedule the polling job
542 private void schedulePollingJob() {
543 logger.debug("Schedule polling job");
546 pollingJob = scheduler.scheduleWithFixedDelay(() -> {
547 synchronized (sequenceLock) {
548 if (connector.isConnected()) {
549 logger.debug("Polling the amplifier for updated status...");
551 if (!disableKeypadPolling) {
552 // poll each zone up to the number of zones specified in the configuration
553 amp.getZoneIds().stream().limit(numZones).forEach((streamZoneId) -> {
555 connector.queryZone(streamZoneId);
556 } catch (MonopriceAudioException e) {
557 logger.debug("Polling error for zone id {}: {}", streamZoneId, e.getMessage());
562 // ping only (no zone updates) to verify the connection is still alive
563 connector.sendPing();
564 } catch (MonopriceAudioException e) {
565 logger.debug("Ping error: {}", e.getMessage());
569 // if the last successful polling update was more than 2.25 intervals ago, the amplifier
570 // is either switched off or not responding even though the connection is still good
571 if ((System.currentTimeMillis() - lastPollingUpdate) > (pollingInterval * 2.25 * 1000)) {
572 logger.debug("Amplifier not responding to status requests");
573 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
574 "@text/offline.communication-error-polling");
576 scheduleReconnectJob();
580 }, INITIAL_POLLING_DELAY_SEC, pollingInterval, TimeUnit.SECONDS);
584 * Cancel the polling job
586 private void cancelPollingJob() {
587 ScheduledFuture<?> pollingJob = this.pollingJob;
588 if (pollingJob != null) {
589 pollingJob.cancel(true);
590 this.pollingJob = null;
594 private void processZoneUpdate(MonopriceAudioZoneDTO zoneData, MonopriceAudioZoneDTO newZoneData) {
595 // only process the update if something actually changed in this zone since the last polling update
596 if (!newZoneData.toString().equals(zoneData.toString())) {
597 if (!newZoneData.getPage().equals(zoneData.getPage())) {
598 zoneData.setPage(newZoneData.getPage());
599 updateChannelState(zoneData.getZone(), CHANNEL_TYPE_PAGE);
602 if (!newZoneData.getPower().equals(zoneData.getPower())) {
603 zoneData.setPower(newZoneData.getPower());
604 updateChannelState(zoneData.getZone(), CHANNEL_TYPE_POWER);
607 if (!newZoneData.getMute().equals(zoneData.getMute())) {
608 zoneData.setMute(newZoneData.getMute());
609 updateChannelState(zoneData.getZone(), CHANNEL_TYPE_MUTE);
612 if (!newZoneData.getDnd().equals(zoneData.getDnd())) {
613 zoneData.setDnd(newZoneData.getDnd());
614 updateChannelState(zoneData.getZone(), CHANNEL_TYPE_DND);
617 if (newZoneData.getVolume() != zoneData.getVolume()) {
618 zoneData.setVolume(newZoneData.getVolume());
619 updateChannelState(zoneData.getZone(), CHANNEL_TYPE_VOLUME);
622 if (newZoneData.getTreble() != zoneData.getTreble()) {
623 zoneData.setTreble(newZoneData.getTreble());
624 updateChannelState(zoneData.getZone(), CHANNEL_TYPE_TREBLE);
627 if (newZoneData.getBass() != zoneData.getBass()) {
628 zoneData.setBass(newZoneData.getBass());
629 updateChannelState(zoneData.getZone(), CHANNEL_TYPE_BASS);
632 if (newZoneData.getBalance() != zoneData.getBalance()) {
633 zoneData.setBalance(newZoneData.getBalance());
634 updateChannelState(zoneData.getZone(), CHANNEL_TYPE_BALANCE);
637 if (!newZoneData.getSource().equals(zoneData.getSource())) {
638 zoneData.setSource(newZoneData.getSource());
639 updateChannelState(zoneData.getZone(), CHANNEL_TYPE_SOURCE);
642 if (!newZoneData.getKeypad().equals(zoneData.getKeypad())) {
643 zoneData.setKeypad(newZoneData.getKeypad());
644 updateChannelState(zoneData.getZone(), CHANNEL_TYPE_KEYPAD);
648 lastPollingUpdate = System.currentTimeMillis();
651 private void processMonoprice70Update(MonopriceAudioZoneDTO zoneData, MonopriceAudioZoneDTO newZoneData) {
652 if (newZoneData.getTreble() != NIL) {
653 zoneData.setTreble(newZoneData.getTreble());
654 updateChannelState(zoneData.getZone(), CHANNEL_TYPE_TREBLE);
655 } else if (newZoneData.getBass() != NIL) {
656 zoneData.setBass(newZoneData.getBass());
657 updateChannelState(zoneData.getZone(), CHANNEL_TYPE_BASS);
658 } else if (newZoneData.getBalance() != NIL) {
659 zoneData.setBalance(newZoneData.getBalance());
660 updateChannelState(zoneData.getZone(), CHANNEL_TYPE_BALANCE);
662 zoneData.setPower(newZoneData.getPower());
663 zoneData.setMute(newZoneData.getMute());
664 zoneData.setVolume(newZoneData.getVolume());
665 zoneData.setSource(newZoneData.getSource());
666 updateChannelState(zoneData.getZone(), CHANNEL_TYPE_POWER);
667 updateChannelState(zoneData.getZone(), CHANNEL_TYPE_MUTE);
668 updateChannelState(zoneData.getZone(), CHANNEL_TYPE_VOLUME);
669 updateChannelState(zoneData.getZone(), CHANNEL_TYPE_SOURCE);
672 lastPollingUpdate = System.currentTimeMillis();
676 * Update the state of a channel
678 * @param zoneId the zone id used to lookup the channel to be updated
679 * @param channelType the channel type to be updated
681 private void updateChannelState(String zoneId, String channelType) {
682 MonopriceAudioZoneDTO zoneData = zoneDataMap.get(zoneId);
684 if (zoneData != null) {
685 String channel = amp.getZoneName(zoneId) + CHANNEL_DELIMIT + channelType;
687 if (!isLinked(channel)) {
691 logger.debug("updating channel state for zone: {}, channel type: {}", zoneId, channelType);
693 State state = UnDefType.UNDEF;
694 switch (channelType) {
695 case CHANNEL_TYPE_POWER:
696 state = OnOffType.from(zoneData.isPowerOn());
698 case CHANNEL_TYPE_SOURCE:
699 state = new DecimalType(zoneData.getSource());
701 case CHANNEL_TYPE_VOLUME:
702 long volumePct = Math.round(
703 (zoneData.getVolume() - MIN_VOLUME) / (double) (amp.getMaxVol() - MIN_VOLUME) * 100.0);
704 state = new PercentType(BigDecimal.valueOf(volumePct));
706 case CHANNEL_TYPE_MUTE:
707 state = OnOffType.from(zoneData.isMuted());
709 case CHANNEL_TYPE_TREBLE:
710 state = new DecimalType(BigDecimal.valueOf(zoneData.getTreble() - amp.getToneOffset()));
712 case CHANNEL_TYPE_BASS:
713 state = new DecimalType(BigDecimal.valueOf(zoneData.getBass() - amp.getToneOffset()));
715 case CHANNEL_TYPE_BALANCE:
716 state = new DecimalType(BigDecimal.valueOf(zoneData.getBalance() - amp.getBalOffset()));
718 case CHANNEL_TYPE_DND:
719 state = OnOffType.from(zoneData.isDndOn());
721 case CHANNEL_TYPE_PAGE:
722 state = zoneData.isPageActive() ? OpenClosedType.OPEN : OpenClosedType.CLOSED;
724 case CHANNEL_TYPE_KEYPAD:
725 state = zoneData.isKeypadActive() ? OpenClosedType.OPEN : OpenClosedType.CLOSED;
730 updateState(channel, state);