]> git.basschouten.com Git - openhab-addons.git/blob
7090456ac0adb95ffec2411b689fa8ba84e6dcc8
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2023 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
7  * This program and the accompanying materials are made available under the
8  * terms of the Eclipse Public License 2.0 which is available at
9  * http://www.eclipse.org/legal/epl-2.0
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.binding.russound.internal.rio.source;
14
15 import java.util.Collections;
16 import java.util.HashMap;
17 import java.util.Map;
18 import java.util.Objects;
19 import java.util.concurrent.atomic.AtomicInteger;
20 import java.util.concurrent.locks.Lock;
21 import java.util.concurrent.locks.ReentrantLock;
22 import java.util.regex.Matcher;
23 import java.util.regex.Pattern;
24
25 import org.eclipse.jdt.annotation.Nullable;
26 import org.eclipse.jetty.client.HttpClient;
27 import org.openhab.binding.russound.internal.net.SocketSession;
28 import org.openhab.binding.russound.internal.net.SocketSessionListener;
29 import org.openhab.binding.russound.internal.rio.AbstractRioProtocol;
30 import org.openhab.binding.russound.internal.rio.RioConstants;
31 import org.openhab.binding.russound.internal.rio.RioHandlerCallback;
32 import org.openhab.binding.russound.internal.rio.StatefulHandlerCallback;
33 import org.openhab.binding.russound.internal.rio.models.GsonUtilities;
34 import org.openhab.binding.russound.internal.rio.models.RioBank;
35 import org.openhab.core.library.types.StringType;
36 import org.openhab.core.types.State;
37 import org.slf4j.Logger;
38 import org.slf4j.LoggerFactory;
39
40 import com.google.gson.Gson;
41 import com.google.gson.JsonSyntaxException;
42
43 /**
44  * This is the protocol handler for the Russound Source. This handler will issue the protocol commands and will
45  * process the responses from the Russound system. Please see documentation for what channels are supported by which
46  * source types.
47  *
48  * @author Tim Roberts - Initial contribution
49  */
50 class RioSourceProtocol extends AbstractRioProtocol {
51     private final Logger logger = LoggerFactory.getLogger(RioSourceProtocol.class);
52
53     /**
54      * The source identifier (1-12)
55      */
56     private final int source;
57
58     // Protocol constants
59     private static final String SRC_NAME = "name";
60     private static final String SRC_TYPE = "type";
61     private static final String SRC_IPADDRESS = "ipaddress";
62     private static final String SRC_COMPOSERNAME = "composername";
63     private static final String SRC_CHANNEL = "channel";
64     private static final String SRC_CHANNELNAME = "channelname";
65     private static final String SRC_GENRE = "genre";
66     private static final String SRC_ARTISTNAME = "artistname";
67     private static final String SRC_ALBUMNAME = "albumname";
68     private static final String SRC_COVERARTURL = "coverarturl";
69     private static final String SRC_PLAYLISTNAME = "playlistname";
70     private static final String SRC_SONGNAME = "songname";
71     private static final String SRC_MODE = "mode";
72     private static final String SRC_SHUFFLEMODE = "shufflemode";
73     private static final String SRC_REPEATMODE = "repeatmode";
74     private static final String SRC_RATING = "rating";
75     private static final String SRC_PROGRAMSERVICENAME = "programservicename";
76     private static final String SRC_RADIOTEXT = "radiotext";
77     private static final String SRC_RADIOTEXT2 = "radiotext2";
78     private static final String SRC_RADIOTEXT3 = "radiotext3";
79     private static final String SRC_RADIOTEXT4 = "radiotext4";
80
81     // Multimedia channels
82     private static final String SRC_MMSCREEN = "mmscreen";
83     private static final String SRC_MMTITLE = "mmtitle.text";
84     private static final String SRC_MMATTR = "attr";
85     private static final String SRC_MMBTNOK = "mmbtnok.text";
86     private static final String SRC_MMBTNBACK = "mmbtnback.text";
87     private static final String SRC_MMINFOBLOCK = "mminfoblock.text";
88
89     private static final String SRC_MMHELP = "mmhelp.text";
90     private static final String SRC_MMTEXTFIELD = "mmtextfield.text";
91
92     // This is an undocumented volume
93     private static final String SRC_VOLUME = "volume";
94
95     private static final String BANK_NAME = "name";
96
97     // Response patterns
98     private static final Pattern RSP_MMMENUNOTIFICATION = Pattern.compile("^\\{.*\\}$");
99     private static final Pattern RSP_SRCNOTIFICATION = Pattern
100             .compile("(?i)^[SN] S\\[(\\d+)\\]\\.([a-zA-Z_0-9.\\[\\]]+)=\"(.*)\"$");
101     private static final Pattern RSP_BANKNOTIFICATION = Pattern
102             .compile("(?i)^[SN] S\\[(\\d+)\\].B\\[(\\d+)\\].(\\w+)=\"(.*)\"$");
103     private static final Pattern RSP_PRESETNOTIFICATION = Pattern
104             .compile("(?i)^[SN] S\\[(\\d+)\\].B\\[(\\d+)\\].P\\[(\\d+)\\].(\\w+)=\"(.*)\"$");
105
106     /**
107      * Current banks
108      */
109     private final RioBank[] banks = new RioBank[6];
110
111     /**
112      * {@link Gson} use to create/read json
113      */
114     private final Gson gson;
115
116     /**
117      * Lock used to control access to {@link #infoText}
118      */
119     private final Lock infoLock = new ReentrantLock();
120
121     /**
122      * The information text appeneded from media management calls
123      */
124     private final StringBuilder infoText = new StringBuilder(100);
125
126     /**
127      * The table of channels to unique identifiers for media management functions
128      */
129     @SuppressWarnings("serial")
130     private final Map<String, AtomicInteger> mmSeqNbrs = Collections
131             .unmodifiableMap(new HashMap<String, AtomicInteger>() {
132                 {
133                     put(RioConstants.CHANNEL_SOURCEMMMENU, new AtomicInteger(0));
134                     put(RioConstants.CHANNEL_SOURCEMMSCREEN, new AtomicInteger(0));
135                     put(RioConstants.CHANNEL_SOURCEMMTITLE, new AtomicInteger(0));
136                     put(RioConstants.CHANNEL_SOURCEMMATTR, new AtomicInteger(0));
137                     put(RioConstants.CHANNEL_SOURCEMMBUTTONOKTEXT, new AtomicInteger(0));
138                     put(RioConstants.CHANNEL_SOURCEMMBUTTONBACKTEXT, new AtomicInteger(0));
139                     put(RioConstants.CHANNEL_SOURCEMMINFOTEXT, new AtomicInteger(0));
140                     put(RioConstants.CHANNEL_SOURCEMMHELPTEXT, new AtomicInteger(0));
141                     put(RioConstants.CHANNEL_SOURCEMMTEXTFIELD, new AtomicInteger(0));
142                 }
143             });
144
145     /**
146      * The client used for http requests
147      */
148     private final HttpClient httpClient;
149
150     /**
151      * Constructs the protocol handler from given parameters
152      *
153      * @param source the source identifier
154      * @param session a non-null {@link SocketSession} (may be connected or disconnected)
155      * @param callback a non-null {@link RioHandlerCallback} to callback
156      * @throws Exception exception when starting the {@link HttpClient}
157      */
158     RioSourceProtocol(int source, SocketSession session, RioHandlerCallback callback) throws Exception {
159         super(session, callback);
160         if (source < 1 || source > 12) {
161             throw new IllegalArgumentException("Source must be between 1-12: " + source);
162         }
163         this.source = source;
164         httpClient = new HttpClient();
165         httpClient.setFollowRedirects(true);
166         httpClient.start();
167
168         gson = GsonUtilities.createGson();
169
170         for (int x = 1; x <= 6; x++) {
171             banks[x - 1] = new RioBank(x);
172         }
173     }
174
175     /**
176      * Helper method to issue post online commands
177      */
178     void postOnline() {
179         watchSource(true);
180         refreshSourceIpAddress();
181         refreshSourceName();
182
183         updateBanksChannel();
184     }
185
186     /**
187      * Helper method to refresh a source key
188      *
189      * @param keyName a non-null, non-empty source key to refresh
190      * @throws IllegalArgumentException if keyName is null or empty
191      */
192     private void refreshSourceKey(String keyName) {
193         if (keyName == null || keyName.trim().length() == 0) {
194             throw new IllegalArgumentException("keyName cannot be null or empty");
195         }
196         sendCommand("GET S[" + source + "]." + keyName);
197     }
198
199     /**
200      * Refreshes the source name
201      */
202     void refreshSourceName() {
203         refreshSourceKey(SRC_NAME);
204     }
205
206     /**
207      * Refresh the source model type
208      */
209     void refreshSourceType() {
210         refreshSourceKey(SRC_TYPE);
211     }
212
213     /**
214      * Refresh the source ip address
215      */
216     void refreshSourceIpAddress() {
217         refreshSourceKey(SRC_IPADDRESS);
218     }
219
220     /**
221      * Refresh composer name
222      */
223     void refreshSourceComposerName() {
224         refreshSourceKey(SRC_COMPOSERNAME);
225     }
226
227     /**
228      * Refresh the channel frequency (for tuners)
229      */
230     void refreshSourceChannel() {
231         refreshSourceKey(SRC_CHANNEL);
232     }
233
234     /**
235      * Refresh the channel's name
236      */
237     void refreshSourceChannelName() {
238         refreshSourceKey(SRC_CHANNELNAME);
239     }
240
241     /**
242      * Refresh the song's genre
243      */
244     void refreshSourceGenre() {
245         refreshSourceKey(SRC_GENRE);
246     }
247
248     /**
249      * Refresh the artist name
250      */
251     void refreshSourceArtistName() {
252         refreshSourceKey(SRC_ARTISTNAME);
253     }
254
255     /**
256      * Refresh the album name
257      */
258     void refreshSourceAlbumName() {
259         refreshSourceKey(SRC_ALBUMNAME);
260     }
261
262     /**
263      * Refresh the cover art URL
264      */
265     void refreshSourceCoverArtUrl() {
266         refreshSourceKey(SRC_COVERARTURL);
267     }
268
269     /**
270      * Refresh the playlist name
271      */
272     void refreshSourcePlaylistName() {
273         refreshSourceKey(SRC_PLAYLISTNAME);
274     }
275
276     /**
277      * Refresh the song name
278      */
279     void refreshSourceSongName() {
280         refreshSourceKey(SRC_SONGNAME);
281     }
282
283     /**
284      * Refresh the provider mode/streaming service
285      */
286     void refreshSourceMode() {
287         refreshSourceKey(SRC_MODE);
288     }
289
290     /**
291      * Refresh the shuffle mode
292      */
293     void refreshSourceShuffleMode() {
294         refreshSourceKey(SRC_SHUFFLEMODE);
295     }
296
297     /**
298      * Refresh the repeat mode
299      */
300     void refreshSourceRepeatMode() {
301         refreshSourceKey(SRC_REPEATMODE);
302     }
303
304     /**
305      * Refresh the rating of the song
306      */
307     void refreshSourceRating() {
308         refreshSourceKey(SRC_RATING);
309     }
310
311     /**
312      * Refresh the program service name
313      */
314     void refreshSourceProgramServiceName() {
315         refreshSourceKey(SRC_PROGRAMSERVICENAME);
316     }
317
318     /**
319      * Refresh the radio text
320      */
321     void refreshSourceRadioText() {
322         refreshSourceKey(SRC_RADIOTEXT);
323     }
324
325     /**
326      * Refresh the radio text (line #2)
327      */
328     void refreshSourceRadioText2() {
329         refreshSourceKey(SRC_RADIOTEXT2);
330     }
331
332     /**
333      * Refresh the radio text (line #3)
334      */
335     void refreshSourceRadioText3() {
336         refreshSourceKey(SRC_RADIOTEXT3);
337     }
338
339     /**
340      * Refresh the radio text (line #4)
341      */
342     void refreshSourceRadioText4() {
343         refreshSourceKey(SRC_RADIOTEXT4);
344     }
345
346     /**
347      * Refresh the source volume
348      */
349     void refreshSourceVolume() {
350         refreshSourceKey(SRC_VOLUME);
351     }
352
353     /**
354      * Refreshes the names of the banks
355      */
356     void refreshBanks() {
357         for (int b = 1; b <= 6; b++) {
358             sendCommand("GET S[" + source + "].B[" + b + "]." + BANK_NAME);
359         }
360     }
361
362     /**
363      * Sets the bank names from the supplied bank JSON and returns a runnable to call {@link #updateBanksChannel()}
364      *
365      * @param bankJson a possibly null, possibly empty json containing the {@link RioBank} to update
366      * @return a non-null {@link Runnable} to execute after this call
367      */
368     Runnable setBanks(String bankJson) {
369         // If null or empty - simply return a do nothing runnable
370         if (bankJson == null || bankJson.isEmpty()) {
371             return () -> {
372             };
373         }
374
375         try {
376             final RioBank[] newBanks;
377             newBanks = gson.fromJson(bankJson, RioBank[].class);
378             for (int x = 0; x < newBanks.length; x++) {
379                 final RioBank bank = newBanks[x];
380                 if (bank == null) {
381                     continue; // caused by {id,valid,name},,{id,valid,name}
382                 }
383
384                 final int bankId = bank.getId();
385                 if (bankId < 1 || bankId > 6) {
386                     logger.debug("Invalid bank id (not between 1 and 6) - ignoring: {}:{}", bankId, bankJson);
387                 } else {
388                     final RioBank myBank = banks[bankId - 1];
389
390                     if (!Objects.equals(myBank.getName(), bank.getName())) {
391                         myBank.setName(bank.getName());
392                         sendCommand(
393                                 "SET S[" + source + "].B[" + bankId + "]." + BANK_NAME + "=\"" + bank.getName() + "\"");
394                     }
395                 }
396             }
397         } catch (JsonSyntaxException e) {
398             logger.debug("Invalid JSON: {}", e.getMessage(), e);
399         }
400
401         // regardless of what happens above - reupdate the channel
402         // (to remove anything bad from it)
403         return this::updateBanksChannel;
404     }
405
406     /**
407      * Helper method to simply update the banks channel. Will create a JSON representation from {@link #banks} and send
408      * it via the channel
409      */
410     private void updateBanksChannel() {
411         final String bankJson = gson.toJson(banks);
412         stateChanged(RioConstants.CHANNEL_SOURCEBANKS, new StringType(bankJson));
413     }
414
415     /**
416      * Turns on/off watching the source for notifications
417      *
418      * @param watch true to turn on, false to turn off
419      */
420     void watchSource(boolean watch) {
421         sendCommand("WATCH S[" + source + "] " + (watch ? "ON" : "OFF"));
422     }
423
424     /**
425      * Helper method to handle any media management change. If the channel is the INFO text channel, we delegate to
426      * {@link #handleMMInfoText(String)} instead. This helper method will simply get the next MM identifier and send the
427      * json representation out for the channel change (this ensures unique messages for each MM notification)
428      *
429      * @param channelId a non-null, non-empty channelId
430      * @param value the value for the channel
431      * @throws IllegalArgumentException if channelID is null or empty
432      */
433     private void handleMMChange(String channelId, String value) {
434         if (channelId == null || channelId.isEmpty()) {
435             throw new IllegalArgumentException("channelId cannot be null or empty");
436         }
437
438         final AtomicInteger ai = mmSeqNbrs.get(channelId);
439         if (ai == null) {
440             logger.error("Channel {} does not have an ID configuration - programmer error!", channelId);
441         } else {
442             if (channelId.equals(RioConstants.CHANNEL_SOURCEMMINFOTEXT)) {
443                 value = handleMMInfoText(value);
444                 if (value == null) {
445                     return;
446                 }
447             }
448
449             final int id = ai.getAndIncrement();
450
451             final String json = gson.toJson(new IdValue(id, value));
452             stateChanged(channelId, new StringType(json));
453         }
454     }
455
456     /**
457      * Helper method to handle MMInfoText notifications. There may be multiple infotext messages that represent a single
458      * message. We know when we get the last info text when the MMATTR contains an 'E' (last item). Once we have the
459      * last item, we update the channel with the complete message.
460      *
461      * @param infoTextValue the last info text value
462      * @return a non-null containing the complete or null if the message isn't complete yet
463      */
464     private String handleMMInfoText(String infoTextValue) {
465         final StatefulHandlerCallback callback = ((StatefulHandlerCallback) getCallback());
466
467         final State attr = callback.getProperty(RioConstants.CHANNEL_SOURCEMMATTR);
468
469         infoLock.lock();
470         try {
471             infoText.append(infoTextValue.toString());
472             if (attr != null && attr.toString().indexOf("E") >= 0) {
473                 final String text = infoText.toString();
474
475                 infoText.setLength(0);
476                 callback.removeState(RioConstants.CHANNEL_SOURCEMMATTR);
477
478                 return text;
479             }
480             return null;
481         } finally {
482             infoLock.unlock();
483         }
484     }
485
486     /**
487      * Handles any source notifications returned by the russound system
488      *
489      * @param m a non-null matcher
490      * @param resp a possibly null, possibly empty response
491      */
492     private void handleSourceNotification(Matcher m, String resp) {
493         if (m == null) {
494             throw new IllegalArgumentException("m (matcher) cannot be null");
495         }
496         if (m.groupCount() == 3) {
497             try {
498                 final int notifySource = Integer.parseInt(m.group(1));
499                 if (notifySource != source) {
500                     return;
501                 }
502                 final String key = m.group(2).toLowerCase();
503                 final String value = m.group(3);
504
505                 switch (key) {
506                     case SRC_NAME:
507                         stateChanged(RioConstants.CHANNEL_SOURCENAME, new StringType(value));
508                         break;
509
510                     case SRC_TYPE:
511                         stateChanged(RioConstants.CHANNEL_SOURCETYPE, new StringType(value));
512                         break;
513
514                     case SRC_IPADDRESS:
515                         setProperty(RioConstants.PROPERTY_SOURCEIPADDRESS, value);
516                         break;
517
518                     case SRC_COMPOSERNAME:
519                         stateChanged(RioConstants.CHANNEL_SOURCECOMPOSERNAME, new StringType(value));
520                         break;
521
522                     case SRC_CHANNEL:
523                         stateChanged(RioConstants.CHANNEL_SOURCECHANNEL, new StringType(value));
524                         break;
525
526                     case SRC_CHANNELNAME:
527                         stateChanged(RioConstants.CHANNEL_SOURCECHANNELNAME, new StringType(value));
528                         break;
529
530                     case SRC_GENRE:
531                         stateChanged(RioConstants.CHANNEL_SOURCEGENRE, new StringType(value));
532                         break;
533
534                     case SRC_ARTISTNAME:
535                         stateChanged(RioConstants.CHANNEL_SOURCEARTISTNAME, new StringType(value));
536                         break;
537
538                     case SRC_ALBUMNAME:
539                         stateChanged(RioConstants.CHANNEL_SOURCEALBUMNAME, new StringType(value));
540                         break;
541
542                     case SRC_COVERARTURL:
543                         stateChanged(RioConstants.CHANNEL_SOURCECOVERARTURL, new StringType(value));
544                         break;
545
546                     case SRC_PLAYLISTNAME:
547                         stateChanged(RioConstants.CHANNEL_SOURCEPLAYLISTNAME, new StringType(value));
548                         break;
549
550                     case SRC_SONGNAME:
551                         stateChanged(RioConstants.CHANNEL_SOURCESONGNAME, new StringType(value));
552                         break;
553
554                     case SRC_MODE:
555                         stateChanged(RioConstants.CHANNEL_SOURCEMODE, new StringType(value));
556                         break;
557
558                     case SRC_SHUFFLEMODE:
559                         stateChanged(RioConstants.CHANNEL_SOURCESHUFFLEMODE, new StringType(value));
560                         break;
561
562                     case SRC_REPEATMODE:
563                         stateChanged(RioConstants.CHANNEL_SOURCEREPEATMODE, new StringType(value));
564                         break;
565
566                     case SRC_RATING:
567                         stateChanged(RioConstants.CHANNEL_SOURCERATING, new StringType(value));
568                         break;
569
570                     case SRC_PROGRAMSERVICENAME:
571                         stateChanged(RioConstants.CHANNEL_SOURCEPROGRAMSERVICENAME, new StringType(value));
572                         break;
573
574                     case SRC_RADIOTEXT:
575                         stateChanged(RioConstants.CHANNEL_SOURCERADIOTEXT, new StringType(value));
576                         break;
577
578                     case SRC_RADIOTEXT2:
579                         stateChanged(RioConstants.CHANNEL_SOURCERADIOTEXT2, new StringType(value));
580                         break;
581
582                     case SRC_RADIOTEXT3:
583                         stateChanged(RioConstants.CHANNEL_SOURCERADIOTEXT3, new StringType(value));
584                         break;
585
586                     case SRC_RADIOTEXT4:
587                         stateChanged(RioConstants.CHANNEL_SOURCERADIOTEXT4, new StringType(value));
588                         break;
589
590                     case SRC_VOLUME:
591                         stateChanged(RioConstants.CHANNEL_SOURCEVOLUME, new StringType(value));
592                         break;
593
594                     case SRC_MMSCREEN:
595                         handleMMChange(RioConstants.CHANNEL_SOURCEMMSCREEN, value);
596                         break;
597
598                     case SRC_MMTITLE:
599                         handleMMChange(RioConstants.CHANNEL_SOURCEMMTITLE, value);
600                         break;
601
602                     case SRC_MMATTR:
603                         handleMMChange(RioConstants.CHANNEL_SOURCEMMATTR, value);
604                         break;
605
606                     case SRC_MMBTNOK:
607                         handleMMChange(RioConstants.CHANNEL_SOURCEMMBUTTONOKTEXT, value);
608                         break;
609
610                     case SRC_MMBTNBACK:
611                         handleMMChange(RioConstants.CHANNEL_SOURCEMMBUTTONBACKTEXT, value);
612                         break;
613
614                     case SRC_MMHELP:
615                         handleMMChange(RioConstants.CHANNEL_SOURCEMMHELPTEXT, value);
616                         break;
617
618                     case SRC_MMTEXTFIELD:
619                         handleMMChange(RioConstants.CHANNEL_SOURCEMMTEXTFIELD, value);
620                         break;
621
622                     case SRC_MMINFOBLOCK:
623                         handleMMChange(RioConstants.CHANNEL_SOURCEMMINFOTEXT, value);
624                         break;
625                     default:
626                         logger.warn("Unknown source notification: '{}'", resp);
627                         break;
628                 }
629             } catch (NumberFormatException e) {
630                 logger.warn("Invalid Source Notification (source not a parsable integer): '{}')", resp);
631             }
632         } else {
633             logger.warn("Invalid Source Notification response: '{}'", resp);
634         }
635     }
636
637     /**
638      * Handles any bank notifications returned by the russound system
639      *
640      * @param m a non-null matcher
641      * @param resp a possibly null, possibly empty response
642      */
643     private void handleBankNotification(Matcher m, String resp) {
644         if (m == null) {
645             throw new IllegalArgumentException("m (matcher) cannot be null");
646         }
647
648         // System notification
649         if (m.groupCount() == 4) {
650             try {
651                 final int bank = Integer.parseInt(m.group(2));
652                 if (bank >= 1 && bank <= 6) {
653                     final int notifySource = Integer.parseInt(m.group(1));
654                     if (notifySource != source) {
655                         return;
656                     }
657
658                     final String key = m.group(3).toLowerCase();
659                     final String value = m.group(4);
660
661                     switch (key) {
662                         case BANK_NAME:
663                             banks[bank - 1].setName(value);
664                             updateBanksChannel();
665                             break;
666
667                         default:
668                             logger.warn("Unknown bank name notification: '{}'", resp);
669                             break;
670                     }
671                 } else {
672                     logger.debug("Bank ID must be between 1 and 6: {}", resp);
673                 }
674
675             } catch (NumberFormatException e) {
676                 logger.warn("Invalid Bank Name Notification (bank/source not a parsable integer): '{}')", resp);
677             }
678
679         } else {
680             logger.warn("Invalid Bank Notification: '{}')", resp);
681         }
682     }
683
684     /**
685      * Implements {@link SocketSessionListener#responseReceived(String)} to try to process the response from the
686      * russound system. This response may be for other protocol handler - so ignore if we don't recognize the response.
687      *
688      * @param a possibly null, possibly empty response
689      */
690     @Override
691     public void responseReceived(@Nullable String response) {
692         if (response == null || response.isEmpty()) {
693             return;
694         }
695
696         Matcher m = RSP_BANKNOTIFICATION.matcher(response);
697         if (m.matches()) {
698             handleBankNotification(m, response);
699             return;
700         }
701
702         m = RSP_PRESETNOTIFICATION.matcher(response);
703         if (m.matches()) {
704             // does nothing
705             return;
706         }
707
708         m = RSP_SRCNOTIFICATION.matcher(response);
709         if (m.matches()) {
710             handleSourceNotification(m, response);
711         }
712
713         m = RSP_MMMENUNOTIFICATION.matcher(response);
714         if (m.matches()) {
715             try {
716                 handleMMChange(RioConstants.CHANNEL_SOURCEMMMENU, response);
717             } catch (NumberFormatException e) {
718                 logger.debug("Could not parse the menu text (1) from {}", response);
719             }
720         }
721     }
722
723     /**
724      * Overrides the default implementation to turn watch off ({@link #watchSource(boolean)}) before calling the dispose
725      */
726     @Override
727     public void dispose() {
728         watchSource(false);
729         if (httpClient != null) {
730             try {
731                 httpClient.stop();
732             } catch (Exception e) {
733                 logger.debug("Error stopping the httpclient", e);
734             }
735         }
736         super.dispose();
737     }
738
739     /**
740      * The following class is simply used as a model for an id/value combination that will be serialized to JSON.
741      * Nothing needs to be public because the serialization walks the properties.
742      *
743      * @author Tim Roberts
744      *
745      */
746     @SuppressWarnings("unused")
747     private class IdValue {
748         /** The id of the value */
749         private final int id;
750
751         /** The value for the id */
752         private final String value;
753
754         /**
755          * Constructions ID/Value from the given parms (no validations are done)
756          *
757          * @param id the identifier
758          * @param value the associated value
759          */
760         public IdValue(int id, String value) {
761             this.id = id;
762             this.value = value;
763         }
764     }
765 }