]> git.basschouten.com Git - openhab-addons.git/blob
6e210d21fb371aa35e388a1dbe223c69c01a574b
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2022 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.rotel.internal.communication;
14
15 import static org.openhab.binding.rotel.internal.RotelBindingConstants.*;
16 import static org.openhab.binding.rotel.internal.protocol.hex.RotelHexProtocolHandler.START;
17
18 import java.io.InterruptedIOException;
19 import java.nio.charset.StandardCharsets;
20 import java.util.Arrays;
21 import java.util.List;
22 import java.util.Map;
23 import java.util.Objects;
24 import java.util.StringJoiner;
25
26 import org.eclipse.jdt.annotation.NonNullByDefault;
27 import org.eclipse.jdt.annotation.Nullable;
28 import org.openhab.binding.rotel.internal.RotelException;
29 import org.openhab.binding.rotel.internal.RotelModel;
30 import org.openhab.binding.rotel.internal.RotelPlayStatus;
31 import org.openhab.binding.rotel.internal.RotelRepeatMode;
32 import org.openhab.binding.rotel.internal.protocol.RotelAbstractProtocolHandler;
33 import org.openhab.binding.rotel.internal.protocol.RotelProtocol;
34 import org.openhab.binding.rotel.internal.protocol.hex.RotelHexProtocolHandler;
35 import org.slf4j.Logger;
36 import org.slf4j.LoggerFactory;
37
38 /**
39  * Class for simulating the communication with the Rotel device
40  *
41  * @author Laurent Garnier - Initial contribution
42  */
43 @NonNullByDefault
44 public class RotelSimuConnector extends RotelConnector {
45
46     private static final int STEP_TONE_LEVEL = 1;
47
48     private final Logger logger = LoggerFactory.getLogger(RotelSimuConnector.class);
49
50     private final RotelModel model;
51     private final RotelProtocol protocol;
52     private final Map<RotelSource, String> sourcesLabels;
53
54     private Object lock = new Object();
55
56     private byte[] feedbackMsg = new byte[1];
57     private int idxInFeedbackMsg = feedbackMsg.length;
58
59     private boolean[] powers = { false, false, false, false, false };
60     private RotelSource[] sources;
61     private RotelSource recordSource;
62     private boolean multiinput;
63     private RotelDsp dsp = RotelDsp.CAT4_NONE;
64     private int[] volumes = { 50, 10, 20, 30, 40 };
65     private boolean[] mutes = { false, false, false, false, false };
66     private boolean tcbypass;
67     private int[] basses = { 0, 0, 0, 0, 0 };
68     private int[] trebles = { 0, 0, 0, 0, 0 };
69     private int[] balances = { 0, 0, 0, 0, 0 };
70     private boolean showTreble;
71     private boolean speakerA = true;
72     private boolean speakerB = false;
73     private RotelPlayStatus playStatus = RotelPlayStatus.STOPPED;
74     private int track = 1;
75     private boolean randomMode;
76     private RotelRepeatMode repeatMode = RotelRepeatMode.OFF;
77     private boolean selectingRecord;
78     private int showZone;
79     private int dimmer;
80
81     private int minVolume;
82     private int maxVolume;
83     private int minToneLevel;
84     private int maxToneLevel;
85     private int minBalance;
86     private int maxBalance;
87
88     /**
89      * Constructor
90      *
91      * @param model the projector model in use
92      * @param protocolHandler the protocol handler
93      * @param sourcesLabels the custom labels for sources
94      * @param readerThreadName the name of thread to be created
95      */
96     public RotelSimuConnector(RotelModel model, RotelAbstractProtocolHandler protocolHandler,
97             Map<RotelSource, String> sourcesLabels, String readerThreadName) {
98         super(protocolHandler, true, readerThreadName);
99         this.model = model;
100         this.protocol = protocolHandler.getProtocol();
101         this.sourcesLabels = sourcesLabels;
102         this.minVolume = 0;
103         this.maxVolume = model.hasVolumeControl() ? model.getVolumeMax() : 0;
104         this.maxToneLevel = model.hasToneControl() ? model.getToneLevelMax() : 0;
105         this.minToneLevel = -this.maxToneLevel;
106         this.maxBalance = model.hasBalanceControl() ? model.getBalanceLevelMax() : 0;
107         this.minBalance = -this.maxBalance;
108         List<RotelSource> modelSources = model.getSources();
109         RotelSource source = modelSources.isEmpty() ? RotelSource.CAT0_CD : modelSources.get(0);
110         sources = new RotelSource[] { source, source, source, source, source };
111         recordSource = source;
112     }
113
114     @Override
115     public synchronized void open() throws RotelException {
116         logger.debug("Opening simulated connection");
117         readerThread.start();
118         setConnected(true);
119         logger.debug("Simulated connection opened");
120     }
121
122     @Override
123     public synchronized void close() {
124         logger.debug("Closing simulated connection");
125         super.cleanup();
126         setConnected(false);
127         logger.debug("Simulated connection closed");
128     }
129
130     @Override
131     protected int readInput(byte[] dataBuffer) throws RotelException, InterruptedIOException {
132         synchronized (lock) {
133             int len = feedbackMsg.length - idxInFeedbackMsg;
134             if (len > 0) {
135                 if (len > dataBuffer.length) {
136                     len = dataBuffer.length;
137                 }
138                 System.arraycopy(feedbackMsg, idxInFeedbackMsg, dataBuffer, 0, len);
139                 idxInFeedbackMsg += len;
140                 return len;
141             }
142         }
143         // Give more chance to someone else than the reader thread to get the lock
144         try {
145             Thread.sleep(20);
146         } catch (InterruptedException e) {
147             Thread.currentThread().interrupt();
148         }
149         return 0;
150     }
151
152     /**
153      * Built the simulated feedback message for a sent command
154      *
155      * @param cmd the sent command
156      * @param value the integer value considered in the sent command for volume, bass or treble adjustment
157      */
158     public void buildFeedbackMessage(RotelCommand cmd, @Nullable Integer value) {
159         String text = buildSourceLine1Response();
160         String textLine1Left = buildSourceLine1LeftResponse();
161         String textLine1Right = buildVolumeLine1RightResponse();
162         String textLine2 = "";
163         String textAscii = "";
164         boolean accepted = true;
165         boolean resetZone = true;
166         int numZone = 0;
167         switch (cmd) {
168             case ZONE1_VOLUME_UP:
169             case ZONE1_VOLUME_DOWN:
170             case ZONE1_VOLUME_SET:
171             case ZONE1_MUTE_TOGGLE:
172             case ZONE1_MUTE_ON:
173             case ZONE1_MUTE_OFF:
174             case ZONE1_BASS_UP:
175             case ZONE1_BASS_DOWN:
176             case ZONE1_BASS_SET:
177             case ZONE1_TREBLE_UP:
178             case ZONE1_TREBLE_DOWN:
179             case ZONE1_TREBLE_SET:
180             case ZONE1_BALANCE_LEFT:
181             case ZONE1_BALANCE_RIGHT:
182             case ZONE1_BALANCE_SET:
183                 numZone = 1;
184                 break;
185             case ZONE2_POWER_OFF:
186             case ZONE2_POWER_ON:
187             case ZONE2_VOLUME_UP:
188             case ZONE2_VOLUME_DOWN:
189             case ZONE2_VOLUME_SET:
190             case ZONE2_MUTE_TOGGLE:
191             case ZONE2_MUTE_ON:
192             case ZONE2_MUTE_OFF:
193             case ZONE2_BASS_UP:
194             case ZONE2_BASS_DOWN:
195             case ZONE2_BASS_SET:
196             case ZONE2_TREBLE_UP:
197             case ZONE2_TREBLE_DOWN:
198             case ZONE2_TREBLE_SET:
199             case ZONE2_BALANCE_LEFT:
200             case ZONE2_BALANCE_RIGHT:
201             case ZONE2_BALANCE_SET:
202                 numZone = 2;
203                 break;
204             case ZONE3_POWER_OFF:
205             case ZONE3_POWER_ON:
206             case ZONE3_VOLUME_UP:
207             case ZONE3_VOLUME_DOWN:
208             case ZONE3_VOLUME_SET:
209             case ZONE3_MUTE_TOGGLE:
210             case ZONE3_MUTE_ON:
211             case ZONE3_MUTE_OFF:
212             case ZONE3_BASS_UP:
213             case ZONE3_BASS_DOWN:
214             case ZONE3_BASS_SET:
215             case ZONE3_TREBLE_UP:
216             case ZONE3_TREBLE_DOWN:
217             case ZONE3_TREBLE_SET:
218             case ZONE3_BALANCE_LEFT:
219             case ZONE3_BALANCE_RIGHT:
220             case ZONE3_BALANCE_SET:
221                 numZone = 3;
222                 break;
223             case ZONE4_POWER_OFF:
224             case ZONE4_POWER_ON:
225             case ZONE4_VOLUME_UP:
226             case ZONE4_VOLUME_DOWN:
227             case ZONE4_VOLUME_SET:
228             case ZONE4_MUTE_TOGGLE:
229             case ZONE4_MUTE_ON:
230             case ZONE4_MUTE_OFF:
231             case ZONE4_BASS_UP:
232             case ZONE4_BASS_DOWN:
233             case ZONE4_BASS_SET:
234             case ZONE4_TREBLE_UP:
235             case ZONE4_TREBLE_DOWN:
236             case ZONE4_TREBLE_SET:
237             case ZONE4_BALANCE_LEFT:
238             case ZONE4_BALANCE_RIGHT:
239             case ZONE4_BALANCE_SET:
240                 numZone = 4;
241                 break;
242             default:
243                 break;
244         }
245         switch (cmd) {
246             case DISPLAY_REFRESH:
247                 break;
248             case POWER_OFF:
249             case MAIN_ZONE_POWER_OFF:
250                 powers[0] = false;
251                 if (model.getNumberOfZones() > 1 && !model.hasPowerControlPerZone()) {
252                     for (int zone = 1; zone <= model.getNumberOfZones(); zone++) {
253                         powers[zone] = false;
254                     }
255                 }
256                 text = buildSourceLine1Response();
257                 textLine1Left = buildSourceLine1LeftResponse();
258                 textLine1Right = buildVolumeLine1RightResponse();
259                 textAscii = buildPowerAsciiResponse();
260                 break;
261             case POWER_ON:
262             case MAIN_ZONE_POWER_ON:
263                 powers[0] = true;
264                 if (model.getNumberOfZones() > 1 && !model.hasPowerControlPerZone()) {
265                     for (int zone = 1; zone <= model.getNumberOfZones(); zone++) {
266                         powers[zone] = true;
267                     }
268                 }
269                 text = buildSourceLine1Response();
270                 textLine1Left = buildSourceLine1LeftResponse();
271                 textLine1Right = buildVolumeLine1RightResponse();
272                 textAscii = buildPowerAsciiResponse();
273                 break;
274             case POWER:
275                 textAscii = buildPowerAsciiResponse();
276                 break;
277             case ZONE2_POWER_OFF:
278             case ZONE3_POWER_OFF:
279             case ZONE4_POWER_OFF:
280                 powers[numZone] = false;
281                 text = textLine2 = buildZonePowerResponse(numZone);
282                 showZone = numZone;
283                 resetZone = false;
284                 break;
285             case ZONE2_POWER_ON:
286             case ZONE3_POWER_ON:
287             case ZONE4_POWER_ON:
288                 powers[numZone] = true;
289                 text = textLine2 = buildZonePowerResponse(numZone);
290                 showZone = numZone;
291                 resetZone = false;
292                 break;
293             case RECORD_FONCTION_SELECT:
294                 if (model.getNumberOfZones() > 1 && model.getZoneSelectCmd() == cmd) {
295                     showZone++;
296                     if (showZone >= model.getNumberOfZones()) {
297                         showZone = 1;
298                         if (!powers[0]) {
299                             showZone++;
300                         }
301                     }
302                 } else {
303                     showZone = 1;
304                 }
305                 if (showZone == 1) {
306                     selectingRecord = powers[0];
307                     showTreble = false;
308                     textLine2 = buildRecordResponse();
309                 } else if (showZone >= 2 && showZone <= 4) {
310                     selectingRecord = false;
311                     text = textLine2 = buildZonePowerResponse(showZone);
312                 }
313                 resetZone = false;
314                 break;
315             case ZONE_SELECT:
316                 if (model.getNumberOfZones() == 1 || (model.getNumberOfZones() > 2 && model.getZoneSelectCmd() == cmd)
317                         || (showZone == 1 && model.getZoneSelectCmd() != cmd)) {
318                     accepted = false;
319                 } else {
320                     if (model.getZoneSelectCmd() == cmd) {
321                         if (!powers[0] && !powers[2]) {
322                             showZone = 2;
323                             powers[2] = true;
324                         } else if (showZone == 2) {
325                             powers[2] = !powers[2];
326                         } else {
327                             showZone = 2;
328                         }
329                     } else if (showZone >= 2 && showZone <= 4) {
330                         powers[showZone] = !powers[showZone];
331                     }
332                     if (showZone >= 2 && showZone <= 4) {
333                         text = textLine2 = buildZonePowerResponse(showZone);
334                     }
335                     resetZone = false;
336                 }
337                 break;
338             default:
339                 accepted = false;
340                 break;
341         }
342         if (!accepted && numZone > 0 && powers[numZone]) {
343             accepted = true;
344             switch (cmd) {
345                 case ZONE1_VOLUME_UP:
346                 case ZONE2_VOLUME_UP:
347                 case ZONE3_VOLUME_UP:
348                 case ZONE4_VOLUME_UP:
349                     if (volumes[numZone] < maxVolume) {
350                         volumes[numZone]++;
351                     }
352                     text = textLine2 = buildZoneVolumeResponse(numZone);
353                     textAscii = buildVolumeAsciiResponse();
354                     break;
355                 case ZONE1_VOLUME_DOWN:
356                 case ZONE2_VOLUME_DOWN:
357                 case ZONE3_VOLUME_DOWN:
358                 case ZONE4_VOLUME_DOWN:
359                     if (volumes[numZone] > minVolume) {
360                         volumes[numZone]--;
361                     }
362                     text = textLine2 = buildZoneVolumeResponse(numZone);
363                     textAscii = buildVolumeAsciiResponse();
364                     break;
365                 case ZONE1_VOLUME_SET:
366                 case ZONE2_VOLUME_SET:
367                 case ZONE3_VOLUME_SET:
368                 case ZONE4_VOLUME_SET:
369                     if (value != null) {
370                         volumes[numZone] = value;
371                     }
372                     text = textLine2 = buildZoneVolumeResponse(numZone);
373                     textAscii = buildVolumeAsciiResponse();
374                     break;
375                 case ZONE1_MUTE_TOGGLE:
376                 case ZONE2_MUTE_TOGGLE:
377                 case ZONE3_MUTE_TOGGLE:
378                 case ZONE4_MUTE_TOGGLE:
379                     mutes[numZone] = !mutes[numZone];
380                     text = textLine2 = buildZoneVolumeResponse(numZone);
381                     textAscii = buildMuteAsciiResponse();
382                     break;
383                 case ZONE1_MUTE_ON:
384                 case ZONE2_MUTE_ON:
385                 case ZONE3_MUTE_ON:
386                 case ZONE4_MUTE_ON:
387                     mutes[numZone] = true;
388                     text = textLine2 = buildZoneVolumeResponse(numZone);
389                     textAscii = buildMuteAsciiResponse();
390                     break;
391                 case ZONE1_MUTE_OFF:
392                 case ZONE2_MUTE_OFF:
393                 case ZONE3_MUTE_OFF:
394                 case ZONE4_MUTE_OFF:
395                     mutes[numZone] = false;
396                     text = textLine2 = buildZoneVolumeResponse(numZone);
397                     textAscii = buildMuteAsciiResponse();
398                     break;
399                 case ZONE1_BASS_UP:
400                 case ZONE2_BASS_UP:
401                 case ZONE3_BASS_UP:
402                 case ZONE4_BASS_UP:
403                     if (!tcbypass && basses[numZone] < maxToneLevel) {
404                         basses[numZone] += STEP_TONE_LEVEL;
405                     }
406                     textAscii = buildBassAsciiResponse();
407                     break;
408                 case ZONE1_BASS_DOWN:
409                 case ZONE2_BASS_DOWN:
410                 case ZONE3_BASS_DOWN:
411                 case ZONE4_BASS_DOWN:
412                     if (!tcbypass && basses[numZone] > minToneLevel) {
413                         basses[numZone] -= STEP_TONE_LEVEL;
414                     }
415                     textAscii = buildBassAsciiResponse();
416                     break;
417                 case ZONE1_BASS_SET:
418                 case ZONE2_BASS_SET:
419                 case ZONE3_BASS_SET:
420                 case ZONE4_BASS_SET:
421                     if (!tcbypass && value != null) {
422                         basses[numZone] = value;
423                     }
424                     textAscii = buildBassAsciiResponse();
425                     break;
426                 case ZONE1_TREBLE_UP:
427                 case ZONE2_TREBLE_UP:
428                 case ZONE3_TREBLE_UP:
429                 case ZONE4_TREBLE_UP:
430                     if (!tcbypass && trebles[numZone] < maxToneLevel) {
431                         trebles[numZone] += STEP_TONE_LEVEL;
432                     }
433                     textAscii = buildTrebleAsciiResponse();
434                     break;
435                 case ZONE1_TREBLE_DOWN:
436                 case ZONE2_TREBLE_DOWN:
437                 case ZONE3_TREBLE_DOWN:
438                 case ZONE4_TREBLE_DOWN:
439                     if (!tcbypass && trebles[numZone] > minToneLevel) {
440                         trebles[numZone] -= STEP_TONE_LEVEL;
441                     }
442                     textAscii = buildTrebleAsciiResponse();
443                     break;
444                 case ZONE1_TREBLE_SET:
445                 case ZONE2_TREBLE_SET:
446                 case ZONE3_TREBLE_SET:
447                 case ZONE4_TREBLE_SET:
448                     if (!tcbypass && value != null) {
449                         trebles[numZone] = value;
450                     }
451                     textAscii = buildTrebleAsciiResponse();
452                     break;
453                 case ZONE1_BALANCE_LEFT:
454                 case ZONE2_BALANCE_LEFT:
455                 case ZONE3_BALANCE_LEFT:
456                 case ZONE4_BALANCE_LEFT:
457                     if (balances[numZone] > minBalance) {
458                         balances[numZone]--;
459                     }
460                     textAscii = buildBalanceAsciiResponse();
461                     break;
462                 case ZONE1_BALANCE_RIGHT:
463                 case ZONE2_BALANCE_RIGHT:
464                 case ZONE3_BALANCE_RIGHT:
465                 case ZONE4_BALANCE_RIGHT:
466                     if (balances[numZone] < maxBalance) {
467                         balances[numZone]++;
468                     }
469                     textAscii = buildBalanceAsciiResponse();
470                     break;
471                 case ZONE1_BALANCE_SET:
472                 case ZONE2_BALANCE_SET:
473                 case ZONE3_BALANCE_SET:
474                 case ZONE4_BALANCE_SET:
475                     if (value != null) {
476                         balances[numZone] = value;
477                     }
478                     textAscii = buildBalanceAsciiResponse();
479                     break;
480                 default:
481                     accepted = false;
482                     break;
483             }
484         }
485         if (!accepted) {
486             // Check if command is a change of source input for a zone
487             for (int zone = 1; zone <= model.getNumberOfZones(); zone++) {
488                 if (powers[zone]) {
489                     try {
490                         sources[zone] = model.getZoneSourceFromCommand(cmd, zone);
491                         text = textLine2 = buildZonePowerResponse(zone);
492                         textAscii = buildSourceAsciiResponse();
493                         mutes[zone] = false;
494                         accepted = true;
495                         showZone = zone;
496                         resetZone = false;
497                         break;
498                     } catch (RotelException e) {
499                     }
500                 }
501             }
502         }
503         if (!accepted && powers[2] && !model.hasZoneCommands(2) && model.getNumberOfZones() > 1 && showZone == 2) {
504             accepted = true;
505             switch (cmd) {
506                 case VOLUME_UP:
507                     if (volumes[2] < maxVolume) {
508                         volumes[2]++;
509                     }
510                     text = textLine2 = buildZoneVolumeResponse(2);
511                     resetZone = false;
512                     break;
513                 case VOLUME_DOWN:
514                     if (volumes[2] > minVolume) {
515                         volumes[2]--;
516                     }
517                     text = textLine2 = buildZoneVolumeResponse(2);
518                     resetZone = false;
519                     break;
520                 case VOLUME_SET:
521                     if (value != null) {
522                         volumes[2] = value;
523                     }
524                     text = textLine2 = buildZoneVolumeResponse(2);
525                     resetZone = false;
526                     break;
527                 default:
528                     accepted = false;
529                     break;
530             }
531             if (!accepted) {
532                 try {
533                     sources[2] = model.getSourceFromCommand(cmd);
534                     text = textLine2 = buildZonePowerResponse(2);
535                     mutes[2] = false;
536                     accepted = true;
537                     resetZone = false;
538                 } catch (RotelException e) {
539                 }
540             }
541         }
542         if (!accepted && powers[0]) {
543             accepted = true;
544             switch (cmd) {
545                 case UPDATE_AUTO:
546                     textAscii = buildAsciiResponse(
547                             protocol == RotelProtocol.ASCII_V1 ? KEY_DISPLAY_UPDATE : KEY_UPDATE_MODE, AUTO);
548                     break;
549                 case UPDATE_MANUAL:
550                     textAscii = buildAsciiResponse(
551                             protocol == RotelProtocol.ASCII_V1 ? KEY_DISPLAY_UPDATE : KEY_UPDATE_MODE, MANUAL);
552                     break;
553                 case VOLUME_GET_MIN:
554                     textAscii = buildAsciiResponse(KEY_VOLUME_MIN, minVolume);
555                     break;
556                 case VOLUME_GET_MAX:
557                     textAscii = buildAsciiResponse(KEY_VOLUME_MAX, maxVolume);
558                     break;
559                 case VOLUME_UP:
560                 case MAIN_ZONE_VOLUME_UP:
561                     if (volumes[0] < maxVolume) {
562                         volumes[0]++;
563                     }
564                     text = buildVolumeLine1Response();
565                     textLine1Right = buildVolumeLine1RightResponse();
566                     textAscii = buildVolumeAsciiResponse();
567                     break;
568                 case VOLUME_DOWN:
569                 case MAIN_ZONE_VOLUME_DOWN:
570                     if (volumes[0] > minVolume) {
571                         volumes[0]--;
572                     }
573                     text = buildVolumeLine1Response();
574                     textLine1Right = buildVolumeLine1RightResponse();
575                     textAscii = buildVolumeAsciiResponse();
576                     break;
577                 case VOLUME_SET:
578                     if (value != null) {
579                         volumes[0] = value;
580                     }
581                     text = buildVolumeLine1Response();
582                     textLine1Right = buildVolumeLine1RightResponse();
583                     textAscii = buildVolumeAsciiResponse();
584                     break;
585                 case VOLUME_GET:
586                     textAscii = buildVolumeAsciiResponse();
587                     break;
588                 case MUTE_TOGGLE:
589                 case MAIN_ZONE_MUTE_TOGGLE:
590                     mutes[0] = !mutes[0];
591                     text = buildSourceLine1Response();
592                     textLine1Right = buildVolumeLine1RightResponse();
593                     textAscii = buildMuteAsciiResponse();
594                     break;
595                 case MUTE_ON:
596                 case MAIN_ZONE_MUTE_ON:
597                     mutes[0] = true;
598                     text = buildSourceLine1Response();
599                     textLine1Right = buildVolumeLine1RightResponse();
600                     textAscii = buildMuteAsciiResponse();
601                     break;
602                 case MUTE_OFF:
603                 case MAIN_ZONE_MUTE_OFF:
604                     mutes[0] = false;
605                     text = buildSourceLine1Response();
606                     textLine1Right = buildVolumeLine1RightResponse();
607                     textAscii = buildMuteAsciiResponse();
608                     break;
609                 case MUTE:
610                     textAscii = buildMuteAsciiResponse();
611                     break;
612                 case TONE_MAX:
613                     textAscii = buildAsciiResponse(KEY_TONE_MAX, String.format("%02d", maxToneLevel));
614                     break;
615                 case TONE_CONTROLS_ON:
616                     tcbypass = false;
617                     textAscii = buildAsciiResponse(KEY_TONE, !tcbypass);
618                     break;
619                 case TONE_CONTROLS_OFF:
620                     tcbypass = true;
621                     textAscii = buildAsciiResponse(KEY_TONE, !tcbypass);
622                     break;
623                 case TONE_CONTROLS:
624                     textAscii = buildAsciiResponse(KEY_TONE, !tcbypass);
625                     break;
626                 case TCBYPASS_ON:
627                     tcbypass = true;
628                     textAscii = buildAsciiResponse(KEY_TCBYPASS, tcbypass);
629                     break;
630                 case TCBYPASS_OFF:
631                     tcbypass = false;
632                     textAscii = buildAsciiResponse(KEY_TCBYPASS, tcbypass);
633                     break;
634                 case TCBYPASS:
635                     textAscii = buildAsciiResponse(KEY_TCBYPASS, tcbypass);
636                     break;
637                 case BASS_UP:
638                     if (!tcbypass && basses[0] < maxToneLevel) {
639                         basses[0] += STEP_TONE_LEVEL;
640                     }
641                     text = buildBassLine1Response();
642                     textLine1Right = buildBassLine1RightResponse();
643                     textAscii = buildBassAsciiResponse();
644                     break;
645                 case BASS_DOWN:
646                     if (!tcbypass && basses[0] > minToneLevel) {
647                         basses[0] -= STEP_TONE_LEVEL;
648                     }
649                     text = buildBassLine1Response();
650                     textLine1Right = buildBassLine1RightResponse();
651                     textAscii = buildBassAsciiResponse();
652                     break;
653                 case BASS_SET:
654                     if (!tcbypass && value != null) {
655                         basses[0] = value;
656                     }
657                     text = buildBassLine1Response();
658                     textLine1Right = buildBassLine1RightResponse();
659                     textAscii = buildBassAsciiResponse();
660                     break;
661                 case BASS:
662                     textAscii = buildBassAsciiResponse();
663                     break;
664                 case TREBLE_UP:
665                     if (!tcbypass && trebles[0] < maxToneLevel) {
666                         trebles[0] += STEP_TONE_LEVEL;
667                     }
668                     text = buildTrebleLine1Response();
669                     textLine1Right = buildTrebleLine1RightResponse();
670                     textAscii = buildTrebleAsciiResponse();
671                     break;
672                 case TREBLE_DOWN:
673                     if (!tcbypass && trebles[0] > minToneLevel) {
674                         trebles[0] -= STEP_TONE_LEVEL;
675                     }
676                     text = buildTrebleLine1Response();
677                     textLine1Right = buildTrebleLine1RightResponse();
678                     textAscii = buildTrebleAsciiResponse();
679                     break;
680                 case TREBLE_SET:
681                     if (!tcbypass && value != null) {
682                         trebles[0] = value;
683                     }
684                     text = buildTrebleLine1Response();
685                     textLine1Right = buildTrebleLine1RightResponse();
686                     textAscii = buildTrebleAsciiResponse();
687                     break;
688                 case TREBLE:
689                     textAscii = buildTrebleAsciiResponse();
690                     break;
691                 case TONE_CONTROL_SELECT:
692                     showTreble = !showTreble;
693                     if (showTreble) {
694                         text = buildTrebleLine1Response();
695                         textLine1Right = buildTrebleLine1RightResponse();
696                     } else {
697                         text = buildBassLine1Response();
698                         textLine1Right = buildBassLine1RightResponse();
699                     }
700                     break;
701                 case BALANCE_LEFT:
702                     if (balances[0] > minBalance) {
703                         balances[0]--;
704                     }
705                     textAscii = buildBalanceAsciiResponse();
706                     break;
707                 case BALANCE_RIGHT:
708                     if (balances[0] < maxBalance) {
709                         balances[0]++;
710                     }
711                     textAscii = buildBalanceAsciiResponse();
712                     break;
713                 case BALANCE_SET:
714                     if (value != null) {
715                         balances[0] = value;
716                     }
717                     textAscii = buildBalanceAsciiResponse();
718                     break;
719                 case BALANCE:
720                     textAscii = buildBalanceAsciiResponse();
721                     break;
722                 case SPEAKER_A_TOGGLE:
723                     speakerA = !speakerA;
724                     textAscii = buildSpeakerAsciiResponse();
725                     break;
726                 case SPEAKER_A_ON:
727                     speakerA = true;
728                     textAscii = buildSpeakerAsciiResponse();
729                     break;
730                 case SPEAKER_A_OFF:
731                     speakerA = false;
732                     textAscii = buildSpeakerAsciiResponse();
733                     break;
734                 case SPEAKER_B_TOGGLE:
735                     speakerB = !speakerB;
736                     textAscii = buildSpeakerAsciiResponse();
737                     break;
738                 case SPEAKER_B_ON:
739                     speakerB = true;
740                     textAscii = buildSpeakerAsciiResponse();
741                     break;
742                 case SPEAKER_B_OFF:
743                     speakerB = false;
744                     textAscii = buildSpeakerAsciiResponse();
745                     break;
746                 case SPEAKER:
747                     textAscii = buildSpeakerAsciiResponse();
748                     break;
749                 case PLAY:
750                     playStatus = RotelPlayStatus.PLAYING;
751                     textAscii = buildPlayStatusAsciiResponse();
752                     break;
753                 case STOP:
754                     playStatus = RotelPlayStatus.STOPPED;
755                     textAscii = buildPlayStatusAsciiResponse();
756                     break;
757                 case PAUSE:
758                     switch (playStatus) {
759                         case PLAYING:
760                             playStatus = RotelPlayStatus.PAUSED;
761                             break;
762                         case PAUSED:
763                         case STOPPED:
764                             playStatus = RotelPlayStatus.PLAYING;
765                             break;
766                     }
767                     textAscii = buildPlayStatusAsciiResponse();
768                     break;
769                 case CD_PLAY_STATUS:
770                 case PLAY_STATUS:
771                     textAscii = buildPlayStatusAsciiResponse();
772                     break;
773                 case TRACK_FORWARD:
774                     track++;
775                     textAscii = buildTrackAsciiResponse();
776                     break;
777                 case TRACK_BACKWORD:
778                     if (track > 1) {
779                         track--;
780                     }
781                     textAscii = buildTrackAsciiResponse();
782                     break;
783                 case TRACK:
784                     textAscii = buildTrackAsciiResponse();
785                     break;
786                 case RANDOM_TOGGLE:
787                     randomMode = !randomMode;
788                     textAscii = buildRandomModeAsciiResponse();
789                     break;
790                 case RANDOM_MODE:
791                     textAscii = buildRandomModeAsciiResponse();
792                     break;
793                 case REPEAT_TOGGLE:
794                     switch (repeatMode) {
795                         case TRACK:
796                             repeatMode = RotelRepeatMode.DISC;
797                             break;
798                         case DISC:
799                             repeatMode = RotelRepeatMode.OFF;
800                             break;
801                         case OFF:
802                             repeatMode = RotelRepeatMode.TRACK;
803                             break;
804                     }
805                     textAscii = buildRepeatModeAsciiResponse();
806                     break;
807                 case REPEAT_MODE:
808                     textAscii = buildRepeatModeAsciiResponse();
809                     break;
810                 case SOURCE_MULTI_INPUT:
811                     multiinput = !multiinput;
812                     text = "MULTI IN " + (multiinput ? "ON" : "OFF");
813                     try {
814                         sources[0] = model.getSourceFromCommand(cmd);
815                         textLine1Left = buildSourceLine1LeftResponse();
816                         textAscii = buildSourceAsciiResponse();
817                         mutes[0] = false;
818                     } catch (RotelException e) {
819                     }
820                     break;
821                 case SOURCE:
822                 case INPUT:
823                     textAscii = buildSourceAsciiResponse();
824                     break;
825                 case STEREO:
826                     dsp = RotelDsp.CAT4_NONE;
827                     textLine2 = "STEREO";
828                     textAscii = buildDspAsciiResponse();
829                     break;
830                 case STEREO3:
831                     dsp = RotelDsp.CAT4_STEREO3;
832                     textLine2 = "DOLBY 3 STEREO";
833                     textAscii = buildDspAsciiResponse();
834                     break;
835                 case STEREO5:
836                     dsp = RotelDsp.CAT4_STEREO5;
837                     textLine2 = "5CH STEREO";
838                     textAscii = buildDspAsciiResponse();
839                     break;
840                 case STEREO7:
841                     dsp = RotelDsp.CAT4_STEREO7;
842                     textLine2 = "7CH STEREO";
843                     textAscii = buildDspAsciiResponse();
844                     break;
845                 case STEREO9:
846                     dsp = RotelDsp.CAT5_STEREO9;
847                     textAscii = buildDspAsciiResponse();
848                     break;
849                 case STEREO11:
850                     dsp = RotelDsp.CAT5_STEREO11;
851                     textAscii = buildDspAsciiResponse();
852                     break;
853                 case DSP1:
854                     dsp = RotelDsp.CAT4_DSP1;
855                     textLine2 = "DSP 1";
856                     textAscii = buildDspAsciiResponse();
857                     break;
858                 case DSP2:
859                     dsp = RotelDsp.CAT4_DSP2;
860                     textLine2 = "DSP 2";
861                     textAscii = buildDspAsciiResponse();
862                     break;
863                 case DSP3:
864                     dsp = RotelDsp.CAT4_DSP3;
865                     textLine2 = "DSP 3";
866                     textAscii = buildDspAsciiResponse();
867                     break;
868                 case DSP4:
869                     dsp = RotelDsp.CAT4_DSP4;
870                     textLine2 = "DSP 4";
871                     textAscii = buildDspAsciiResponse();
872                     break;
873                 case PROLOGIC:
874                     dsp = RotelDsp.CAT4_PROLOGIC;
875                     textLine2 = "DOLBY PRO LOGIC";
876                     textAscii = buildDspAsciiResponse();
877                     break;
878                 case PLII_CINEMA:
879                     dsp = RotelDsp.CAT4_PLII_CINEMA;
880                     textLine2 = "DOLBY PL  C";
881                     textAscii = buildDspAsciiResponse();
882                     break;
883                 case PLII_MUSIC:
884                     dsp = RotelDsp.CAT4_PLII_MUSIC;
885                     textLine2 = "DOLBY PL  M";
886                     textAscii = buildDspAsciiResponse();
887                     break;
888                 case PLII_GAME:
889                     dsp = RotelDsp.CAT4_PLII_GAME;
890                     textLine2 = "DOLBY PL  G";
891                     textAscii = buildDspAsciiResponse();
892                     break;
893                 case PLIIZ:
894                     dsp = RotelDsp.CAT4_PLIIZ;
895                     textLine2 = "DOLBY PL z";
896                     textAscii = buildDspAsciiResponse();
897                     break;
898                 case NEO6_MUSIC:
899                     dsp = RotelDsp.CAT4_NEO6_MUSIC;
900                     textLine2 = "DTS Neo:6 M";
901                     textAscii = buildDspAsciiResponse();
902                     break;
903                 case NEO6_CINEMA:
904                     dsp = RotelDsp.CAT4_NEO6_CINEMA;
905                     textLine2 = "DTS Neo:6 C";
906                     textAscii = buildDspAsciiResponse();
907                     break;
908                 case ATMOS:
909                     dsp = RotelDsp.CAT5_ATMOS;
910                     textAscii = buildDspAsciiResponse();
911                     break;
912                 case NEURAL_X:
913                     dsp = RotelDsp.CAT5_NEURAL_X;
914                     textAscii = buildDspAsciiResponse();
915                     break;
916                 case BYPASS:
917                     dsp = RotelDsp.CAT4_BYPASS;
918                     textLine2 = "BYPASS";
919                     textAscii = buildDspAsciiResponse();
920                     break;
921                 case DSP_MODE:
922                     textAscii = buildDspAsciiResponse();
923                     break;
924                 case FREQUENCY:
925                     textAscii = model.getNumberOfZones() > 1 ? buildAsciiResponse(KEY_FREQ, "44.1,48,none,176.4")
926                             : buildAsciiResponse(KEY_FREQ, "44.1");
927                     break;
928                 case DIMMER_LEVEL_SET:
929                     if (value != null) {
930                         dimmer = value;
931                     }
932                     textAscii = buildAsciiResponse(KEY_DIMMER, dimmer);
933                     break;
934                 case DIMMER_LEVEL_GET:
935                     textAscii = buildAsciiResponse(KEY_DIMMER, dimmer);
936                     break;
937                 case MODEL:
938                     textAscii = buildAsciiResponse(KEY_MODEL, model.getName());
939                     break;
940                 case VERSION:
941                     textAscii = buildAsciiResponse(KEY_VERSION, "1.00");
942                     break;
943                 default:
944                     accepted = false;
945                     break;
946             }
947             if (!accepted) {
948                 // Check if command is a change of source input for the main zone
949                 try {
950                     sources[0] = model.getZoneSourceFromCommand(cmd, 1);
951                     text = buildSourceLine1Response();
952                     textLine1Left = buildSourceLine1LeftResponse();
953                     textAscii = buildSourceAsciiResponse();
954                     accepted = true;
955                 } catch (RotelException e) {
956                 }
957             }
958             if (!accepted) {
959                 // Check if command is a change of source input
960                 try {
961                     if (selectingRecord && !model.hasOtherThanPrimaryCommands()) {
962                         recordSource = model.getSourceFromCommand(cmd);
963                     } else {
964                         sources[0] = model.getSourceFromCommand(cmd);
965                     }
966                     text = buildSourceLine1Response();
967                     textLine1Left = buildSourceLine1LeftResponse();
968                     textAscii = buildSourceAsciiResponse();
969                     mutes[0] = false;
970                     accepted = true;
971                 } catch (RotelException e) {
972                 }
973             }
974             if (!accepted) {
975                 // Check if command is a change of record source
976                 try {
977                     recordSource = model.getRecordSourceFromCommand(cmd);
978                     text = buildSourceLine1Response();
979                     textLine2 = buildRecordResponse();
980                     accepted = true;
981                 } catch (RotelException e) {
982                 }
983             }
984         }
985
986         if (!accepted) {
987             return;
988         }
989
990         if (cmd != RotelCommand.RECORD_FONCTION_SELECT) {
991             selectingRecord = false;
992         }
993         if (resetZone) {
994             showZone = 0;
995         }
996
997         if (model.getRespNbChars() == 42) {
998             while (textLine1Left.length() < 14) {
999                 textLine1Left += " ";
1000             }
1001             while (textLine1Right.length() < 7) {
1002                 textLine1Right += " ";
1003             }
1004             while (textLine2.length() < 21) {
1005                 textLine2 += " ";
1006             }
1007             text = textLine1Left + textLine1Right + textLine2;
1008         }
1009
1010         if (protocol == RotelProtocol.HEX) {
1011             byte[] chars = Arrays.copyOf(text.getBytes(StandardCharsets.US_ASCII), model.getRespNbChars());
1012             byte[] flags = new byte[model.getRespNbFlags()];
1013             try {
1014                 model.setMultiInput(flags, multiinput);
1015             } catch (RotelException e) {
1016             }
1017             try {
1018                 model.setZone2(flags, powers[2]);
1019             } catch (RotelException e) {
1020             }
1021             try {
1022                 model.setZone3(flags, powers[3]);
1023             } catch (RotelException e) {
1024             }
1025             try {
1026                 model.setZone4(flags, powers[4]);
1027             } catch (RotelException e) {
1028             }
1029             int size = 6 + model.getRespNbChars() + model.getRespNbFlags();
1030             byte[] dataBuffer = new byte[size];
1031             int idx = 0;
1032             dataBuffer[idx++] = START;
1033             dataBuffer[idx++] = (byte) (size - 4);
1034             dataBuffer[idx++] = model.getDeviceId();
1035             dataBuffer[idx++] = STANDARD_RESPONSE;
1036             if (model.isCharsBeforeFlags()) {
1037                 System.arraycopy(chars, 0, dataBuffer, idx, model.getRespNbChars());
1038                 idx += model.getRespNbChars();
1039                 System.arraycopy(flags, 0, dataBuffer, idx, model.getRespNbFlags());
1040                 idx += model.getRespNbFlags();
1041             } else {
1042                 System.arraycopy(flags, 0, dataBuffer, idx, model.getRespNbFlags());
1043                 idx += model.getRespNbFlags();
1044                 System.arraycopy(chars, 0, dataBuffer, idx, model.getRespNbChars());
1045                 idx += model.getRespNbChars();
1046             }
1047             byte checksum = RotelHexProtocolHandler.computeCheckSum(dataBuffer, idx - 1);
1048             if ((checksum & 0x000000FF) == 0x000000FD) {
1049                 dataBuffer[idx++] = (byte) 0xFD;
1050                 dataBuffer[idx++] = 0;
1051             } else if ((checksum & 0x000000FF) == 0x000000FE) {
1052                 dataBuffer[idx++] = (byte) 0xFD;
1053                 dataBuffer[idx++] = 1;
1054             } else {
1055                 dataBuffer[idx++] = checksum;
1056             }
1057             synchronized (lock) {
1058                 feedbackMsg = Arrays.copyOf(dataBuffer, idx);
1059                 idxInFeedbackMsg = 0;
1060             }
1061         } else {
1062             String command = textAscii + (protocol == RotelProtocol.ASCII_V1 ? "!" : "$");
1063             synchronized (lock) {
1064                 feedbackMsg = command.getBytes(StandardCharsets.US_ASCII);
1065                 idxInFeedbackMsg = 0;
1066             }
1067         }
1068     }
1069
1070     private String buildAsciiResponse(String key, String value) {
1071         return String.format("%s=%s", key, value);
1072     }
1073
1074     private String buildAsciiResponse(String key, int value) {
1075         return String.format("%s=%d", key, value);
1076     }
1077
1078     private String buildAsciiResponse(String key, boolean value) {
1079         return buildAsciiResponse(key, buildOnOffValue(value));
1080     }
1081
1082     private String buildOnOffValue(boolean on) {
1083         return on ? MSG_VALUE_ON : MSG_VALUE_OFF;
1084     }
1085
1086     private String buildPowerAsciiResponse() {
1087         return buildAsciiResponse(KEY_POWER, powers[0] ? POWER_ON : STANDBY);
1088     }
1089
1090     private String buildVolumeAsciiResponse() {
1091         if (model.getNumberOfZones() > 1) {
1092             StringJoiner sj = new StringJoiner(",");
1093             for (int zone = 1; zone <= model.getNumberOfZones(); zone++) {
1094                 sj.add(String.format("%02d", volumes[zone]));
1095             }
1096             return buildAsciiResponse(KEY_VOLUME, sj.toString());
1097         } else {
1098             return buildAsciiResponse(KEY_VOLUME, String.format("%02d", volumes[0]));
1099         }
1100     }
1101
1102     private String buildMuteAsciiResponse() {
1103         if (model.getNumberOfZones() > 1) {
1104             StringJoiner sj = new StringJoiner(",");
1105             for (int zone = 1; zone <= model.getNumberOfZones(); zone++) {
1106                 sj.add(buildOnOffValue(mutes[zone]));
1107             }
1108             return buildAsciiResponse(KEY_MUTE, sj.toString());
1109         } else {
1110             return buildAsciiResponse(KEY_MUTE, mutes[0]);
1111         }
1112     }
1113
1114     private String buildBassAsciiResponse() {
1115         if (model.getNumberOfZones() > 1) {
1116             StringJoiner sj = new StringJoiner(",");
1117             for (int zone = 1; zone <= model.getNumberOfZones(); zone++) {
1118                 sj.add(buildBassTrebleValue(basses[zone]));
1119             }
1120             return buildAsciiResponse(KEY_BASS, sj.toString());
1121         } else {
1122             return buildAsciiResponse(KEY_BASS, buildBassTrebleValue(basses[0]));
1123         }
1124     }
1125
1126     private String buildTrebleAsciiResponse() {
1127         if (model.getNumberOfZones() > 1) {
1128             StringJoiner sj = new StringJoiner(",");
1129             for (int zone = 1; zone <= model.getNumberOfZones(); zone++) {
1130                 sj.add(buildBassTrebleValue(trebles[zone]));
1131             }
1132             return buildAsciiResponse(KEY_TREBLE, sj.toString());
1133         } else {
1134             return buildAsciiResponse(KEY_TREBLE, buildBassTrebleValue(trebles[0]));
1135         }
1136     }
1137
1138     private String buildBassTrebleValue(int value) {
1139         if (tcbypass || value == 0) {
1140             return "000";
1141         } else if (value > 0) {
1142             return String.format("+%02d", value);
1143         } else {
1144             return String.format("-%02d", -value);
1145         }
1146     }
1147
1148     private String buildBalanceAsciiResponse() {
1149         if (model.getNumberOfZones() > 1) {
1150             StringJoiner sj = new StringJoiner(",");
1151             for (int zone = 1; zone <= model.getNumberOfZones(); zone++) {
1152                 sj.add(buildBalanceValue(balances[zone]));
1153             }
1154             return buildAsciiResponse(KEY_BALANCE, sj.toString());
1155         } else {
1156             return buildAsciiResponse(KEY_BALANCE, buildBalanceValue(balances[0]));
1157         }
1158     }
1159
1160     private String buildBalanceValue(int value) {
1161         if (value == 0) {
1162             return "000";
1163         } else if (value > 0) {
1164             return String.format("r%02d", value);
1165         } else {
1166             return String.format("l%02d", -value);
1167         }
1168     }
1169
1170     private String buildSpeakerAsciiResponse() {
1171         String value;
1172         if (speakerA && speakerB) {
1173             value = MSG_VALUE_SPEAKER_AB;
1174         } else if (speakerA && !speakerB) {
1175             value = MSG_VALUE_SPEAKER_A;
1176         } else if (!speakerA && speakerB) {
1177             value = MSG_VALUE_SPEAKER_B;
1178         } else {
1179             value = MSG_VALUE_OFF;
1180         }
1181         return buildAsciiResponse(KEY_SPEAKER, value);
1182     }
1183
1184     private String buildPlayStatusAsciiResponse() {
1185         String status = "";
1186         switch (playStatus) {
1187             case PLAYING:
1188                 status = PLAY;
1189                 break;
1190             case PAUSED:
1191                 status = PAUSE;
1192                 break;
1193             case STOPPED:
1194                 status = STOP;
1195                 break;
1196         }
1197         return buildAsciiResponse(protocol == RotelProtocol.ASCII_V1 ? KEY1_PLAY_STATUS : KEY2_PLAY_STATUS, status);
1198     }
1199
1200     private String buildTrackAsciiResponse() {
1201         return buildAsciiResponse(KEY_TRACK, String.format("%03d", track));
1202     }
1203
1204     private String buildRandomModeAsciiResponse() {
1205         return buildAsciiResponse(KEY_RANDOM, randomMode);
1206     }
1207
1208     private String buildRepeatModeAsciiResponse() {
1209         String mode = "";
1210         switch (repeatMode) {
1211             case TRACK:
1212                 mode = TRACK;
1213                 break;
1214             case DISC:
1215                 mode = DISC;
1216                 break;
1217             case OFF:
1218                 mode = MSG_VALUE_OFF;
1219                 break;
1220         }
1221         return buildAsciiResponse(KEY_REPEAT, mode);
1222     }
1223
1224     private String buildSourceAsciiResponse() {
1225         if (model.getNumberOfZones() > 1) {
1226             StringJoiner sj = new StringJoiner(",");
1227             for (int zone = 1; zone <= model.getNumberOfZones(); zone++) {
1228                 sj.add(buildZoneSourceValue(sources[zone]));
1229             }
1230             return buildAsciiResponse(KEY_INPUT, sj.toString());
1231         } else {
1232             return buildAsciiResponse(KEY_SOURCE, buildSourceValue(sources[0]));
1233         }
1234     }
1235
1236     private String buildSourceValue(RotelSource source) {
1237         String str = null;
1238         RotelCommand command = source.getCommand();
1239         if (command != null) {
1240             str = protocol == RotelProtocol.ASCII_V1 ? command.getAsciiCommandV1() : command.getAsciiCommandV2();
1241         }
1242         return str == null ? "" : str;
1243     }
1244
1245     private String buildZoneSourceValue(RotelSource source) {
1246         String str = buildSourceValue(source);
1247         int idx = str.indexOf("input_");
1248         return idx < 0 ? str : str.substring(idx + 6);
1249     }
1250
1251     private String buildDspAsciiResponse() {
1252         return buildAsciiResponse(KEY_DSP_MODE, dsp.getFeedback());
1253     }
1254
1255     private String buildSourceLine1Response() {
1256         String text;
1257         if (!powers[0]) {
1258             text = "";
1259         } else if (mutes[0]) {
1260             text = "MUTE ON";
1261         } else {
1262             text = getSourceLabel(sources[0], false) + " " + getSourceLabel(recordSource, true);
1263         }
1264         return text;
1265     }
1266
1267     private String buildSourceLine1LeftResponse() {
1268         String text;
1269         if (!powers[0]) {
1270             text = "";
1271         } else {
1272             text = getSourceLabel(sources[0], false);
1273         }
1274         return text;
1275     }
1276
1277     private String buildRecordResponse() {
1278         String text;
1279         if (!powers[0]) {
1280             text = "";
1281         } else {
1282             text = "REC " + getSourceLabel(recordSource, true);
1283         }
1284         return text;
1285     }
1286
1287     private String buildZonePowerResponse(int numZone) {
1288         String zone;
1289         if (numZone == 2) {
1290             zone = model.getNumberOfZones() > 2 ? "ZONE2" : "ZONE";
1291         } else {
1292             zone = String.format("ZONE%d", numZone);
1293         }
1294         String state = powers[numZone] ? getSourceLabel(sources[numZone], true) : "OFF";
1295         return zone + " " + state;
1296     }
1297
1298     private String buildVolumeLine1Response() {
1299         String text;
1300         if (volumes[0] == minVolume) {
1301             text = " VOLUME  MIN ";
1302         } else if (volumes[0] == maxVolume) {
1303             text = " VOLUME  MAX ";
1304         } else {
1305             text = String.format(" VOLUME   %02d ", volumes[0]);
1306         }
1307         return text;
1308     }
1309
1310     private String buildVolumeLine1RightResponse() {
1311         String text;
1312         if (!powers[0]) {
1313             text = "";
1314         } else if (mutes[0]) {
1315             text = "MUTE ON";
1316         } else if (volumes[0] == minVolume) {
1317             text = "VOL MIN";
1318         } else if (volumes[0] == maxVolume) {
1319             text = "VOL MAX";
1320         } else {
1321             text = String.format("VOL  %02d", volumes[0]);
1322         }
1323         return text;
1324     }
1325
1326     private String buildZoneVolumeResponse(int numZone) {
1327         String zone;
1328         if (numZone == 2) {
1329             zone = model.getNumberOfZones() > 2 ? "ZONE2" : "ZONE";
1330         } else {
1331             zone = String.format("ZONE%d", numZone);
1332         }
1333         String text;
1334         if (mutes[numZone]) {
1335             text = zone + " MUTE ON";
1336         } else if (volumes[numZone] == minVolume) {
1337             text = zone + " VOL MIN";
1338         } else if (volumes[numZone] == maxVolume) {
1339             text = zone + " VOL MAX";
1340         } else {
1341             text = String.format("%s VOL %02d", zone, volumes[numZone]);
1342         }
1343         return text;
1344     }
1345
1346     private String buildBassLine1Response() {
1347         String text;
1348         if (basses[0] == minToneLevel) {
1349             text = "   BASS  MIN ";
1350         } else if (basses[0] == maxToneLevel) {
1351             text = "   BASS  MAX ";
1352         } else if (basses[0] == 0) {
1353             text = "   BASS    0 ";
1354         } else if (basses[0] > 0) {
1355             text = String.format("   BASS  +%02d ", basses[0]);
1356         } else {
1357             text = String.format("   BASS  -%02d ", -basses[0]);
1358         }
1359         return text;
1360     }
1361
1362     private String buildBassLine1RightResponse() {
1363         String text;
1364         if (basses[0] == minToneLevel) {
1365             text = "LF  MIN";
1366         } else if (basses[0] == maxToneLevel) {
1367             text = "LF  MAX";
1368         } else if (basses[0] == 0) {
1369             text = "LF    0";
1370         } else if (basses[0] > 0) {
1371             text = String.format("LF + %02d", basses[0]);
1372         } else {
1373             text = String.format("LF - %02d", -basses[0]);
1374         }
1375         return text;
1376     }
1377
1378     private String buildTrebleLine1Response() {
1379         String text;
1380         if (trebles[0] == minToneLevel) {
1381             text = " TREBLE  MIN ";
1382         } else if (trebles[0] == maxToneLevel) {
1383             text = " TREBLE  MAX ";
1384         } else if (trebles[0] == 0) {
1385             text = " TREBLE    0 ";
1386         } else if (trebles[0] > 0) {
1387             text = String.format(" TREBLE  +%02d ", trebles[0]);
1388         } else {
1389             text = String.format(" TREBLE  -%02d ", -trebles[0]);
1390         }
1391         return text;
1392     }
1393
1394     private String buildTrebleLine1RightResponse() {
1395         String text;
1396         if (trebles[0] == minToneLevel) {
1397             text = "HF  MIN";
1398         } else if (trebles[0] == maxToneLevel) {
1399             text = "HF  MAX";
1400         } else if (trebles[0] == 0) {
1401             text = "HF    0";
1402         } else if (trebles[0] > 0) {
1403             text = String.format("HF + %02d", trebles[0]);
1404         } else {
1405             text = String.format("HF - %02d", -trebles[0]);
1406         }
1407         return text;
1408     }
1409
1410     private String getSourceLabel(RotelSource source, boolean considerFollowMain) {
1411         String label;
1412         if (considerFollowMain && source.getName().equals(RotelSource.CAT1_FOLLOW_MAIN.getName())) {
1413             label = "SOURCE";
1414         } else {
1415             label = Objects.requireNonNullElse(sourcesLabels.get(source), source.getLabel());
1416         }
1417
1418         return label;
1419     }
1420 }