]> git.basschouten.com Git - openhab-addons.git/blob
5f9ee4f436d88dc58f4136612d46904f003bed60
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2024 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.protocol.hex;
14
15 import static org.openhab.binding.rotel.internal.RotelBindingConstants.*;
16
17 import java.nio.charset.StandardCharsets;
18 import java.util.Arrays;
19 import java.util.Map;
20
21 import org.eclipse.jdt.annotation.NonNullByDefault;
22 import org.eclipse.jdt.annotation.Nullable;
23 import org.openhab.binding.rotel.internal.RotelException;
24 import org.openhab.binding.rotel.internal.RotelModel;
25 import org.openhab.binding.rotel.internal.communication.RotelCommand;
26 import org.openhab.binding.rotel.internal.communication.RotelDsp;
27 import org.openhab.binding.rotel.internal.communication.RotelFlagsMapping;
28 import org.openhab.binding.rotel.internal.communication.RotelSource;
29 import org.openhab.binding.rotel.internal.protocol.RotelAbstractProtocolHandler;
30 import org.openhab.binding.rotel.internal.protocol.RotelProtocol;
31 import org.openhab.core.util.HexUtils;
32 import org.slf4j.Logger;
33 import org.slf4j.LoggerFactory;
34
35 /**
36  * Class for handling the Rotel HEX protocol (build of command messages, decoding of incoming data)
37  *
38  * @author Laurent Garnier - Initial contribution
39  */
40 @NonNullByDefault
41 public class RotelHexProtocolHandler extends RotelAbstractProtocolHandler {
42
43     public static final byte START = (byte) 0xFE;
44
45     private static final String KEY1_HEX_VOLUME = "volume ";
46     private static final String KEY2_HEX_VOLUME = "vol ";
47     private static final String KEY_HEX_MUTE = "mute ";
48     private static final String KEY1_HEX_BASS = "bass ";
49     private static final String KEY2_HEX_BASS = "lf ";
50     private static final String KEY1_HEX_TREBLE = "treble ";
51     private static final String KEY2_HEX_TREBLE = "hf ";
52     private static final String KEY_HEX_MULTI_IN = "multi in ";
53     private static final String KEY_HEX_STEREO = "stereo";
54     private static final String KEY1_HEX_3CH = "3 stereo";
55     private static final String KEY2_HEX_3CH = "dolby 3 stereo";
56     private static final String KEY_HEX_5CH = "5ch stereo";
57     private static final String KEY_HEX_7CH = "7ch stereo";
58     private static final String KEY_HEX_MUSIC1 = "music 1";
59     private static final String KEY_HEX_MUSIC2 = "music 2";
60     private static final String KEY_HEX_MUSIC3 = "music 3";
61     private static final String KEY_HEX_MUSIC4 = "music 4";
62     private static final String KEY_HEX_DSP1 = "dsp 1";
63     private static final String KEY_HEX_DSP2 = "dsp 2";
64     private static final String KEY_HEX_DSP3 = "dsp 3";
65     private static final String KEY_HEX_DSP4 = "dsp 4";
66     private static final String KEY1_HEX_PROLOGIC = "prologic  emu";
67     private static final String KEY2_HEX_PROLOGIC = "dolby pro logic";
68     private static final String KEY1_HEX_PLII_CINEMA = "prologic  cin";
69     private static final String KEY2_HEX_PLII_CINEMA = "dolby pl  c";
70     private static final String KEY1_HEX_PLII_MUSIC = "prologic  mus";
71     private static final String KEY2_HEX_PLII_MUSIC = "dolby pl  m";
72     private static final String KEY1_HEX_PLII_GAME = "prologic  gam";
73     private static final String KEY2_HEX_PLII_GAME = "dolby pl  g";
74     private static final String KEY1_HEX_PLIIX_CINEMA = "pl x cinema";
75     private static final String KEY2_HEX_PLIIX_CINEMA = "dolby pl x c";
76     private static final String KEY1_HEX_PLIIX_MUSIC = "pl x music";
77     private static final String KEY2_HEX_PLIIX_MUSIC = "dolby pl x m";
78     private static final String KEY1_HEX_PLIIX_GAME = "pl x game";
79     private static final String KEY2_HEX_PLIIX_GAME = "dolby pl x g";
80     private static final String KEY_HEX_PLIIZ = "dolby pl z";
81     private static final String KEY1_HEX_DTS_NEO6_CINEMA = "neo 6 cinema";
82     private static final String KEY2_HEX_DTS_NEO6_CINEMA = "dts neo:6 c";
83     private static final String KEY1_HEX_DTS_NEO6_MUSIC = "neo 6 music";
84     private static final String KEY2_HEX_DTS_NEO6_MUSIC = "dts neo:6 m";
85     private static final String KEY_HEX_DTS = "dts";
86     private static final String KEY_HEX_DTS_ES = "dts-es";
87     private static final String KEY_HEX_DTS_96 = "dts 96";
88     private static final String KEY_HEX_DD = "dolby digital";
89     private static final String KEY_HEX_DD_EX = "dolby d ex";
90     private static final String KEY_HEX_PCM = "pcm";
91     private static final String KEY_HEX_LPCM = "lpcm";
92     private static final String KEY_HEX_MPEG = "mpeg";
93     private static final String KEY_HEX_BYPASS = "bypass";
94     private static final String KEY1_HEX_ZONE2 = "zone ";
95     private static final String KEY2_HEX_ZONE2 = "zone2 ";
96     private static final String KEY_HEX_ZONE3 = "zone3 ";
97     private static final String KEY_HEX_ZONE4 = "zone4 ";
98     private static final String KEY_HEX_RECORD = "rec ";
99     private static final String SOURCE = "source";
100
101     private final Logger logger = LoggerFactory.getLogger(RotelHexProtocolHandler.class);
102
103     private final Map<RotelSource, String> sourcesLabels;
104
105     private final int size;
106     private final byte[] dataBuffer;
107
108     private boolean startCodeReached;
109     private int count;
110     private int index;
111
112     /**
113      * Constructor
114      *
115      * @param model the Rotel model in use
116      * @param sourcesLabels the custom labels for sources
117      */
118     public RotelHexProtocolHandler(RotelModel model, Map<RotelSource, String> sourcesLabels) {
119         super(model);
120         this.sourcesLabels = sourcesLabels;
121         this.size = (6 + model.getRespNbChars() + model.getRespNbFlags());
122         this.dataBuffer = new byte[size];
123         this.startCodeReached = false;
124         this.count = 0;
125         this.index = 0;
126     }
127
128     @Override
129     public RotelProtocol getProtocol() {
130         return RotelProtocol.HEX;
131     }
132
133     @Override
134     public byte[] buildCommandMessage(RotelCommand cmd, @Nullable Integer value) throws RotelException {
135         if (cmd.getHexType() == 0) {
136             throw new RotelException("Command \"" + cmd.getLabel() + "\" ignored: not available for HEX protocol");
137         }
138         final int size = 6;
139         byte[] message = new byte[size];
140         int idx = 0;
141         message[idx++] = START;
142         message[idx++] = 3;
143         message[idx++] = model.getDeviceId();
144         message[idx++] = cmd.getHexType();
145         message[idx++] = (value == null) ? cmd.getHexKey() : (byte) (value & 0x000000FF);
146         final byte checksum = computeCheckSum(message, idx - 1);
147         if ((checksum & 0x000000FF) == 0x000000FD || (checksum & 0x000000FF) == 0x000000FE) {
148             message = Arrays.copyOf(message, size + 1);
149             message[idx++] = (byte) 0xFD;
150             message[idx++] = ((checksum & 0x000000FF) == 0x000000FD) ? (byte) 0 : (byte) 1;
151         } else {
152             message[idx++] = checksum;
153         }
154         logger.debug("Command \"{}\" => {}", cmd, HexUtils.bytesToHex(message));
155         return message;
156     }
157
158     @Override
159     public void handleIncomingData(byte[] inDataBuffer, int length) {
160         for (int i = 0; i < length; i++) {
161             if (inDataBuffer[i] == RotelHexProtocolHandler.START) {
162                 startCodeReached = true;
163                 count = 0;
164                 index = 0;
165             }
166             if (startCodeReached) {
167                 if (index < size) {
168                     dataBuffer[index++] = inDataBuffer[i];
169                 }
170                 if (index == 2) {
171                     count = inDataBuffer[i];
172                 } else if ((count > 0) && (index == (count + 3))) {
173                     if ((inDataBuffer[i] & 0x000000FF) == 0x000000FD) {
174                         count++;
175                     } else {
176                         byte[] msg = Arrays.copyOf(dataBuffer, index);
177                         handleIncomingMessage(msg);
178                         startCodeReached = false;
179                     }
180                 }
181             }
182         }
183     }
184
185     /**
186      * Validate the content of a feedback message
187      *
188      * @param responseMessage the buffer containing the feedback message
189      *
190      * @throws RotelException - If the message has unexpected content
191      */
192     @Override
193     protected void validateResponse(byte[] responseMessage) throws RotelException {
194         // Check minimum message length
195         if (responseMessage.length < 6) {
196             logger.debug("Unexpected message length: {}", responseMessage.length);
197             throw new RotelException("Unexpected message length");
198         }
199
200         // Check START
201         if (responseMessage[0] != START) {
202             logger.debug("Unexpected START in response: {} rather than {}",
203                     Integer.toHexString(responseMessage[0] & 0x000000FF), Integer.toHexString(START & 0x000000FF));
204             throw new RotelException("Unexpected START in response");
205         }
206
207         // Check ID
208         if (responseMessage[2] != model.getDeviceId()) {
209             logger.debug("Unexpected ID in response: {} rather than {}",
210                     Integer.toHexString(responseMessage[2] & 0x000000FF),
211                     Integer.toHexString(model.getDeviceId() & 0x000000FF));
212             throw new RotelException("Unexpected ID in response");
213         }
214
215         // Check TYPE
216         if (responseMessage[3] != STANDARD_RESPONSE && responseMessage[3] != TRIGGER_STATUS
217                 && responseMessage[3] != SMART_DISPLAY_DATA_1 && responseMessage[3] != SMART_DISPLAY_DATA_2
218                 && responseMessage[3] != PRIMARY_CMD && responseMessage[3] != MAIN_ZONE_CMD
219                 && responseMessage[3] != RECORD_SRC_CMD && responseMessage[3] != ZONE2_CMD
220                 && responseMessage[3] != ZONE3_CMD && responseMessage[3] != ZONE4_CMD
221                 && responseMessage[3] != VOLUME_CMD && responseMessage[3] != ZONE2_VOLUME_CMD
222                 && responseMessage[3] != ZONE3_VOLUME_CMD && responseMessage[3] != ZONE4_VOLUME_CMD
223                 && responseMessage[3] != TRIGGER_CMD) {
224             logger.debug("Unexpected TYPE in response: {}", Integer.toHexString(responseMessage[3] & 0x000000FF));
225             throw new RotelException("Unexpected TYPE in response");
226         }
227
228         int expectedLen = (responseMessage[3] == STANDARD_RESPONSE)
229                 ? (5 + model.getRespNbChars() + model.getRespNbFlags())
230                 : responseMessage.length;
231
232         // Check COUNT
233         if (responseMessage[1] != (expectedLen - 3)) {
234             logger.debug("Unexpected COUNT in response: {} rather than {}",
235                     Integer.toHexString(responseMessage[1] & 0x000000FF),
236                     Integer.toHexString((expectedLen - 3) & 0x000000FF));
237             throw new RotelException("Unexpected COUNT in response");
238         }
239
240         final byte checksum = computeCheckSum(responseMessage, expectedLen - 2);
241         if ((checksum & 0x000000FF) == 0x000000FD || (checksum & 0x000000FF) == 0x000000FE) {
242             expectedLen++;
243         }
244
245         // Check message length
246         if (responseMessage.length != expectedLen) {
247             logger.debug("Unexpected message length: {} rather than {}", responseMessage.length, expectedLen);
248             throw new RotelException("Unexpected message length");
249         }
250
251         // Check sum
252         if ((checksum & 0x000000FF) == 0x000000FD) {
253             if ((responseMessage[responseMessage.length - 2] & 0x000000FF) != 0x000000FD
254                     || (responseMessage[responseMessage.length - 1] & 0x000000FF) != 0) {
255                 logger.debug("Invalid check sum in response: {} rather than FD00", HexUtils.bytesToHex(
256                         Arrays.copyOfRange(responseMessage, responseMessage.length - 2, responseMessage.length)));
257                 throw new RotelException("Invalid check sum in response");
258             }
259         } else if ((checksum & 0x000000FF) == 0x000000FE) {
260             if ((responseMessage[responseMessage.length - 2] & 0x000000FF) != 0x000000FD
261                     || (responseMessage[responseMessage.length - 1] & 0x000000FF) != 1) {
262                 logger.debug("Invalid check sum in response: {} rather than FD01", HexUtils.bytesToHex(
263                         Arrays.copyOfRange(responseMessage, responseMessage.length - 2, responseMessage.length)));
264                 throw new RotelException("Invalid check sum in response");
265             }
266         } else if ((checksum & 0x000000FF) != (responseMessage[responseMessage.length - 1] & 0x000000FF)) {
267             logger.debug("Invalid check sum in response: {} rather than {}",
268                     Integer.toHexString(responseMessage[responseMessage.length - 1] & 0x000000FF),
269                     Integer.toHexString(checksum & 0x000000FF));
270             throw new RotelException("Invalid check sum in response");
271         }
272     }
273
274     /**
275      * Compute the checksum of a message
276      *
277      * @param message the buffer containing the message
278      * @param maxIdx the position in the buffer at which the sum has to be stopped
279      *
280      * @return the checksum as a byte
281      */
282     public static byte computeCheckSum(byte[] message, int maxIdx) {
283         int result = 0;
284         for (int i = 1; i <= maxIdx; i++) {
285             result += (message[i] & 0x000000FF);
286         }
287         return (byte) (result & 0x000000FF);
288     }
289
290     /**
291      * Analyze a valid HEX message and dispatch corresponding (key, value) to the event listeners
292      *
293      * @param incomingMessage the received message
294      */
295     @Override
296     protected void handleValidMessage(byte[] incomingMessage) {
297         if (incomingMessage[3] != STANDARD_RESPONSE) {
298             return;
299         }
300
301         final int idxChars = model.isCharsBeforeFlags() ? 4 : (4 + model.getRespNbFlags());
302
303         // Replace characters with code < 32 by a space before converting to a string
304         for (int i = idxChars; i < (idxChars + model.getRespNbChars()); i++) {
305             if (incomingMessage[i] < 0x20) {
306                 incomingMessage[i] = 0x20;
307             }
308         }
309
310         String value = new String(incomingMessage, idxChars, model.getRespNbChars(), StandardCharsets.US_ASCII);
311         logger.debug("handleValidHexMessage: chars *{}*", value);
312
313         final int idxFlags = model.isCharsBeforeFlags() ? (4 + model.getRespNbChars()) : 4;
314         final byte[] flags = Arrays.copyOfRange(incomingMessage, idxFlags, idxFlags + model.getRespNbFlags());
315         if (logger.isTraceEnabled()) {
316             for (int i = 1; i <= flags.length; i++) {
317                 try {
318                     logger.trace("handleValidHexMessage: Flag {} = {} bits 7-0 = {} {} {} {} {} {} {} {}", i,
319                             Integer.toHexString(flags[i - 1] & 0x000000FF), RotelFlagsMapping.isBitFlagOn(flags, i, 7),
320                             RotelFlagsMapping.isBitFlagOn(flags, i, 6), RotelFlagsMapping.isBitFlagOn(flags, i, 5),
321                             RotelFlagsMapping.isBitFlagOn(flags, i, 4), RotelFlagsMapping.isBitFlagOn(flags, i, 3),
322                             RotelFlagsMapping.isBitFlagOn(flags, i, 2), RotelFlagsMapping.isBitFlagOn(flags, i, 1),
323                             RotelFlagsMapping.isBitFlagOn(flags, i, 0));
324                 } catch (RotelException e1) {
325                 }
326             }
327         }
328         try {
329             dispatchKeyValue(KEY_POWER_ZONE2, model.isZone2On(flags) ? POWER_ON : STANDBY);
330         } catch (RotelException e1) {
331             // Can't get zone power information from flags data, so we just do not notify of this information that way
332         }
333         try {
334             dispatchKeyValue(KEY_POWER_ZONE3, model.isZone3On(flags) ? POWER_ON : STANDBY);
335         } catch (RotelException e1) {
336             // Can't get zone power information from flags data, so we just do not notify of this information that way
337         }
338         try {
339             dispatchKeyValue(KEY_POWER_ZONE4, model.isZone4On(flags) ? POWER_ON : STANDBY);
340         } catch (RotelException e1) {
341             // Can't get zone power information from flags data, so we just do not notify of this information that way
342         }
343         boolean checkMultiIn = false;
344         boolean checkSource = true;
345         try {
346             if (model.isMultiInputOn(flags)) {
347                 checkSource = false;
348                 try {
349                     RotelSource source = model.getSourceFromName(RotelSource.CAT1_MULTI.getName());
350                     RotelCommand cmd = source.getCommand();
351                     if (cmd != null) {
352                         String value2 = cmd.getAsciiCommandV2();
353                         if (value2 != null) {
354                             dispatchKeyValue(KEY_SOURCE, value2);
355                         }
356                     }
357                 } catch (RotelException e1) {
358                     // MULTI source not declared for the model (should not happen), we do not notify of this source
359                 }
360             }
361         } catch (RotelException e1) {
362             // Can't get status of multiple input source from flags data, checkMultiIn is set to true to get this
363             // information in another way
364             checkMultiIn = true;
365         }
366         boolean checkStereo = true;
367         try {
368             checkStereo = !model.isMoreThan2Channels(flags);
369         } catch (RotelException e1) {
370             // Can't get stereo information from flags data, checkStereo is set to true to get this information in
371             // another way
372         }
373
374         String valueLowerCase = value.trim().toLowerCase();
375         if (!valueLowerCase.isEmpty() && !valueLowerCase.startsWith(KEY1_HEX_ZONE2)
376                 && !valueLowerCase.startsWith(KEY2_HEX_ZONE2) && !valueLowerCase.startsWith(KEY_HEX_ZONE3)
377                 && !valueLowerCase.startsWith(KEY_HEX_ZONE4)) {
378             dispatchKeyValue(KEY_POWER, POWER_ON);
379         }
380
381         if (model.getRespNbChars() == 42) {
382             // 2 lines of 21 characters with a left part and a right part
383
384             // Line 1 left
385             value = new String(incomingMessage, idxChars, 14, StandardCharsets.US_ASCII);
386             logger.debug("handleValidHexMessage: line 1 left *{}*", value);
387             parseText(value, checkSource, checkMultiIn, false, false, false, false, false, true);
388
389             // Line 1 right
390             value = new String(incomingMessage, idxChars + 14, 7, StandardCharsets.US_ASCII);
391             logger.debug("handleValidHexMessage: line 1 right *{}*", value);
392             parseText(value, false, false, false, false, false, false, false, true);
393
394             // Full line 1
395             value = new String(incomingMessage, idxChars, 21, StandardCharsets.US_ASCII);
396             dispatchKeyValue(KEY_LINE1, value);
397
398             // Line 2 right
399             value = new String(incomingMessage, idxChars + 35, 7, StandardCharsets.US_ASCII);
400             logger.debug("handleValidHexMessage: line 2 right *{}*", value);
401             parseText(value, false, false, false, false, false, false, false, true);
402
403             // Full line 2
404             value = new String(incomingMessage, idxChars + 21, 21, StandardCharsets.US_ASCII);
405             logger.debug("handleValidHexMessage: line 2 *{}*", value);
406             parseText(value, false, false, true, true, false, true, true, true);
407             dispatchKeyValue(KEY_LINE2, value);
408         } else {
409             value = new String(incomingMessage, idxChars, model.getRespNbChars(), StandardCharsets.US_ASCII);
410             parseText(value, checkSource, checkMultiIn, true, false, true, true, checkStereo, false);
411             dispatchKeyValue(KEY_LINE1, value);
412         }
413
414         if (valueLowerCase.isEmpty()) {
415             dispatchKeyValue(KEY_POWER, POWER_OFF_DELAYED);
416         }
417     }
418
419     /**
420      * Parse a text and dispatch appropriate (key, value) to the event listeners for found information
421      *
422      * @param text the text to be parsed
423      * @param searchSource true if a source has to be searched in the text
424      * @param searchMultiIn true if MULTI IN indication has to be searched in the text
425      * @param searchZone true if a zone information has to be searched in the text
426      * @param searchRecord true if a record source has to be searched in the text
427      * @param searchRecordAfterSource true if a record source has to be searched in the text after the a found source
428      * @param searchDsp true if a DSP mode has to be searched in the text
429      * @param searchStereo true if a STEREO has to be considered in the search
430      * @param multipleInfo true if source and volume/mute are provided separately
431      */
432     private void parseText(String text, boolean searchSource, boolean searchMultiIn, boolean searchZone,
433             boolean searchRecord, boolean searchRecordAfterSource, boolean searchDsp, boolean searchStereo,
434             boolean multipleInfo) {
435         String value = text.trim();
436         String valueLowerCase = value.toLowerCase();
437         if (searchRecord) {
438             dispatchKeyValue(KEY_RECORD_SEL, valueLowerCase.startsWith(KEY_HEX_RECORD) ? MSG_VALUE_ON : MSG_VALUE_OFF);
439         }
440         if (searchZone) {
441             if (valueLowerCase.startsWith(KEY1_HEX_ZONE2) || valueLowerCase.startsWith(KEY2_HEX_ZONE2)) {
442                 dispatchKeyValue(KEY_ZONE, "2");
443             } else if (valueLowerCase.startsWith(KEY_HEX_ZONE3)) {
444                 dispatchKeyValue(KEY_ZONE, "3");
445             } else if (valueLowerCase.startsWith(KEY_HEX_ZONE4)) {
446                 dispatchKeyValue(KEY_ZONE, "4");
447             } else {
448                 dispatchKeyValue(KEY_ZONE, "1");
449             }
450         }
451         if (valueLowerCase.startsWith(KEY1_HEX_VOLUME) || valueLowerCase.startsWith(KEY2_HEX_VOLUME)) {
452             value = extractNumber(value,
453                     valueLowerCase.startsWith(KEY1_HEX_VOLUME) ? KEY1_HEX_VOLUME.length() : KEY2_HEX_VOLUME.length());
454             dispatchKeyValue(KEY_VOLUME, value);
455             dispatchKeyValue(KEY_MUTE, MSG_VALUE_OFF);
456         } else if (valueLowerCase.startsWith(KEY_HEX_MUTE)) {
457             value = value.substring(KEY_HEX_MUTE.length()).trim();
458             if (MSG_VALUE_ON.equalsIgnoreCase(value)) {
459                 dispatchKeyValue(KEY_MUTE, MSG_VALUE_ON);
460             } else {
461                 logger.debug("Invalid value {} for zone mute", value);
462             }
463         } else if (valueLowerCase.startsWith(KEY1_HEX_BASS) || valueLowerCase.startsWith(KEY2_HEX_BASS)) {
464             value = extractNumber(value,
465                     valueLowerCase.startsWith(KEY1_HEX_BASS) ? KEY1_HEX_BASS.length() : KEY2_HEX_BASS.length());
466             dispatchKeyValue(KEY_BASS, value);
467         } else if (valueLowerCase.startsWith(KEY1_HEX_TREBLE) || valueLowerCase.startsWith(KEY2_HEX_TREBLE)) {
468             value = extractNumber(value,
469                     valueLowerCase.startsWith(KEY1_HEX_TREBLE) ? KEY1_HEX_TREBLE.length() : KEY2_HEX_TREBLE.length());
470             dispatchKeyValue(KEY_TREBLE, value);
471         } else if (searchMultiIn && valueLowerCase.startsWith(KEY_HEX_MULTI_IN)) {
472             value = value.substring(KEY_HEX_MULTI_IN.length()).trim();
473             if (MSG_VALUE_ON.equalsIgnoreCase(value)) {
474                 try {
475                     RotelSource source = model.getSourceFromName(RotelSource.CAT1_MULTI.getName());
476                     RotelCommand cmd = source.getCommand();
477                     if (cmd != null) {
478                         String value2 = cmd.getAsciiCommandV2();
479                         if (value2 != null) {
480                             dispatchKeyValue(KEY_SOURCE, value2);
481                         }
482                     }
483                 } catch (RotelException e1) {
484                     // MULTI source not declared for the model (should not happen), we do not notify of this source
485                 }
486             } else if (!MSG_VALUE_OFF.equalsIgnoreCase(value)) {
487                 logger.debug("Invalid value {} for MULTI IN", value);
488             }
489         } else if (searchDsp && valueLowerCase.startsWith(KEY_HEX_BYPASS)) {
490             dispatchKeyValue(KEY_DSP_MODE, RotelDsp.CAT4_NONE.getFeedback());
491         } else if (searchDsp && searchStereo && valueLowerCase.startsWith(KEY_HEX_STEREO)) {
492             dispatchKeyValue(KEY_DSP_MODE, RotelDsp.CAT4_NONE.getFeedback());
493         } else if (searchDsp && (valueLowerCase.startsWith(KEY1_HEX_3CH) || valueLowerCase.startsWith(KEY2_HEX_3CH))) {
494             dispatchKeyValue(KEY_DSP_MODE, RotelDsp.CAT4_STEREO3.getFeedback());
495         } else if (searchDsp && valueLowerCase.startsWith(KEY_HEX_5CH)) {
496             dispatchKeyValue(KEY_DSP_MODE, RotelDsp.CAT4_STEREO5.getFeedback());
497         } else if (searchDsp && valueLowerCase.startsWith(KEY_HEX_7CH)) {
498             dispatchKeyValue(KEY_DSP_MODE, RotelDsp.CAT4_STEREO7.getFeedback());
499         } else if (searchDsp
500                 && (valueLowerCase.startsWith(KEY_HEX_MUSIC1) || valueLowerCase.startsWith(KEY_HEX_DSP1))) {
501             dispatchKeyValue(KEY_DSP_MODE, RotelDsp.CAT4_DSP1.getFeedback());
502         } else if (searchDsp
503                 && (valueLowerCase.startsWith(KEY_HEX_MUSIC2) || valueLowerCase.startsWith(KEY_HEX_DSP2))) {
504             dispatchKeyValue(KEY_DSP_MODE, RotelDsp.CAT4_DSP2.getFeedback());
505         } else if (searchDsp
506                 && (valueLowerCase.startsWith(KEY_HEX_MUSIC3) || valueLowerCase.startsWith(KEY_HEX_DSP3))) {
507             dispatchKeyValue(KEY_DSP_MODE, RotelDsp.CAT4_DSP3.getFeedback());
508         } else if (searchDsp
509                 && (valueLowerCase.startsWith(KEY_HEX_MUSIC4) || valueLowerCase.startsWith(KEY_HEX_DSP4))) {
510             dispatchKeyValue(KEY_DSP_MODE, RotelDsp.CAT4_DSP4.getFeedback());
511         } else if (searchDsp && (valueLowerCase.startsWith(KEY1_HEX_PLII_CINEMA)
512                 || valueLowerCase.startsWith(KEY2_HEX_PLII_CINEMA) || valueLowerCase.startsWith(KEY1_HEX_PLIIX_CINEMA)
513                 || searchDsp && valueLowerCase.startsWith(KEY2_HEX_PLIIX_CINEMA))) {
514             dispatchKeyValue(KEY_DSP_MODE, RotelDsp.CAT2_PLII_CINEMA.getFeedback());
515         } else if (searchDsp && (valueLowerCase.startsWith(KEY1_HEX_PLII_MUSIC)
516                 || valueLowerCase.startsWith(KEY2_HEX_PLII_MUSIC) || valueLowerCase.startsWith(KEY1_HEX_PLIIX_MUSIC)
517                 || valueLowerCase.startsWith(KEY2_HEX_PLIIX_MUSIC))) {
518             dispatchKeyValue(KEY_DSP_MODE, RotelDsp.CAT2_PLII_MUSIC.getFeedback());
519         } else if (searchDsp && (valueLowerCase.startsWith(KEY1_HEX_PLII_GAME)
520                 || valueLowerCase.startsWith(KEY2_HEX_PLII_GAME) || valueLowerCase.startsWith(KEY1_HEX_PLIIX_GAME)
521                 || valueLowerCase.startsWith(KEY2_HEX_PLIIX_GAME))) {
522             dispatchKeyValue(KEY_DSP_MODE, RotelDsp.CAT2_PLII_GAME.getFeedback());
523         } else if (searchDsp && valueLowerCase.startsWith(KEY_HEX_PLIIZ)) {
524             dispatchKeyValue(KEY_DSP_MODE, RotelDsp.CAT4_PLIIZ.getFeedback());
525         } else if (searchDsp
526                 && (valueLowerCase.startsWith(KEY1_HEX_PROLOGIC) || valueLowerCase.startsWith(KEY2_HEX_PROLOGIC))) {
527             dispatchKeyValue(KEY_DSP_MODE, RotelDsp.CAT4_PROLOGIC.getFeedback());
528         } else if (searchDsp && (valueLowerCase.startsWith(KEY1_HEX_DTS_NEO6_CINEMA)
529                 || valueLowerCase.startsWith(KEY2_HEX_DTS_NEO6_CINEMA))) {
530             dispatchKeyValue(KEY_DSP_MODE, RotelDsp.CAT4_NEO6_CINEMA.getFeedback());
531         } else if (searchDsp && (valueLowerCase.startsWith(KEY1_HEX_DTS_NEO6_MUSIC)
532                 || valueLowerCase.startsWith(KEY2_HEX_DTS_NEO6_MUSIC))) {
533             dispatchKeyValue(KEY_DSP_MODE, RotelDsp.CAT4_NEO6_MUSIC.getFeedback());
534         } else if (searchDsp && valueLowerCase.startsWith(KEY_HEX_DTS_ES)) {
535             logger.debug("DTS-ES");
536             dispatchKeyValue(KEY_DSP_MODE, RotelDsp.CAT4_NONE.getFeedback());
537         } else if (searchDsp && valueLowerCase.startsWith(KEY_HEX_DTS_96)) {
538             logger.debug("DTS 96");
539             dispatchKeyValue(KEY_DSP_MODE, RotelDsp.CAT4_NONE.getFeedback());
540         } else if (searchDsp && valueLowerCase.startsWith(KEY_HEX_DTS)) {
541             logger.debug("DTS");
542             dispatchKeyValue(KEY_DSP_MODE, RotelDsp.CAT4_NONE.getFeedback());
543         } else if (searchDsp && valueLowerCase.startsWith(KEY_HEX_DD_EX)) {
544             logger.debug("DD-EX");
545             dispatchKeyValue(KEY_DSP_MODE, RotelDsp.CAT4_NONE.getFeedback());
546         } else if (searchDsp && valueLowerCase.startsWith(KEY_HEX_DD)) {
547             logger.debug("DD");
548             dispatchKeyValue(KEY_DSP_MODE, RotelDsp.CAT4_NONE.getFeedback());
549         } else if (searchDsp && valueLowerCase.startsWith(KEY_HEX_LPCM)) {
550             logger.debug("LPCM");
551             dispatchKeyValue(KEY_DSP_MODE, RotelDsp.CAT4_NONE.getFeedback());
552         } else if (searchDsp && valueLowerCase.startsWith(KEY_HEX_PCM)) {
553             logger.debug("PCM");
554             dispatchKeyValue(KEY_DSP_MODE, RotelDsp.CAT4_NONE.getFeedback());
555         } else if (searchDsp && valueLowerCase.startsWith(KEY_HEX_MPEG)) {
556             logger.debug("MPEG");
557             dispatchKeyValue(KEY_DSP_MODE, RotelDsp.CAT4_NONE.getFeedback());
558         } else if (searchZone
559                 && (valueLowerCase.startsWith(KEY1_HEX_ZONE2) || valueLowerCase.startsWith(KEY2_HEX_ZONE2))) {
560             value = value.substring(
561                     valueLowerCase.startsWith(KEY1_HEX_ZONE2) ? KEY1_HEX_ZONE2.length() : KEY2_HEX_ZONE2.length());
562             parseZone2(value, multipleInfo);
563         } else if (searchZone && valueLowerCase.startsWith(KEY_HEX_ZONE3)) {
564             parseZone3(value.substring(KEY_HEX_ZONE3.length()), multipleInfo);
565         } else if (searchZone && valueLowerCase.startsWith(KEY_HEX_ZONE4)) {
566             parseZone4(value.substring(KEY_HEX_ZONE4.length()), multipleInfo);
567         } else if (searchRecord && valueLowerCase.startsWith(KEY_HEX_RECORD)) {
568             parseRecord(value.substring(KEY_HEX_RECORD.length()));
569         } else if (searchSource || searchRecordAfterSource) {
570             parseSourceAndRecord(value, searchSource, searchRecordAfterSource, multipleInfo);
571         }
572     }
573
574     /**
575      * Parse a text to identify a source
576      *
577      * @param text the text to be parsed
578      * @param acceptFollowMain true if follow main has to be considered in the search
579      *
580      * @return the identified source or null if no source is identified in the text
581      */
582     private @Nullable RotelSource parseSource(String text, boolean acceptFollowMain) {
583         String value = text.trim();
584         RotelSource source = null;
585         if (!value.isEmpty()) {
586             if (acceptFollowMain && SOURCE.equalsIgnoreCase(value)) {
587                 try {
588                     source = model.getSourceFromName(RotelSource.CAT1_FOLLOW_MAIN.getName());
589                 } catch (RotelException e) {
590                     // MAIN (follow main zone source) source not declared for the model, we return null
591                 }
592             } else {
593                 for (RotelSource src : sourcesLabels.keySet()) {
594                     String label = sourcesLabels.get(src);
595                     if (label != null && value.startsWith(label)) {
596                         if (source == null || sourcesLabels.get(source).length() < label.length()) {
597                             source = src;
598                         }
599                     }
600                 }
601             }
602         }
603         return source;
604     }
605
606     private void parseSourceAndRecord(String text, boolean searchSource, boolean searchRecordAfterSource,
607             boolean multipleInfo) {
608         RotelSource source = parseSource(text, false);
609         if (source != null) {
610             if (searchSource) {
611                 RotelCommand cmd = source.getCommand();
612                 if (cmd != null) {
613                     String value2 = cmd.getAsciiCommandV2();
614                     if (value2 != null) {
615                         dispatchKeyValue(KEY_SOURCE, value2);
616                         if (!multipleInfo) {
617                             dispatchKeyValue(KEY_MUTE, MSG_VALUE_OFF);
618                         }
619                     }
620                 }
621             }
622
623             if (searchRecordAfterSource) {
624                 String value = text.substring(getSourceLabel(source).length()).trim();
625                 source = parseSource(value, true);
626                 if (source != null) {
627                     RotelCommand cmd = source.getRecordCommand();
628                     if (cmd != null) {
629                         value = cmd.getAsciiCommandV2();
630                         if (value != null) {
631                             dispatchKeyValue(KEY_RECORD, value);
632                         }
633                     }
634                 }
635             }
636         }
637     }
638
639     private String getSourceLabel(RotelSource source) {
640         String label = sourcesLabels.get(source);
641         return (label == null) ? source.getLabel() : label;
642     }
643
644     private void parseRecord(String text) {
645         String value = text.trim();
646         RotelSource source = parseSource(value, true);
647         if (source != null) {
648             RotelCommand cmd = source.getRecordCommand();
649             if (cmd != null) {
650                 value = cmd.getAsciiCommandV2();
651                 if (value != null) {
652                     dispatchKeyValue(KEY_RECORD, value);
653                 }
654             }
655         } else {
656             logger.debug("Invalid value {} for record source", value);
657         }
658     }
659
660     private void parseZone2(String text, boolean multipleInfo) {
661         String value = text.trim();
662         String valueLowerCase = value.toLowerCase();
663         if (valueLowerCase.startsWith(KEY1_HEX_VOLUME) || valueLowerCase.startsWith(KEY2_HEX_VOLUME)) {
664             value = extractNumber(value,
665                     valueLowerCase.startsWith(KEY1_HEX_VOLUME) ? KEY1_HEX_VOLUME.length() : KEY2_HEX_VOLUME.length());
666             dispatchKeyValue(KEY_VOLUME_ZONE2, value);
667             dispatchKeyValue(KEY_MUTE_ZONE2, MSG_VALUE_OFF);
668         } else if (valueLowerCase.startsWith(KEY_HEX_MUTE)) {
669             value = value.substring(KEY_HEX_MUTE.length()).trim();
670             if (MSG_VALUE_ON.equalsIgnoreCase(value)) {
671                 dispatchKeyValue(KEY_MUTE_ZONE2, MSG_VALUE_ON);
672             } else {
673                 logger.debug("Invalid value {} for zone mute", value);
674             }
675         } else if (!MSG_VALUE_OFF.equalsIgnoreCase(value)) {
676             RotelSource source = parseSource(value, true);
677             if (source != null) {
678                 RotelCommand cmd = source.getZoneCommand(2);
679                 if (cmd != null) {
680                     value = cmd.getAsciiCommandV2();
681                     if (value != null) {
682                         dispatchKeyValue(KEY_SOURCE_ZONE2, value);
683                         if (!multipleInfo) {
684                             dispatchKeyValue(KEY_MUTE_ZONE2, MSG_VALUE_OFF);
685                         }
686                     }
687                 }
688             } else {
689                 logger.debug("Invalid value {} for zone 2 source", value);
690             }
691         }
692     }
693
694     private void parseZone3(String text, boolean multipleInfo) {
695         String value = text.trim();
696         String valueLowerCase = value.toLowerCase();
697         if (valueLowerCase.startsWith(KEY1_HEX_VOLUME) || valueLowerCase.startsWith(KEY2_HEX_VOLUME)) {
698             value = extractNumber(value,
699                     valueLowerCase.startsWith(KEY1_HEX_VOLUME) ? KEY1_HEX_VOLUME.length() : KEY2_HEX_VOLUME.length());
700             dispatchKeyValue(KEY_VOLUME_ZONE3, value);
701             dispatchKeyValue(KEY_MUTE_ZONE3, MSG_VALUE_OFF);
702         } else if (valueLowerCase.startsWith(KEY_HEX_MUTE)) {
703             value = value.substring(KEY_HEX_MUTE.length()).trim();
704             if (MSG_VALUE_ON.equalsIgnoreCase(value)) {
705                 dispatchKeyValue(KEY_MUTE_ZONE3, MSG_VALUE_ON);
706             } else {
707                 logger.debug("Invalid value {} for zone mute", value);
708             }
709         } else if (!MSG_VALUE_OFF.equalsIgnoreCase(value)) {
710             RotelSource source = parseSource(value, true);
711             if (source != null) {
712                 RotelCommand cmd = source.getZoneCommand(3);
713                 if (cmd != null) {
714                     value = cmd.getAsciiCommandV2();
715                     if (value != null) {
716                         dispatchKeyValue(KEY_SOURCE_ZONE3, value);
717                         if (!multipleInfo) {
718                             dispatchKeyValue(KEY_MUTE_ZONE3, MSG_VALUE_OFF);
719                         }
720                     }
721                 }
722             } else {
723                 logger.debug("Invalid value {} for zone 3 source", value);
724             }
725         }
726     }
727
728     private void parseZone4(String text, boolean multipleInfo) {
729         String value = text.trim();
730         String valueLowerCase = value.toLowerCase();
731         if (valueLowerCase.startsWith(KEY1_HEX_VOLUME) || valueLowerCase.startsWith(KEY2_HEX_VOLUME)) {
732             value = extractNumber(value,
733                     valueLowerCase.startsWith(KEY1_HEX_VOLUME) ? KEY1_HEX_VOLUME.length() : KEY2_HEX_VOLUME.length());
734             dispatchKeyValue(KEY_VOLUME_ZONE4, value);
735             dispatchKeyValue(KEY_MUTE_ZONE4, MSG_VALUE_OFF);
736         } else if (valueLowerCase.startsWith(KEY_HEX_MUTE)) {
737             value = value.substring(KEY_HEX_MUTE.length()).trim();
738             if (MSG_VALUE_ON.equalsIgnoreCase(value)) {
739                 dispatchKeyValue(KEY_MUTE_ZONE4, MSG_VALUE_ON);
740             } else {
741                 logger.debug("Invalid value {} for zone mute", value);
742             }
743         } else if (!MSG_VALUE_OFF.equalsIgnoreCase(value)) {
744             RotelSource source = parseSource(value, true);
745             if (source != null) {
746                 RotelCommand cmd = source.getZoneCommand(4);
747                 if (cmd != null) {
748                     value = cmd.getAsciiCommandV2();
749                     if (value != null) {
750                         dispatchKeyValue(KEY_SOURCE_ZONE4, value);
751                         if (!multipleInfo) {
752                             dispatchKeyValue(KEY_MUTE_ZONE4, MSG_VALUE_OFF);
753                         }
754                     }
755                 }
756             } else {
757                 logger.debug("Invalid value {} for zone 4 source", value);
758             }
759         }
760     }
761
762     /**
763      * Extract from a string a number
764      *
765      * @param value the string
766      * @param startIndex the index in the string at which the integer has to be extracted
767      *
768      * @return the number as a string with its sign and no blank between the sign and the digits
769      */
770     private String extractNumber(String value, int startIndex) {
771         String result = value.substring(startIndex).trim();
772         // Delete possible blank(s) between the sign and the number
773         if (result.startsWith("+") || result.startsWith("-")) {
774             result = result.substring(0, 1) + result.substring(1, result.length()).trim();
775         }
776         return result;
777     }
778 }