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.regex.Matcher;
26 import java.util.regex.Pattern;
27 import java.util.stream.Collectors;
28 import java.util.stream.IntStream;
29 import java.util.stream.Stream;
31 import org.eclipse.jdt.annotation.NonNullByDefault;
32 import org.eclipse.jdt.annotation.Nullable;
33 import org.openhab.binding.monopriceaudio.internal.MonopriceAudioException;
34 import org.openhab.binding.monopriceaudio.internal.MonopriceAudioStateDescriptionOptionProvider;
35 import org.openhab.binding.monopriceaudio.internal.communication.MonopriceAudioCommand;
36 import org.openhab.binding.monopriceaudio.internal.communication.MonopriceAudioConnector;
37 import org.openhab.binding.monopriceaudio.internal.communication.MonopriceAudioDefaultConnector;
38 import org.openhab.binding.monopriceaudio.internal.communication.MonopriceAudioIpConnector;
39 import org.openhab.binding.monopriceaudio.internal.communication.MonopriceAudioMessageEvent;
40 import org.openhab.binding.monopriceaudio.internal.communication.MonopriceAudioMessageEventListener;
41 import org.openhab.binding.monopriceaudio.internal.communication.MonopriceAudioSerialConnector;
42 import org.openhab.binding.monopriceaudio.internal.communication.MonopriceAudioZone;
43 import org.openhab.binding.monopriceaudio.internal.configuration.MonopriceAudioThingConfiguration;
44 import org.openhab.binding.monopriceaudio.internal.dto.MonopriceAudioZoneDTO;
45 import org.openhab.core.io.transport.serial.SerialPortManager;
46 import org.openhab.core.library.types.DecimalType;
47 import org.openhab.core.library.types.OnOffType;
48 import org.openhab.core.library.types.OpenClosedType;
49 import org.openhab.core.library.types.PercentType;
50 import org.openhab.core.thing.Channel;
51 import org.openhab.core.thing.ChannelUID;
52 import org.openhab.core.thing.Thing;
53 import org.openhab.core.thing.ThingStatus;
54 import org.openhab.core.thing.ThingStatusDetail;
55 import org.openhab.core.thing.binding.BaseThingHandler;
56 import org.openhab.core.types.Command;
57 import org.openhab.core.types.RefreshType;
58 import org.openhab.core.types.State;
59 import org.openhab.core.types.StateOption;
60 import org.openhab.core.types.UnDefType;
61 import org.slf4j.Logger;
62 import org.slf4j.LoggerFactory;
65 * The {@link MonopriceAudioHandler} is responsible for handling commands, which are sent to one of the channels.
67 * Based on the Rotel binding by Laurent Garnier
69 * @author Michael Lobstein - Initial contribution
72 public class MonopriceAudioHandler extends BaseThingHandler implements MonopriceAudioMessageEventListener {
73 private static final long RECON_POLLING_INTERVAL_SEC = 60;
74 private static final long INITIAL_POLLING_DELAY_SEC = 5;
75 private static final Pattern PATTERN = Pattern
76 .compile("^(\\d{2})(\\d{2})(\\d{2})(\\d{2})(\\d{2})(\\d{2})(\\d{2})(\\d{2})(\\d{2})(\\d{2})(\\d{2})");
78 private static final String ZONE = "ZONE";
79 private static final String ALL = "all";
80 private static final String CHANNEL_DELIMIT = "#";
81 private static final String ON_STR = "01";
82 private static final String OFF_STR = "00";
84 private static final int ONE = 1;
85 private static final int MAX_ZONES = 18;
86 private static final int MAX_SRC = 6;
87 private static final int MIN_VOLUME = 0;
88 private static final int MAX_VOLUME = 38;
89 private static final int MIN_TONE = -7;
90 private static final int MAX_TONE = 7;
91 private static final int MIN_BALANCE = -10;
92 private static final int MAX_BALANCE = 10;
93 private static final int BALANCE_OFFSET = 10;
94 private static final int TONE_OFFSET = 7;
96 // build a Map with a MonopriceAudioZoneDTO for each zoneId
97 private final Map<String, MonopriceAudioZoneDTO> zoneDataMap = MonopriceAudioZone.VALID_ZONE_IDS.stream()
98 .collect(Collectors.toMap(s -> s, s -> new MonopriceAudioZoneDTO()));
100 private final Logger logger = LoggerFactory.getLogger(MonopriceAudioHandler.class);
101 private final MonopriceAudioStateDescriptionOptionProvider stateDescriptionProvider;
102 private final SerialPortManager serialPortManager;
104 private @Nullable ScheduledFuture<?> reconnectJob;
105 private @Nullable ScheduledFuture<?> pollingJob;
107 private MonopriceAudioConnector connector = new MonopriceAudioDefaultConnector();
109 private Set<String> ignoreZones = new HashSet<>();
110 private long lastPollingUpdate = System.currentTimeMillis();
111 private long pollingInterval = 0;
112 private int numZones = 0;
113 private int allVolume = 1;
114 private int initialAllVolume = 0;
115 private Object sequenceLock = new Object();
117 public MonopriceAudioHandler(Thing thing, MonopriceAudioStateDescriptionOptionProvider stateDescriptionProvider,
118 SerialPortManager serialPortManager) {
120 this.stateDescriptionProvider = stateDescriptionProvider;
121 this.serialPortManager = serialPortManager;
125 public void initialize() {
126 final String uid = this.getThing().getUID().getAsString();
127 MonopriceAudioThingConfiguration config = getConfigAs(MonopriceAudioThingConfiguration.class);
128 final String serialPort = config.serialPort;
129 final String host = config.host;
130 final Integer port = config.port;
131 final String ignoreZonesConfig = config.ignoreZones;
133 // Check configuration settings
134 String configError = null;
135 if ((serialPort == null || serialPort.isEmpty()) && (host == null || host.isEmpty())) {
136 configError = "undefined serialPort and host configuration settings; please set one of them";
137 } else if (serialPort != null && (host == null || host.isEmpty())) {
138 if (serialPort.toLowerCase().startsWith("rfc2217")) {
139 configError = "use host and port configuration settings for a serial over IP connection";
143 configError = "undefined port configuration setting";
144 } else if (port <= 0) {
145 configError = "invalid port configuration setting";
149 if (configError != null) {
150 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, configError);
154 if (serialPort != null) {
155 connector = new MonopriceAudioSerialConnector(serialPortManager, serialPort, uid);
156 } else if (port != null) {
157 connector = new MonopriceAudioIpConnector(host, port, uid);
159 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
160 "Either Serial port or Host & Port must be specifed");
164 pollingInterval = config.pollingInterval;
165 numZones = config.numZones;
166 initialAllVolume = config.initialAllVolume;
168 // If zones were specified to be ignored by the 'all*' commands, use the specified binding
169 // zone ids to get the controller's internal zone ids and save those to a list
170 if (ignoreZonesConfig != null) {
171 for (String zone : ignoreZonesConfig.split(",")) {
173 int zoneInt = Integer.parseInt(zone);
174 if (zoneInt >= ONE && zoneInt <= MAX_ZONES) {
175 ignoreZones.add(ZONE + zoneInt);
177 logger.warn("Invalid ignore zone value: {}, value must be between {} and {}", zone, ONE,
180 } catch (NumberFormatException nfe) {
181 logger.warn("Invalid ignore zone value: {}", zone);
186 // Build a state option list for the source labels
187 List<StateOption> sourcesLabels = new ArrayList<>();
188 sourcesLabels.add(new StateOption("1", config.inputLabel1));
189 sourcesLabels.add(new StateOption("2", config.inputLabel2));
190 sourcesLabels.add(new StateOption("3", config.inputLabel3));
191 sourcesLabels.add(new StateOption("4", config.inputLabel4));
192 sourcesLabels.add(new StateOption("5", config.inputLabel5));
193 sourcesLabels.add(new StateOption("6", config.inputLabel6));
195 // Put the source labels on all active zones
196 List<Integer> activeZones = IntStream.range(1, numZones + 1).boxed().collect(Collectors.toList());
198 stateDescriptionProvider.setStateOptions(
199 new ChannelUID(getThing().getUID(), ALL + CHANNEL_DELIMIT + CHANNEL_TYPE_ALLSOURCE), sourcesLabels);
200 activeZones.forEach(zoneNum -> {
201 stateDescriptionProvider.setStateOptions(new ChannelUID(getThing().getUID(),
202 ZONE.toLowerCase() + zoneNum + CHANNEL_DELIMIT + CHANNEL_TYPE_SOURCE), sourcesLabels);
205 // remove the channels for the zones we are not using
206 if (numZones < MAX_ZONES) {
207 List<Channel> channels = new ArrayList<>(this.getThing().getChannels());
209 List<Integer> zonesToRemove = IntStream.range(numZones + 1, MAX_ZONES + 1).boxed()
210 .collect(Collectors.toList());
212 zonesToRemove.forEach(zone -> {
213 channels.removeIf(c -> (c.getUID().getId().contains(ZONE.toLowerCase() + zone)));
215 updateThing(editThing().withChannels(channels).build());
218 // initialize the all volume state
219 allVolume = initialAllVolume;
220 long allVolumePct = Math
221 .round((double) (initialAllVolume - MIN_VOLUME) / (double) (MAX_VOLUME - MIN_VOLUME) * 100.0);
222 updateState(ALL + CHANNEL_DELIMIT + CHANNEL_TYPE_ALLVOLUME, new PercentType(BigDecimal.valueOf(allVolumePct)));
224 scheduleReconnectJob();
225 schedulePollingJob();
227 updateStatus(ThingStatus.UNKNOWN);
231 public void dispose() {
232 cancelReconnectJob();
239 public void handleCommand(ChannelUID channelUID, Command command) {
240 String channel = channelUID.getId();
241 String[] channelSplit = channel.split(CHANNEL_DELIMIT);
242 MonopriceAudioZone zone = MonopriceAudioZone.valueOf(channelSplit[0].toUpperCase());
243 String channelType = channelSplit[1];
245 if (getThing().getStatus() != ThingStatus.ONLINE) {
246 logger.debug("Thing is not ONLINE; command {} from channel {} is ignored", command, channel);
250 boolean success = true;
251 synchronized (sequenceLock) {
252 if (!connector.isConnected()) {
253 logger.debug("Command {} from channel {} is ignored: connection not established", command, channel);
257 if (command instanceof RefreshType) {
258 MonopriceAudioZoneDTO zoneDTO = zoneDataMap.get(zone.getZoneId());
259 if (zoneDTO != null) {
260 updateChannelState(zone, channelType, zoneDTO);
262 logger.info("Could not execute REFRESH command for zone {}: null", zone.getZoneId());
267 Stream<String> zoneStream = MonopriceAudioZone.VALID_ZONES.stream().limit(numZones);
269 switch (channelType) {
270 case CHANNEL_TYPE_POWER:
271 if (command instanceof OnOffType) {
272 connector.sendCommand(zone, MonopriceAudioCommand.POWER, command == OnOffType.ON ? 1 : 0);
273 zoneDataMap.get(zone.getZoneId()).setPower(command == OnOffType.ON ? ON_STR : OFF_STR);
276 case CHANNEL_TYPE_SOURCE:
277 if (command instanceof DecimalType) {
278 int value = ((DecimalType) command).intValue();
279 if (value >= ONE && value <= MAX_SRC) {
280 logger.debug("Got source command {} zone {}", value, zone);
281 connector.sendCommand(zone, MonopriceAudioCommand.SOURCE, value);
282 zoneDataMap.get(zone.getZoneId()).setSource(String.format("%02d", value));
286 case CHANNEL_TYPE_VOLUME:
287 if (command instanceof PercentType) {
288 int value = (int) Math
289 .round(((PercentType) command).doubleValue() / 100.0 * (MAX_VOLUME - MIN_VOLUME))
291 logger.debug("Got volume command {} zone {}", value, zone);
292 connector.sendCommand(zone, MonopriceAudioCommand.VOLUME, value);
293 zoneDataMap.get(zone.getZoneId()).setVolume(value);
296 case CHANNEL_TYPE_MUTE:
297 if (command instanceof OnOffType) {
298 connector.sendCommand(zone, MonopriceAudioCommand.MUTE, command == OnOffType.ON ? 1 : 0);
299 zoneDataMap.get(zone.getZoneId()).setMute(command == OnOffType.ON ? ON_STR : OFF_STR);
302 case CHANNEL_TYPE_TREBLE:
303 if (command instanceof DecimalType) {
304 int value = ((DecimalType) command).intValue();
305 if (value >= MIN_TONE && value <= MAX_TONE) {
306 logger.debug("Got treble command {} zone {}", value, zone);
307 connector.sendCommand(zone, MonopriceAudioCommand.TREBLE, value + TONE_OFFSET);
308 zoneDataMap.get(zone.getZoneId()).setTreble(value + TONE_OFFSET);
312 case CHANNEL_TYPE_BASS:
313 if (command instanceof DecimalType) {
314 int value = ((DecimalType) command).intValue();
315 if (value >= MIN_TONE && value <= MAX_TONE) {
316 logger.debug("Got bass command {} zone {}", value, zone);
317 connector.sendCommand(zone, MonopriceAudioCommand.BASS, value + TONE_OFFSET);
318 zoneDataMap.get(zone.getZoneId()).setBass(value + TONE_OFFSET);
322 case CHANNEL_TYPE_BALANCE:
323 if (command instanceof DecimalType) {
324 int value = ((DecimalType) command).intValue();
325 if (value >= MIN_BALANCE && value <= MAX_BALANCE) {
326 logger.debug("Got balance command {} zone {}", value, zone);
327 connector.sendCommand(zone, MonopriceAudioCommand.BALANCE, value + BALANCE_OFFSET);
328 zoneDataMap.get(zone.getZoneId()).setBalance(value + BALANCE_OFFSET);
332 case CHANNEL_TYPE_DND:
333 if (command instanceof OnOffType) {
334 connector.sendCommand(zone, MonopriceAudioCommand.DND, command == OnOffType.ON ? 1 : 0);
335 zoneDataMap.get(zone.getZoneId()).setDnd(command == OnOffType.ON ? ON_STR : OFF_STR);
338 case CHANNEL_TYPE_ALLPOWER:
339 if (command instanceof OnOffType) {
340 zoneStream.forEach((zoneName) -> {
341 if (command == OnOffType.OFF || !ignoreZones.contains(zoneName)) {
343 connector.sendCommand(MonopriceAudioZone.valueOf(zoneName),
344 MonopriceAudioCommand.POWER, command == OnOffType.ON ? 1 : 0);
345 if (command == OnOffType.ON) {
346 // reset the volume of each zone to allVolume
347 connector.sendCommand(MonopriceAudioZone.valueOf(zoneName),
348 MonopriceAudioCommand.VOLUME, allVolume);
350 } catch (MonopriceAudioException e) {
351 logger.warn("Error Turning All Zones On: {}", e.getMessage());
358 case CHANNEL_TYPE_ALLSOURCE:
359 if (command instanceof DecimalType) {
360 int value = ((DecimalType) command).intValue();
361 if (value >= ONE && value <= MAX_SRC) {
362 zoneStream.forEach((zoneName) -> {
363 if (!ignoreZones.contains(zoneName)) {
365 connector.sendCommand(MonopriceAudioZone.valueOf(zoneName),
366 MonopriceAudioCommand.SOURCE, value);
367 } catch (MonopriceAudioException e) {
368 logger.warn("Error Setting Source for All Zones: {}", e.getMessage());
375 case CHANNEL_TYPE_ALLVOLUME:
376 if (command instanceof PercentType) {
377 int value = (int) Math
378 .round(((PercentType) command).doubleValue() / 100.0 * (MAX_VOLUME - MIN_VOLUME))
381 zoneStream.forEach((zoneName) -> {
382 if (!ignoreZones.contains(zoneName)) {
384 connector.sendCommand(MonopriceAudioZone.valueOf(zoneName),
385 MonopriceAudioCommand.VOLUME, value);
386 } catch (MonopriceAudioException e) {
387 logger.warn("Error Setting Volume for All Zones: {}", e.getMessage());
393 case CHANNEL_TYPE_ALLMUTE:
394 if (command instanceof OnOffType) {
395 int cmd = command == OnOffType.ON ? 1 : 0;
396 zoneStream.forEach((zoneName) -> {
397 if (!ignoreZones.contains(zoneName)) {
399 connector.sendCommand(MonopriceAudioZone.valueOf(zoneName),
400 MonopriceAudioCommand.MUTE, cmd);
401 } catch (MonopriceAudioException e) {
402 logger.warn("Error Setting Mute for All Zones: {}", e.getMessage());
410 logger.debug("Command {} from channel {} failed: unexpected command", command, channel);
415 logger.trace("Command {} from channel {} succeeded", command, channel);
417 } catch (MonopriceAudioException e) {
418 logger.warn("Command {} from channel {} failed: {}", command, channel, e.getMessage());
419 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Sending command failed");
421 scheduleReconnectJob();
427 * Open the connection with the MonopriceAudio device
429 * @return true if the connection is opened successfully or false if not
431 private synchronized boolean openConnection() {
432 connector.addEventListener(this);
435 } catch (MonopriceAudioException e) {
436 logger.debug("openConnection() failed: {}", e.getMessage());
438 logger.debug("openConnection(): {}", connector.isConnected() ? "connected" : "disconnected");
439 return connector.isConnected();
443 * Close the connection with the MonopriceAudio device
445 private synchronized void closeConnection() {
446 if (connector.isConnected()) {
448 connector.removeEventListener(this);
449 logger.debug("closeConnection(): disconnected");
454 public void onNewMessageEvent(MonopriceAudioMessageEvent evt) {
455 String key = evt.getKey();
456 String updateData = evt.getValue().trim();
457 if (!MonopriceAudioConnector.KEY_ERROR.equals(key)) {
458 updateStatus(ThingStatus.ONLINE);
462 case MonopriceAudioConnector.KEY_ERROR:
463 logger.debug("Reading feedback message failed");
464 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Reading thread ended");
468 case MonopriceAudioConnector.KEY_ZONE_UPDATE:
469 String zoneId = updateData.substring(0, 2);
470 MonopriceAudioZoneDTO zoneDTO = zoneDataMap.get(zoneId);
471 if (MonopriceAudioZone.VALID_ZONE_IDS.contains(zoneId) && zoneDTO != null) {
472 MonopriceAudioZone targetZone = MonopriceAudioZone.fromZoneId(zoneId);
473 processZoneUpdate(targetZone, zoneDTO, updateData);
475 logger.warn("invalid event: {} for key: {} or zone data null", evt.getValue(), key);
479 logger.debug("onNewMessageEvent: unhandled key {}", key);
482 } catch (NumberFormatException e) {
483 logger.warn("Invalid value {} for key {}", updateData, key);
484 } catch (MonopriceAudioException e) {
485 logger.warn("Error processing zone update: {}", e.getMessage());
490 * Schedule the reconnection job
492 private void scheduleReconnectJob() {
493 logger.debug("Schedule reconnect job");
494 cancelReconnectJob();
495 reconnectJob = scheduler.scheduleWithFixedDelay(() -> {
496 synchronized (sequenceLock) {
497 if (!connector.isConnected()) {
498 logger.debug("Trying to reconnect...");
502 if (openConnection()) {
504 long prevUpdateTime = lastPollingUpdate;
505 connector.queryZone(MonopriceAudioZone.ZONE1);
507 // prevUpdateTime should have changed if a zone update was received
508 if (lastPollingUpdate == prevUpdateTime) {
509 error = "Controller not responding to status requests";
512 } catch (MonopriceAudioException e) {
513 error = "First command after connection failed";
514 logger.warn("{}: {}", error, e.getMessage());
518 error = "Reconnection failed";
521 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, error);
523 updateStatus(ThingStatus.ONLINE);
524 lastPollingUpdate = System.currentTimeMillis();
528 }, 1, RECON_POLLING_INTERVAL_SEC, TimeUnit.SECONDS);
532 * Cancel the reconnection job
534 private void cancelReconnectJob() {
535 ScheduledFuture<?> reconnectJob = this.reconnectJob;
536 if (reconnectJob != null) {
537 reconnectJob.cancel(true);
538 this.reconnectJob = null;
543 * Schedule the polling job
545 private void schedulePollingJob() {
546 logger.debug("Schedule polling job");
549 pollingJob = scheduler.scheduleWithFixedDelay(() -> {
550 synchronized (sequenceLock) {
551 if (connector.isConnected()) {
552 logger.debug("Polling the controller for updated status...");
554 // poll each zone up to the number of zones specified in the configuration
555 MonopriceAudioZone.VALID_ZONES.stream().limit(numZones).forEach((zoneName) -> {
557 connector.queryZone(MonopriceAudioZone.valueOf(zoneName));
558 } catch (MonopriceAudioException e) {
559 logger.warn("Polling error: {}", e.getMessage());
563 // if the last successful polling update was more than 2.25 intervals ago, the controller
564 // is either switched off or not responding even though the connection is still good
565 if ((System.currentTimeMillis() - lastPollingUpdate) > (pollingInterval * 2.25 * 1000)) {
566 logger.warn("Controller not responding to status requests");
567 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
568 "Controller not responding to status requests");
570 scheduleReconnectJob();
574 }, INITIAL_POLLING_DELAY_SEC, pollingInterval, TimeUnit.SECONDS);
578 * Cancel the polling job
580 private void cancelPollingJob() {
581 ScheduledFuture<?> pollingJob = this.pollingJob;
582 if (pollingJob != null) {
583 pollingJob.cancel(true);
584 this.pollingJob = null;
589 * Update the state of a channel
591 * @param channel the channel
593 private void updateChannelState(MonopriceAudioZone zone, String channelType, MonopriceAudioZoneDTO zoneData) {
594 String channel = zone.name().toLowerCase() + CHANNEL_DELIMIT + channelType;
596 if (!isLinked(channel)) {
600 State state = UnDefType.UNDEF;
601 switch (channelType) {
602 case CHANNEL_TYPE_POWER:
603 state = zoneData.isPowerOn() ? OnOffType.ON : OnOffType.OFF;
605 case CHANNEL_TYPE_SOURCE:
606 state = new DecimalType(zoneData.getSource());
608 case CHANNEL_TYPE_VOLUME:
609 long volumePct = Math.round(
610 (double) (zoneData.getVolume() - MIN_VOLUME) / (double) (MAX_VOLUME - MIN_VOLUME) * 100.0);
611 state = new PercentType(BigDecimal.valueOf(volumePct));
613 case CHANNEL_TYPE_MUTE:
614 state = zoneData.isMuted() ? OnOffType.ON : OnOffType.OFF;
616 case CHANNEL_TYPE_TREBLE:
617 state = new DecimalType(BigDecimal.valueOf(zoneData.getTreble() - TONE_OFFSET));
619 case CHANNEL_TYPE_BASS:
620 state = new DecimalType(BigDecimal.valueOf(zoneData.getBass() - TONE_OFFSET));
622 case CHANNEL_TYPE_BALANCE:
623 state = new DecimalType(BigDecimal.valueOf(zoneData.getBalance() - BALANCE_OFFSET));
625 case CHANNEL_TYPE_DND:
626 state = zoneData.isDndOn() ? OnOffType.ON : OnOffType.OFF;
628 case CHANNEL_TYPE_PAGE:
629 state = zoneData.isPageActive() ? OpenClosedType.OPEN : OpenClosedType.CLOSED;
631 case CHANNEL_TYPE_KEYPAD:
632 state = zoneData.isKeypadActive() ? OpenClosedType.OPEN : OpenClosedType.CLOSED;
637 updateState(channel, state);
640 private void processZoneUpdate(MonopriceAudioZone zone, MonopriceAudioZoneDTO zoneData, String newZoneData) {
641 // only process the update if something actually changed in this zone since the last time through
642 if (!newZoneData.equals(zoneData.toString())) {
643 // example status string: 1200010000130809100601, matcher pattern from above:
644 // "^(\\d{2})(\\d{2})(\\d{2})(\\d{2})(\\d{2})(\\d{2})(\\d{2})(\\d{2})(\\d{2})(\\d{2})(\\d{2})"
645 Matcher matcher = PATTERN.matcher(newZoneData);
646 if (matcher.find()) {
647 zoneData.setZone(matcher.group(1));
649 if (!matcher.group(2).equals(zoneData.getPage())) {
650 zoneData.setPage(matcher.group(2));
651 updateChannelState(zone, CHANNEL_TYPE_PAGE, zoneData);
654 if (!matcher.group(3).equals(zoneData.getPower())) {
655 zoneData.setPower(matcher.group(3));
656 updateChannelState(zone, CHANNEL_TYPE_POWER, zoneData);
659 if (!matcher.group(4).equals(zoneData.getMute())) {
660 zoneData.setMute(matcher.group(4));
661 updateChannelState(zone, CHANNEL_TYPE_MUTE, zoneData);
664 if (!matcher.group(5).equals(zoneData.getDnd())) {
665 zoneData.setDnd(matcher.group(5));
666 updateChannelState(zone, CHANNEL_TYPE_DND, zoneData);
669 int volume = Integer.parseInt(matcher.group(6));
670 if (volume != zoneData.getVolume()) {
671 zoneData.setVolume(volume);
672 updateChannelState(zone, CHANNEL_TYPE_VOLUME, zoneData);
675 int treble = Integer.parseInt(matcher.group(7));
676 if (treble != zoneData.getTreble()) {
677 zoneData.setTreble(treble);
678 updateChannelState(zone, CHANNEL_TYPE_TREBLE, zoneData);
681 int bass = Integer.parseInt(matcher.group(8));
682 if (bass != zoneData.getBass()) {
683 zoneData.setBass(bass);
684 updateChannelState(zone, CHANNEL_TYPE_BASS, zoneData);
687 int balance = Integer.parseInt(matcher.group(9));
688 if (balance != zoneData.getBalance()) {
689 zoneData.setBalance(balance);
690 updateChannelState(zone, CHANNEL_TYPE_BALANCE, zoneData);
693 if (!matcher.group(10).equals(zoneData.getSource())) {
694 zoneData.setSource(matcher.group(10));
695 updateChannelState(zone, CHANNEL_TYPE_SOURCE, zoneData);
698 if (!matcher.group(11).equals(zoneData.getKeypad())) {
699 zoneData.setKeypad(matcher.group(11));
700 updateChannelState(zone, CHANNEL_TYPE_KEYPAD, zoneData);
703 logger.debug("Invalid zone update message: {}", newZoneData);
707 lastPollingUpdate = System.currentTimeMillis();