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.russound.internal.rio.zone;
15 import java.util.ArrayList;
16 import java.util.List;
17 import java.util.Objects;
18 import java.util.concurrent.atomic.AtomicInteger;
19 import java.util.regex.Matcher;
20 import java.util.regex.Pattern;
22 import org.openhab.binding.russound.internal.net.SocketSession;
23 import org.openhab.binding.russound.internal.net.SocketSessionListener;
24 import org.openhab.binding.russound.internal.rio.AbstractRioProtocol;
25 import org.openhab.binding.russound.internal.rio.RioConstants;
26 import org.openhab.binding.russound.internal.rio.RioHandlerCallback;
27 import org.openhab.binding.russound.internal.rio.RioPresetsProtocol;
28 import org.openhab.binding.russound.internal.rio.RioSystemFavoritesProtocol;
29 import org.openhab.binding.russound.internal.rio.models.GsonUtilities;
30 import org.openhab.binding.russound.internal.rio.models.RioFavorite;
31 import org.openhab.core.library.types.DecimalType;
32 import org.openhab.core.library.types.OnOffType;
33 import org.openhab.core.library.types.PercentType;
34 import org.openhab.core.library.types.StringType;
35 import org.slf4j.Logger;
36 import org.slf4j.LoggerFactory;
38 import com.google.gson.Gson;
39 import com.google.gson.JsonSyntaxException;
42 * This is the protocol handler for the Russound Zone. This handler will issue the protocol commands and will
43 * process the responses from the Russound system.
45 * @author Tim Roberts - Initial contribution
47 class RioZoneProtocol extends AbstractRioProtocol
48 implements RioSystemFavoritesProtocol.Listener, RioPresetsProtocol.Listener {
50 private final Logger logger = LoggerFactory.getLogger(RioZoneProtocol.class);
53 * The controller identifier
55 private final int controller;
60 private final int zone;
63 private static final String ZONE_NAME = "name"; // 12 max
64 private static final String ZONE_SOURCE = "currentsource"; // 1-8 or 1-12
65 private static final String ZONE_BASS = "bass"; // -10 to 10
66 private static final String ZONE_TREBLE = "treble"; // -10 to 10
67 private static final String ZONE_BALANCE = "balance"; // -10 to 10
68 private static final String ZONE_LOUDNESS = "loudness"; // OFF/ON
69 private static final String ZONE_TURNONVOLUME = "turnonvolume"; // 0 to 50
70 private static final String ZONE_DONOTDISTURB = "donotdisturb"; // OFF/ON/SLAVE
71 private static final String ZONE_PARTYMODE = "partymode"; // OFF/ON/MASTER
72 private static final String ZONE_STATUS = "status"; // OFF/ON/MASTER
73 private static final String ZONE_VOLUME = "volume"; // 0 to 50
74 private static final String ZONE_MUTE = "mute"; // OFF/ON/MASTER
75 private static final String ZONE_PAGE = "page"; // OFF/ON/MASTER
76 private static final String ZONE_SHAREDSOURCE = "sharedsource"; // OFF/ON/MASTER
77 private static final String ZONE_SLEEPTIMEREMAINING = "sleeptimeremaining"; // OFF/ON/MASTER
78 private static final String ZONE_LASTERROR = "lasterror"; // OFF/ON/MASTER
79 private static final String ZONE_ENABLED = "enabled"; // OFF/ON
81 // Multimedia functions
82 private static final String ZONE_MMINIT = "MMInit"; // button
83 private static final String ZONE_MMCONTEXTMENU = "MMContextMenu"; // button
86 private static final String FAV_NAME = "name";
87 private static final String FAV_VALID = "valid";
90 private static final Pattern RSP_ZONENOTIFICATION = Pattern
91 .compile("(?i)^[SN] C\\[(\\d+)\\]\\.Z\\[(\\d+)\\]\\.(\\w+)=\"(.*)\"$");
93 private static final Pattern RSP_ZONEFAVORITENOTIFICATION = Pattern
94 .compile("(?i)^[SN] C\\[(\\d+)\\].Z\\[(\\d+)\\].favorite\\[(\\d+)\\].(\\w+)=\"(.*)\"$");
97 private final RioFavorite[] zoneFavorites = new RioFavorite[2];
99 // The current source identifier (or -1 if none)
100 private final AtomicInteger sourceId = new AtomicInteger(-1);
102 // GSON object used for json
103 private final Gson gson;
105 // The favorites protocol
106 private final RioSystemFavoritesProtocol favoritesProtocol;
108 // The presets protocol
109 private final RioPresetsProtocol presetsProtocol;
112 * Constructs the protocol handler from given parameters
114 * @param zone the zone identifier
115 * @param controller the controller identifier
116 * @param favoritesProtocol a non-null {@link RioSystemFavoritesProtocol}
117 * @param presetsProtocol a non-null {@link RioPresetsProtocol}
118 * @param session a non-null {@link SocketSession} (may be connected or disconnected)
119 * @param callback a non-null {@link RioHandlerCallback} to callback
121 RioZoneProtocol(int zone, int controller, RioSystemFavoritesProtocol favoritesProtocol,
122 RioPresetsProtocol presetsProtocol, SocketSession session, RioHandlerCallback callback) {
123 super(session, callback);
125 if (controller < 1 || controller > 6) {
126 throw new IllegalArgumentException("Controller must be between 1-6: " + controller);
128 if (zone < 1 || zone > 8) {
129 throw new IllegalArgumentException("Zone must be between 1-6: " + zone);
132 this.controller = controller;
135 this.favoritesProtocol = favoritesProtocol;
136 this.favoritesProtocol.addListener(this);
138 this.presetsProtocol = presetsProtocol;
139 this.presetsProtocol.addListener(this);
141 this.gson = GsonUtilities.createGson();
143 this.zoneFavorites[0] = new RioFavorite(1);
144 this.zoneFavorites[1] = new RioFavorite(2);
148 * Helper method to issue post online commands
153 refreshZoneEnabled();
156 systemFavoritesUpdated(favoritesProtocol.getJson());
160 * Helper method to refresh a system keyname
162 * @param keyname a non-null, non-empty keyname
163 * @throws IllegalArgumentException if keyname is null or empty
165 private void refreshZoneKey(String keyname) {
166 if (keyname == null || keyname.trim().length() == 0) {
167 throw new IllegalArgumentException("keyName cannot be null or empty");
170 sendCommand("GET C[" + controller + "].Z[" + zone + "]." + keyname);
174 * Refresh a zone name
176 void refreshZoneName() {
177 refreshZoneKey(ZONE_NAME);
181 * Refresh the zone's source
183 void refreshZoneSource() {
184 refreshZoneKey(ZONE_SOURCE);
188 * Refresh the zone's bass setting
190 void refreshZoneBass() {
191 refreshZoneKey(ZONE_BASS);
195 * Refresh the zone's treble setting
197 void refreshZoneTreble() {
198 refreshZoneKey(ZONE_TREBLE);
202 * Refresh the zone's balance setting
204 void refreshZoneBalance() {
205 refreshZoneKey(ZONE_BALANCE);
209 * Refresh the zone's loudness setting
211 void refreshZoneLoudness() {
212 refreshZoneKey(ZONE_LOUDNESS);
216 * Refresh the zone's turn on volume setting
218 void refreshZoneTurnOnVolume() {
219 refreshZoneKey(ZONE_TURNONVOLUME);
223 * Refresh the zone's do not disturb setting
225 void refreshZoneDoNotDisturb() {
226 refreshZoneKey(ZONE_DONOTDISTURB);
230 * Refresh the zone's party mode setting
232 void refreshZonePartyMode() {
233 refreshZoneKey(ZONE_PARTYMODE);
237 * Refresh the zone's status
239 void refreshZoneStatus() {
240 refreshZoneKey(ZONE_STATUS);
244 * Refresh the zone's volume setting
246 void refreshZoneVolume() {
247 refreshZoneKey(ZONE_VOLUME);
251 * Refresh the zone's mute setting
253 void refreshZoneMute() {
254 refreshZoneKey(ZONE_MUTE);
258 * Refresh the zone's paging setting
260 void refreshZonePage() {
261 refreshZoneKey(ZONE_PAGE);
265 * Refresh the zone's shared source setting
267 void refreshZoneSharedSource() {
268 refreshZoneKey(ZONE_SHAREDSOURCE);
272 * Refresh the zone's sleep time remaining setting
274 void refreshZoneSleepTimeRemaining() {
275 refreshZoneKey(ZONE_SLEEPTIMEREMAINING);
279 * Refresh the zone's last error
281 void refreshZoneLastError() {
282 refreshZoneKey(ZONE_LASTERROR);
286 * Refresh the zone's enabled setting
288 void refreshZoneEnabled() {
289 refreshZoneKey(ZONE_ENABLED);
293 * Refreshes the system favorites via {@link #favoritesProtocol}
295 void refreshSystemFavorites() {
296 favoritesProtocol.refreshSystemFavorites();
300 * Refreshes the zone favorites
302 void refreshZoneFavorites() {
303 for (int x = 1; x <= 2; x++) {
304 sendCommand("GET C[" + controller + "].Z[" + zone + "].favorite[" + x + "].valid");
305 sendCommand("GET C[" + controller + "].Z[" + zone + "].favorite[" + x + "].name");
310 * Refresh the zone preset via {@link #presetsProtocol}
312 void refreshZonePresets() {
313 presetsProtocol.refreshPresets();
317 * Turns on/off watching for zone notifications
319 * @param on true to turn on, false to turn off
321 void watchZone(boolean watch) {
322 sendCommand("WATCH C[" + controller + "].Z[" + zone + "] " + (watch ? "ON" : "OFF"));
326 * Set's the zone bass setting (from -10 to 10)
328 * @param bass the bass setting from -10 to 10
329 * @throws IllegalArgumentException if bass < -10 or > 10
331 void setZoneBass(int bass) {
332 if (bass < -10 || bass > 10) {
333 throw new IllegalArgumentException("Bass must be between -10 and 10: " + bass);
335 sendCommand("SET C[" + controller + "].Z[" + zone + "]." + ZONE_BASS + "=\"" + bass + "\"");
339 * Set's the zone treble setting (from -10 to 10)
341 * @param treble the treble setting from -10 to 10
342 * @throws IllegalArgumentException if treble < -10 or > 10
344 void setZoneTreble(int treble) {
345 if (treble < -10 || treble > 10) {
346 throw new IllegalArgumentException("Treble must be between -10 and 10: " + treble);
348 sendCommand("SET C[" + controller + "].Z[" + zone + "]." + ZONE_TREBLE + "=\"" + treble + "\"");
352 * Set's the zone balance setting (from -10 [full left] to 10 [full right])
354 * @param balance the balance setting from -10 to 10
355 * @throws IllegalArgumentException if balance < -10 or > 10
357 void setZoneBalance(int balance) {
358 if (balance < -10 || balance > 10) {
359 throw new IllegalArgumentException("Balance must be between -10 and 10: " + balance);
361 sendCommand("SET C[" + controller + "].Z[" + zone + "]." + ZONE_BALANCE + "=\"" + balance + "\"");
365 * Set's the zone's loudness
367 * @param on true to turn on loudness, false to turn off
369 void setZoneLoudness(boolean on) {
370 sendCommand("SET C[" + controller + "].Z[" + zone + "]." + ZONE_LOUDNESS + "=\"" + (on ? "ON" : "OFF") + "\"");
374 * Set's the zone turn on volume (will be scaled between 0 and 50)
376 * @param volume the turn on volume (between 0 and 1)
377 * @throws IllegalArgumentException if volume < 0 or > 1
379 void setZoneTurnOnVolume(double volume) {
380 if (volume < 0 || volume > 1) {
381 throw new IllegalArgumentException("Volume must be between 0 and 1: " + volume);
384 final int scaledVolume = (int) ((volume * 100) / 2);
385 sendCommand("SET C[" + controller + "].Z[" + zone + "]." + ZONE_TURNONVOLUME + "=\"" + scaledVolume + "\"");
389 * Set's the zone sleep time remaining in seconds (from 0 to 60). Will be rounded to nearest 5 (37 will become 35,
390 * 38 will become 40).
392 * @param sleepTime the sleeptime in seconds
393 * @throws IllegalArgumentException if sleepTime < 0 or > 60
395 void setZoneSleepTimeRemaining(int sleepTime) {
396 if (sleepTime < 0 || sleepTime > 60) {
397 throw new IllegalArgumentException("Sleep Time Remaining must be between 0 and 60: " + sleepTime);
399 sleepTime = (int) (5 * Math.round(sleepTime / 5.0));
400 sendCommand("SET C[" + controller + "].Z[" + zone + "]." + ZONE_SLEEPTIMEREMAINING + "=\"" + sleepTime + "\"");
404 * Set's the zone source (physical source from 1 to 12)
406 * @param source the source (1 to 12)
407 * @throws IllegalArgumentException if source is < 1 or > 12
409 void setZoneSource(int source) {
410 if (source < 1 || source > 12) {
411 throw new IllegalArgumentException("Source must be between 1 and 12");
413 sendCommand("EVENT C[" + controller + "].Z[" + zone + "]!SelectSource " + source);
417 * Set's the zone's status
419 * @param on true to turn on, false otherwise
421 void setZoneStatus(boolean on) {
422 sendCommand("EVENT C[" + controller + "].Z[" + zone + "]!Zone" + (on ? "On" : "Off"));
426 * Set's the zone's partymode (supports on/off/master). Case does not matter - will be
427 * converted to uppercase for the system.
429 * @param partyMode a non-null, non-empty party mode
430 * @throws IllegalArgumentException if partymode is null, empty or not (on/off/master).
432 void setZonePartyMode(String partyMode) {
433 if (partyMode == null || partyMode.trim().length() == 0) {
434 throw new IllegalArgumentException("PartyMode cannot be null or empty");
436 if ("|on|off|master|".indexOf("|" + partyMode + "|") == -1) {
437 throw new IllegalArgumentException(
438 "Party mode can only be set to on, off or master: " + partyMode.toUpperCase());
440 sendCommand("EVENT C[" + controller + "].Z[" + zone + "]!PartyMode " + partyMode);
444 * Set's the zone's do not disturb (supports on/off/slave). Case does not matter - will be
445 * converted to uppercase for the system. Please note that slave will be translated to "ON" but may be refreshed
446 * back to "SLAVE" if a master zone has been designated
448 * @param doNotDisturb a non-null, non-empty do not disturb mode
449 * @throws IllegalArgumentException if doNotDisturb is null, empty or not (on/off/slave).
451 void setZoneDoNotDisturb(String doNotDisturb) {
452 if (doNotDisturb == null || doNotDisturb.trim().length() == 0) {
453 throw new IllegalArgumentException("Do Not Disturb cannot be null or empty");
455 if ("|on|off|slave|".indexOf("|" + doNotDisturb + "|") == -1) {
456 throw new IllegalArgumentException("Do Not Disturb can only be set to on, off or slave: " + doNotDisturb);
458 sendCommand("EVENT C[" + controller + "].Z[" + zone + "]!DoNotDisturb "
459 + ("off".equals(doNotDisturb) ? "OFF" : "ON")); // translate "slave" to "on"
463 * Sets the zone's volume level (scaled to 0-50)
465 * @param volume the volume level
466 * @throws IllegalArgumentException if volume is < 0 or > 1
468 void setZoneVolume(double volume) {
469 if (volume < 0 || volume > 1) {
470 throw new IllegalArgumentException("Volume must be between 0 and 1");
473 final int scaledVolume = (int) ((volume * 100) / 2);
474 sendKeyPress("Volume " + scaledVolume);
478 * Sets the volume up or down by 1
480 * @param increase true to increase by 1, false to decrease
482 void setZoneVolume(boolean increase) {
483 sendKeyPress("Volume" + (increase ? "Up" : "Down"));
487 * Toggles the zone's mute
489 void toggleZoneMute() {
490 sendKeyRelease("Mute");
494 * Toggles the zone's shuffle if the source supports shuffle mode
496 void toggleZoneShuffle() {
497 sendCommand("EVENT C[" + controller + "].Z[" + zone + "]!Shuffle");
501 * Toggles the zone's repeat if the source supports repeat mod
503 void toggleZoneRepeat() {
504 sendCommand("EVENT C[" + controller + "].Z[" + zone + "]!Repeat");
508 * Assign a rating to the current song if the source supports a rating
510 * @param like true to like, false to dislike
512 void setZoneRating(boolean like) {
513 sendCommand("EVENT C[" + controller + "].Z[" + zone + "]!MMRate " + (like ? "hi" : "low"));
517 * Sets the system favorite based on what is currently being played in the zone via {@link #favoritesProtocol}
519 * @param favJson a possibly null, possibly empty JSON of favorites to set
521 void setSystemFavorites(String favJson) {
522 favoritesProtocol.setSystemFavorites(controller, zone, favJson);
526 * Sets the zone favorites to what is currently playing
528 * @param favJson a possibly null, possibly empty json for favorites to set
529 * @return a non-null {@link Runnable} that should be run after the call
531 Runnable setZoneFavorites(String favJson) {
532 if (favJson.isEmpty()) {
537 final List<Integer> updateFavIds = new ArrayList<>();
539 final RioFavorite[] favs = gson.fromJson(favJson, RioFavorite[].class);
540 for (int x = favs.length - 1; x >= 0; x--) {
541 final RioFavorite fav = favs[x];
543 continue;// caused by {id,valid,name},,{id,valid,name}
545 final int favId = fav.getId();
546 if (favId < 1 || favId > 2) {
547 logger.debug("Invalid favorite id (not between 1 and 2) - ignoring: {}:{}", favId, favJson);
549 final RioFavorite myFav = zoneFavorites[favId - 1];
550 final boolean favValid = fav.isValid();
551 final String favName = fav.getName();
553 if (!Objects.equals(myFav.getName(), favName) || myFav.isValid() != favValid) {
554 myFav.setName(favName);
555 myFav.setValid(favValid);
557 sendEvent("saveZoneFavorite \"" + favName + "\" " + favId);
558 updateFavIds.add(favId);
560 sendEvent("deleteZoneFavorite " + favId);
565 } catch (JsonSyntaxException e) {
566 logger.debug("Invalid JSON: {}", e.getMessage(), e);
568 // regardless of what happens above - reupdate the channel
569 // (to remove anything bad from it)
571 for (Integer favId : updateFavIds) {
572 sendCommand("GET C[" + controller + "].Z[" + zone + "].favorite[" + favId + "].valid");
573 sendCommand("GET C[" + controller + "].Z[" + zone + "].favorite[" + favId + "].name");
575 updateZoneFavoritesChannel();
580 * Sets the zone presets for what is currently playing via {@link #presetsProtocol}
582 * @param presetJson a possibly empty, possibly null preset json
584 void setZonePresets(String presetJson) {
585 presetsProtocol.setZonePresets(controller, zone, sourceId.get(), presetJson);
589 * Sends a KeyPress instruction to the zone
591 * @param keyPress a non-null, non-empty string to send
592 * @throws IllegalArgumentException if keyPress is null or empty
594 void sendKeyPress(String keyPress) {
595 if (keyPress == null || keyPress.trim().length() == 0) {
596 throw new IllegalArgumentException("keyPress cannot be null or empty");
598 sendCommand("EVENT C[" + controller + "].Z[" + zone + "]!KeyPress " + keyPress);
602 * Sends a KeyRelease instruction to the zone
604 * @param keyRelease a non-null, non-empty string to send
605 * @throws IllegalArgumentException if keyRelease is null or empty
607 void sendKeyRelease(String keyRelease) {
608 if (keyRelease == null || keyRelease.trim().length() == 0) {
609 throw new IllegalArgumentException("keyRelease cannot be null or empty");
611 sendCommand("EVENT C[" + controller + "].Z[" + zone + "]!KeyRelease " + keyRelease);
615 * Sends a KeyHold instruction to the zone
617 * @param keyHold a non-null, non-empty string to send
618 * @throws IllegalArgumentException if keyHold is null or empty
620 void sendKeyHold(String keyHold) {
621 if (keyHold == null || keyHold.trim().length() == 0) {
622 throw new IllegalArgumentException("keyHold cannot be null or empty");
624 sendCommand("EVENT C[" + controller + "].Z[" + zone + "]!KeyHold " + keyHold);
628 * Sends a KeyCode instruction to the zone
630 * @param keyCode a non-null, non-empty string to send
631 * @throws IllegalArgumentException if keyCode is null or empty
633 void sendKeyCode(String keyCode) {
634 if (keyCode == null || keyCode.trim().length() == 0) {
635 throw new IllegalArgumentException("keyCode cannot be null or empty");
637 sendCommand("EVENT C[" + controller + "].Z[" + zone + "]!KeyCode " + keyCode);
641 * Sends an EVENT instruction to the zone
643 * @param event a non-null, non-empty string to send
644 * @throws IllegalArgumentException if event is null or empty
646 void sendEvent(String event) {
647 if (event == null || event.trim().length() == 0) {
648 throw new IllegalArgumentException("event cannot be null or empty");
650 sendCommand("EVENT C[" + controller + "].Z[" + zone + "]!" + event);
654 * Sends the MMInit [home screen] command
657 sendEvent("MMVerbosity 2");
658 sendEvent("MMIndex ABSOLUTE");
659 sendEvent("MMFormat JSON");
660 sendEvent("MMUseBlockInfo TRUE");
661 sendEvent("MMUseForms FALSE");
662 sendEvent("MMMaxItems 25");
664 sendEvent(ZONE_MMINIT);
668 * Requests a context menu
670 void sendMMContextMenu() {
671 sendEvent("MMVerbosity 2");
672 sendEvent("MMIndex ABSOLUTE");
673 sendEvent("MMFormat JSON");
674 sendEvent("MMUseBlockInfo TRUE");
675 sendEvent("MMUseForms FALSE");
676 sendEvent("MMMaxItems 25");
678 sendEvent(ZONE_MMCONTEXTMENU);
682 * Handles any zone notifications returned by the russound system
684 * @param m a non-null matcher
685 * @param resp a possibly null, possibly empty response
687 private void handleZoneNotification(Matcher m, String resp) {
689 throw new IllegalArgumentException("m (matcher) cannot be null");
691 if (m.groupCount() == 4) {
693 final int notifyController = Integer.parseInt(m.group(1));
694 if (notifyController != controller) {
697 final int notifyZone = Integer.parseInt(m.group(2));
698 if (notifyZone != zone) {
701 final String key = m.group(3).toLowerCase();
702 final String value = m.group(4);
706 stateChanged(RioConstants.CHANNEL_ZONENAME, new StringType(value));
711 final int nbr = Integer.parseInt(value);
712 stateChanged(RioConstants.CHANNEL_ZONESOURCE, new DecimalType(nbr));
714 if (nbr != sourceId.getAndSet(nbr)) {
716 presetsUpdated(nbr, presetsProtocol.getJson(nbr));
718 } catch (NumberFormatException e) {
719 logger.warn("Invalid zone notification (source not parsable): '{}')", resp);
725 final int nbr = Integer.parseInt(value);
726 stateChanged(RioConstants.CHANNEL_ZONEBASS, new DecimalType(nbr));
727 } catch (NumberFormatException e) {
728 logger.warn("Invalid zone notification (bass not parsable): '{}')", resp);
734 final int nbr = Integer.parseInt(value);
735 stateChanged(RioConstants.CHANNEL_ZONETREBLE, new DecimalType(nbr));
736 } catch (NumberFormatException e) {
737 logger.warn("Invalid zone notification (treble not parsable): '{}')", resp);
743 final int nbr = Integer.parseInt(value);
744 stateChanged(RioConstants.CHANNEL_ZONEBALANCE, new DecimalType(nbr));
745 } catch (NumberFormatException e) {
746 logger.warn("Invalid zone notification (balance not parsable): '{}')", resp);
751 stateChanged(RioConstants.CHANNEL_ZONELOUDNESS, OnOffType.from("ON".equals(value)));
754 case ZONE_TURNONVOLUME:
756 final int nbr = Integer.parseInt(value);
757 stateChanged(RioConstants.CHANNEL_ZONETURNONVOLUME, new PercentType(nbr * 2));
758 } catch (NumberFormatException e) {
759 logger.warn("Invalid zone notification (turnonvolume not parsable): '{}')", resp);
763 case ZONE_DONOTDISTURB:
764 stateChanged(RioConstants.CHANNEL_ZONEDONOTDISTURB, new StringType(value));
768 stateChanged(RioConstants.CHANNEL_ZONEPARTYMODE, new StringType(value));
772 stateChanged(RioConstants.CHANNEL_ZONESTATUS, OnOffType.from("ON".equals(value)));
775 stateChanged(RioConstants.CHANNEL_ZONEMUTE, OnOffType.from("ON".equals(value)));
778 case ZONE_SHAREDSOURCE:
779 stateChanged(RioConstants.CHANNEL_ZONESHAREDSOURCE, OnOffType.from("ON".equals(value)));
783 stateChanged(RioConstants.CHANNEL_ZONELASTERROR, new StringType(value));
787 stateChanged(RioConstants.CHANNEL_ZONEPAGE, OnOffType.from("ON".equals(value)));
790 case ZONE_SLEEPTIMEREMAINING:
792 final int nbr = Integer.parseInt(value);
793 stateChanged(RioConstants.CHANNEL_ZONESLEEPTIMEREMAINING, new DecimalType(nbr));
794 } catch (NumberFormatException e) {
795 logger.warn("Invalid zone notification (sleeptimeremaining not parsable): '{}')", resp);
800 stateChanged(RioConstants.CHANNEL_ZONEENABLED, OnOffType.from("ON".equals(value)));
805 final int nbr = Integer.parseInt(value);
806 stateChanged(RioConstants.CHANNEL_ZONEVOLUME, new PercentType(nbr * 2));
807 } catch (NumberFormatException e) {
808 logger.warn("Invalid zone notification (volume not parsable): '{}')", resp);
813 logger.warn("Unknown zone notification: '{}'", resp);
816 } catch (NumberFormatException e) {
817 logger.warn("Invalid Zone Notification (controller/zone not a parsable integer): '{}')", resp);
820 logger.warn("Invalid Zone Notification response: '{}'", resp);
825 * Handles any system notifications returned by the russound system
827 * @param m a non-null matcher
828 * @param resp a possibly null, possibly empty response
830 void handleZoneFavoriteNotification(Matcher m, String resp) {
832 throw new IllegalArgumentException("m (matcher) cannot be null");
834 if (m.groupCount() == 5) {
836 final int notifyController = Integer.parseInt(m.group(1));
837 if (notifyController != controller) {
840 final int notifyZone = Integer.parseInt(m.group(2));
841 if (notifyZone != zone) {
845 final int favoriteId = Integer.parseInt(m.group(3));
847 if (favoriteId >= 1 && favoriteId <= 2) {
848 final RioFavorite fav = zoneFavorites[favoriteId - 1];
850 final String key = m.group(4);
851 final String value = m.group(5);
856 updateZoneFavoritesChannel();
859 fav.setValid(!"false".equalsIgnoreCase(value));
860 updateZoneFavoritesChannel();
864 logger.warn("Unknown zone favorite notification: '{}'", resp);
868 logger.warn("Invalid Zone Favorite Notification (favorite < 1 or > 2): '{}')", resp);
870 } catch (NumberFormatException e) {
871 logger.warn("Invalid Zone Favorite Notification (favorite not a parsable integer): '{}')", resp);
874 logger.warn("Invalid Zone Notification response: '{}'", resp);
879 * Will update the zone favorites channel with only valid favorites
881 private void updateZoneFavoritesChannel() {
882 final List<RioFavorite> favs = new ArrayList<>();
883 for (final RioFavorite fav : zoneFavorites) {
889 final String favJson = gson.toJson(favs);
890 stateChanged(RioConstants.CHANNEL_ZONEFAVORITES, new StringType(favJson));
894 * Callback method when system favorites are updated. Simply issues a state change for the zone system favorites
895 * channel using the jsonString as the value
898 public void systemFavoritesUpdated(String jsonString) {
899 stateChanged(RioConstants.CHANNEL_ZONESYSFAVORITES, new StringType(jsonString));
903 * Callback method when presets are updated. Simply issues a state change for the zone presets channel using the
904 * jsonString as the value
907 public void presetsUpdated(int sourceIdUpdated, String jsonString) {
908 if (sourceIdUpdated != sourceId.get()) {
911 stateChanged(RioConstants.CHANNEL_ZONEPRESETS, new StringType(jsonString));
915 * Implements {@link SocketSessionListener#responseReceived(String)} to try to process the response from the
916 * russound system. This response may be for other protocol handler - so ignore if we don't recognize the response.
918 * @param a possibly null, possibly empty response
921 public void responseReceived(String response) {
922 if (response == null || response.isEmpty()) {
926 Matcher m = RSP_ZONENOTIFICATION.matcher(response);
928 handleZoneNotification(m, response);
931 m = RSP_ZONEFAVORITENOTIFICATION.matcher(response);
933 handleZoneFavoriteNotification(m, response);
938 * Overrides the default implementation to turn watch off ({@link #watchZone(boolean)}) before calling the dispose
941 public void dispose() {
943 favoritesProtocol.removeListener(this);