]> git.basschouten.com Git - openhab-addons.git/blob
efd3f42573589f102590f2707959008f2b3d9a57
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2020 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.atlona.internal.pro3;
14
15 import java.io.IOException;
16 import java.util.concurrent.ArrayBlockingQueue;
17 import java.util.concurrent.BlockingQueue;
18 import java.util.concurrent.TimeUnit;
19 import java.util.regex.Matcher;
20 import java.util.regex.Pattern;
21
22 import org.apache.commons.lang.StringUtils;
23 import org.openhab.binding.atlona.internal.AtlonaHandlerCallback;
24 import org.openhab.binding.atlona.internal.net.SocketSession;
25 import org.openhab.binding.atlona.internal.net.SocketSessionListener;
26 import org.openhab.core.library.types.DecimalType;
27 import org.openhab.core.library.types.OnOffType;
28 import org.openhab.core.thing.ThingStatus;
29 import org.openhab.core.thing.ThingStatusDetail;
30 import org.slf4j.Logger;
31 import org.slf4j.LoggerFactory;
32
33 /**
34  * This is the protocol handler for the PRO3 product line. This handler will issue the protocol commands and will
35  * process the responses from the PRO3 switch. This handler was written to respond to any response that can be sent from
36  * the TCP/IP session (either in response to our own commands or in response to external events [other TCP/IP sessions,
37  * web GUI, front panel keystrokes, etc]).
38  *
39  * @author Tim Roberts - Initial contribution
40  */
41 class AtlonaPro3PortocolHandler {
42     private final Logger logger = LoggerFactory.getLogger(AtlonaPro3PortocolHandler.class);
43
44     /**
45      * The {@link SocketSession} used by this protocol handler
46      */
47     private final SocketSession session;
48
49     /**
50      * The {@link AtlonaPro3Config} configuration used by this handler
51      */
52     private final AtlonaPro3Config config;
53
54     /**
55      * The {@link AtlonaPro3Capabilities} of the PRO3 model
56      */
57     private final AtlonaPro3Capabilities capabilities;
58
59     /**
60      * The {@link AtlonaPro3Handler} to call back to update status and state
61      */
62     private final AtlonaHandlerCallback callback;
63
64     /**
65      * The model type identified by the switch. We save it for faster refreshes since it will not change
66      */
67     private String modelType;
68
69     /**
70      * The version (firmware) identified by the switch. We save it for faster refreshes since it will not change between
71      * sessions
72      */
73     private String version;
74
75     /**
76      * A special (invalid) command used internally by this handler to identify whether the switch wants a login or not
77      * (see {@link #login()})
78      */
79     private static final String NOTVALID_USER_OR_CMD = "notvalid$934%912";
80
81     // ------------------------------------------------------------------------------------------------
82     // The following are the various command formats specified by the Atlona protocol
83     private static final String CMD_POWERON = "PWON";
84     private static final String CMD_POWEROFF = "PWOFF";
85     private static final String CMD_POWER_STATUS = "PWSTA";
86     private static final String CMD_VERSION = "Version";
87     private static final String CMD_TYPE = "Type";
88     private static final String CMD_PANELLOCK = "Lock";
89     private static final String CMD_PANELUNLOCK = "Unlock";
90     private static final String CMD_PORT_RESETALL = "All#";
91     private static final String CMD_PORT_POWER_FORMAT = "x%d$ %s";
92     private static final String CMD_PORT_ALL_FORMAT = "x%dAll";
93     private static final String CMD_PORT_SWITCH_FORMAT = "x%dAVx%d";
94     private static final String CMD_PORT_MIRROR_FORMAT = "MirrorHdmi%d Out%d";
95     private static final String CMD_PORT_MIRROR_STATUS_FORMAT = "MirrorHdmi%d sta";
96     private static final String CMD_PORT_UNMIRROR_FORMAT = "UnMirror%d";
97     private static final String CMD_VOLUME_FORMAT = "VOUT%d %s";
98     private static final String CMD_VOLUME_MUTE_FORMAT = "VOUTMute%d %s";
99     private static final String CMD_IROFF = "IROFF";
100     private static final String CMD_IRON = "IRON";
101     private static final String CMD_PORT_STATUS = "Status";
102     private static final String CMD_PORT_STATUS_FORMAT = "Statusx%d";
103     private static final String CMD_SAVEIO_FORMAT = "Save%d";
104     private static final String CMD_RECALLIO_FORMAT = "Recall%d";
105     private static final String CMD_CLEARIO_FORMAT = "Clear%d";
106     private static final String CMD_MATRIX_RESET = "Mreset";
107     private static final String CMD_BROADCAST_ON = "Broadcast on";
108
109     // ------------------------------------------------------------------------------------------------
110     // The following are the various responses specified by the Atlona protocol
111     private static final String RSP_FAILED = "Command FAILED:";
112
113     private static final String RSP_LOGIN = "Login";
114     private static final String RSP_PASSWORD = "Password";
115
116     private final Pattern powerStatusPattern = Pattern.compile("PW(\\w+)");
117     private final Pattern versionPattern = Pattern.compile("Firmware (.*)");
118     private final Pattern typePattern = Pattern.compile("AT-UHD-PRO3-(\\d+)M");
119     private static final String RSP_ALL = "All#";
120     private static final String RSP_LOCK = "Lock";
121     private static final String RSP_UNLOCK = "Unlock";
122     private final Pattern portStatusPattern = Pattern.compile("x(\\d+)AVx(\\d+),?+");
123     private final Pattern portPowerPattern = Pattern.compile("x(\\d+)\\$ (\\w+)");
124     private final Pattern portAllPattern = Pattern.compile("x(\\d+)All");
125     private final Pattern portMirrorPattern = Pattern.compile("MirrorHdmi(\\d+) (\\p{Alpha}+)(\\d*)");
126     private final Pattern portUnmirrorPattern = Pattern.compile("UnMirror(\\d+)");
127     private final Pattern volumePattern = Pattern.compile("VOUT(\\d+) (-?\\d+)");
128     private final Pattern volumeMutePattern = Pattern.compile("VOUTMute(\\d+) (\\w+)");
129     private static final String RSP_IROFF = "IROFF";
130     private static final String RSP_IRON = "IRON";
131     private final Pattern saveIoPattern = Pattern.compile("Save(\\d+)");
132     private final Pattern recallIoPattern = Pattern.compile("Recall(\\d+)");
133     private final Pattern clearIoPattern = Pattern.compile("Clear(\\d+)");
134     private final Pattern broadCastPattern = Pattern.compile("Broadcast (\\w+)");
135     private static final String RSP_MATRIX_RESET = "Mreset";
136
137     // ------------------------------------------------------------------------------------------------
138     // The following isn't part of the atlona protocol and is generated by us
139     private static final String CMD_PING = "ping";
140     private static final String RSP_PING = "Command FAILED: (ping)";
141
142     /**
143      * Constructs the protocol handler from given parameters
144      *
145      * @param session a non-null {@link SocketSession} (may be connected or disconnected)
146      * @param config a non-null {@link AtlonaPro3Config}
147      * @param capabilities a non-null {@link AtlonaPro3Capabilities}
148      * @param callback a non-null {@link AtlonaHandlerCallback} to update state and status
149      */
150     AtlonaPro3PortocolHandler(SocketSession session, AtlonaPro3Config config, AtlonaPro3Capabilities capabilities,
151             AtlonaHandlerCallback callback) {
152         if (session == null) {
153             throw new IllegalArgumentException("session cannot be null");
154         }
155
156         if (config == null) {
157             throw new IllegalArgumentException("config cannot be null");
158         }
159
160         if (capabilities == null) {
161             throw new IllegalArgumentException("capabilities cannot be null");
162         }
163
164         if (callback == null) {
165             throw new IllegalArgumentException("callback cannot be null");
166         }
167
168         this.session = session;
169         this.config = config;
170         this.capabilities = capabilities;
171         this.callback = callback;
172     }
173
174     /**
175      * Attempts to log into the switch when prompted by the switch. Please see code comments on the exact protocol for
176      * this.
177      *
178      * @return a null if logged in successfully (or if switch didn't require login). Non-null if an exception occurred.
179      * @throws IOException an IO exception occurred during login
180      */
181     String login() throws Exception {
182         logger.debug("Logging into atlona switch");
183         // Void to make sure we retrieve them
184         modelType = null;
185         version = null;
186
187         NoDispatchingCallback callback = new NoDispatchingCallback();
188         session.addListener(callback);
189
190         // Burn the initial (empty) return
191         String response;
192         try {
193             response = callback.getResponse();
194             if (!response.equals("")) {
195                 logger.info("Altona protocol violation - didn't start with an inital empty response: '{}'", response);
196             }
197         } catch (Exception e) {
198             // ignore - may not having given us an initial ""
199         }
200
201         // At this point - we are not sure if it's:
202         // 1) waiting for a command input
203         // or 2) has sent a "Login: " prompt
204         // By sending a string that doesn't exist as a command or user
205         // we can tell which by the response to the invalid command
206         session.sendCommand(NOTVALID_USER_OR_CMD);
207
208         // Command failed - Altona not configured with IPLogin - return success
209         response = callback.getResponse();
210         if (response.startsWith(RSP_FAILED)) {
211             logger.debug("Altona didn't require a login");
212             postLogin();
213             return null;
214         }
215
216         // We should have been presented wit a new "\r\nLogin: "
217         response = callback.getResponse();
218         if (!response.equals("")) {
219             logger.info("Altona protocol violation - didn't start with an inital empty response: '{}'", response);
220         }
221
222         // Get the new "Login: " prompt response
223         response = callback.getResponse();
224         if (response.equals(RSP_LOGIN)) {
225             if (config.getUserName() == null || config.getUserName().trim().length() == 0) {
226                 return "Atlona PRO3 has enabled Telnet/IP Login but no username was provided in the configuration.";
227             }
228
229             // Send the username and wait for a ": " response
230             session.sendCommand(config.getUserName());
231         } else {
232             return "Altona protocol violation - wasn't initially a command failure or login prompt: " + response;
233         }
234
235         // We should have gotten the password response
236         response = callback.getResponse();
237
238         // Burn the empty response if we got one (
239         if (response.equals("")) {
240             response = callback.getResponse();
241         }
242         if (!response.equals(RSP_PASSWORD)) {
243             // If we got another login response, username wasn't valid
244             if (response.equals(RSP_LOGIN)) {
245                 return "Username " + config.getUserName() + " is not a valid user on the atlona";
246             }
247             return "Altona protocol violation - invalid response to a login: " + response;
248         }
249
250         // Make sure we have a password
251         if (config.getPassword() == null || config.getPassword().trim().length() == 0) {
252             return "Atlona PRO3 has enabled Telnet/IP Login but no password was provided in the configuration.";
253         }
254
255         // Send the password
256         session.sendCommand(config.getPassword());
257         response = callback.getResponse();
258
259         // At this point, we don't know if we received a
260         // 1) "\r\n" and waiting for a command
261         // or 2) "\r\nLogin: " if the password is invalid
262         // Send an invalid command to see if we get the failed command response
263
264         // First make sure we had an empty response (the "\r\n" part)
265         if (!response.equals("")) {
266             logger.info("Altona protocol violation - not an empty response after password: '{}'", response);
267         }
268
269         // Now send an invalid command
270         session.sendCommand(NOTVALID_USER_OR_CMD);
271
272         // If we get an invalid command response - we are logged in
273         response = callback.getResponse();
274         if (response.startsWith(RSP_FAILED)) {
275             postLogin();
276             return null;
277         }
278
279         // Nope - password invalid
280         return "Password was invalid - please check your atlona setup";
281     }
282
283     /**
284      * Post successful login stuff - mark us online and refresh from the switch
285      */
286     private void postLogin() {
287         logger.debug("Atlona switch now connected");
288         session.clearListeners();
289         session.addListener(new NormalResponseCallback());
290         callback.statusChanged(ThingStatus.ONLINE, ThingStatusDetail.NONE, null);
291
292         // Set broadcast to on to receive notifications when
293         // routing changes (via the webpage, or presets or IR, etc)
294         sendCommand(CMD_BROADCAST_ON);
295
296         // setup the most likely state of these switches (there is no protocol to get them)
297         refreshAll();
298     }
299
300     /**
301      * Returns the callback being used by this handler
302      *
303      * @return a non-null {@link AtlonaHandlerCallback}
304      */
305     AtlonaHandlerCallback getCallback() {
306         return callback;
307     }
308
309     /**
310      * Pings the server with an (invalid) ping command to keep the connection alive
311      */
312     void ping() {
313         sendCommand(CMD_PING);
314     }
315
316     /**
317      * Refreshes the state from the switch itself. This will retrieve all the state (that we can get) from the switch.
318      */
319     void refreshAll() {
320         logger.debug("Refreshing matrix state");
321         if (version == null) {
322             refreshVersion();
323         } else {
324             callback.setProperty(AtlonaPro3Constants.PROPERTY_VERSION, version);
325         }
326
327         if (modelType == null) {
328             refreshType();
329         } else {
330             callback.setProperty(AtlonaPro3Constants.PROPERTY_TYPE, modelType);
331         }
332
333         refreshPower();
334         refreshAllPortStatuses();
335
336         final int nbrPowerPorts = capabilities.getNbrPowerPorts();
337         for (int x = 1; x <= nbrPowerPorts; x++) {
338             refreshPortPower(x);
339         }
340
341         final int nbrAudioPorts = capabilities.getNbrAudioPorts();
342         for (int x = 1; x <= nbrAudioPorts; x++) {
343             refreshVolumeStatus(x);
344             refreshVolumeMute(x);
345         }
346
347         for (int x : capabilities.getHdmiPorts()) {
348             refreshPortStatus(x);
349         }
350     }
351
352     /**
353      * Sets the power to the switch
354      *
355      * @param on true if on, false otherwise
356      */
357     void setPower(boolean on) {
358         sendCommand(on ? CMD_POWERON : CMD_POWEROFF);
359     }
360
361     /**
362      * Queries the switch about it's power state
363      */
364     void refreshPower() {
365         sendCommand(CMD_POWER_STATUS);
366     }
367
368     /**
369      * Queries the switch about it's version (firmware)
370      */
371     void refreshVersion() {
372         sendCommand(CMD_VERSION);
373     }
374
375     /**
376      * Queries the switch about it's type (model)
377      */
378     void refreshType() {
379         sendCommand(CMD_TYPE);
380     }
381
382     /**
383      * Sets whether the front panel is locked or not
384      *
385      * @param locked true if locked, false otherwise
386      */
387     void setPanelLock(boolean locked) {
388         sendCommand(locked ? CMD_PANELLOCK : CMD_PANELUNLOCK);
389     }
390
391     /**
392      * Resets all ports back to their default state.
393      */
394     void resetAllPorts() {
395         sendCommand(CMD_PORT_RESETALL);
396     }
397
398     /**
399      * Sets whether the specified port is powered (i.e. outputing).
400      *
401      * @param portNbr a greater than zero port number
402      * @param on true if powered.
403      */
404     void setPortPower(int portNbr, boolean on) {
405         if (portNbr <= 0) {
406             throw new IllegalArgumentException("portNbr must be greater than 0");
407         }
408         sendCommand(String.format(CMD_PORT_POWER_FORMAT, portNbr, on ? "on" : "off"));
409     }
410
411     /**
412      * Refreshes whether the specified port is powered (i.e. outputing).
413      *
414      * @param portNbr a greater than zero port number
415      */
416     void refreshPortPower(int portNbr) {
417         if (portNbr <= 0) {
418             throw new IllegalArgumentException("portNbr must be greater than 0");
419         }
420         sendCommand(String.format(CMD_PORT_POWER_FORMAT, portNbr, "sta"));
421     }
422
423     /**
424      * Sets all the output ports to the specified input port.
425      *
426      * @param portNbr a greater than zero port number
427      */
428     void setPortAll(int portNbr) {
429         if (portNbr <= 0) {
430             throw new IllegalArgumentException("portNbr must be greater than 0");
431         }
432         sendCommand(String.format(CMD_PORT_ALL_FORMAT, portNbr));
433     }
434
435     /**
436      * Sets the input port number to the specified output port number.
437      *
438      * @param inPortNbr a greater than zero port number
439      * @param outPortNbr a greater than zero port number
440      */
441     void setPortSwitch(int inPortNbr, int outPortNbr) {
442         if (inPortNbr <= 0) {
443             throw new IllegalArgumentException("inPortNbr must be greater than 0");
444         }
445         if (outPortNbr <= 0) {
446             throw new IllegalArgumentException("outPortNbr must be greater than 0");
447         }
448         sendCommand(String.format(CMD_PORT_SWITCH_FORMAT, inPortNbr, outPortNbr));
449     }
450
451     /**
452      * Sets the hdmi port number to mirror the specified output port number.
453      *
454      * @param hdmiPortNbr a greater than zero port number
455      * @param outPortNbr a greater than zero port number
456      */
457     void setPortMirror(int hdmiPortNbr, int outPortNbr) {
458         if (hdmiPortNbr <= 0) {
459             throw new IllegalArgumentException("hdmiPortNbr must be greater than 0");
460         }
461         if (outPortNbr <= 0) {
462             throw new IllegalArgumentException("outPortNbr must be greater than 0");
463         }
464
465         if (capabilities.getHdmiPorts().contains(hdmiPortNbr)) {
466             sendCommand(String.format(CMD_PORT_MIRROR_FORMAT, hdmiPortNbr, outPortNbr));
467         } else {
468             logger.info("Trying to set port mirroring on a non-hdmi port: {}", hdmiPortNbr);
469         }
470     }
471
472     /**
473      * Disabled mirroring on the specified hdmi port number.
474      *
475      * @param hdmiPortNbr a greater than zero port number
476      * @param outPortNbr a greater than zero port number
477      */
478     void removePortMirror(int hdmiPortNbr) {
479         if (hdmiPortNbr <= 0) {
480             throw new IllegalArgumentException("hdmiPortNbr must be greater than 0");
481         }
482
483         if (capabilities.getHdmiPorts().contains(hdmiPortNbr)) {
484             sendCommand(String.format(CMD_PORT_UNMIRROR_FORMAT, hdmiPortNbr));
485         } else {
486             logger.info("Trying to remove port mirroring on a non-hdmi port: {}", hdmiPortNbr);
487         }
488     }
489
490     /**
491      * Sets the volume level on the specified audio port.
492      *
493      * @param portNbr a greater than zero port number
494      * @param level a volume level in decibels (must range from -79 to +15)
495      */
496     void setVolume(int portNbr, double level) {
497         if (portNbr <= 0) {
498             throw new IllegalArgumentException("portNbr must be greater than 0");
499         }
500         if (level < -79 || level > 15) {
501             throw new IllegalArgumentException("level must be between -79 to +15");
502         }
503         sendCommand(String.format(CMD_VOLUME_FORMAT, portNbr, level));
504     }
505
506     /**
507      * Refreshes the volume level for the given audio port.
508      *
509      * @param portNbr a greater than zero port number
510      */
511     void refreshVolumeStatus(int portNbr) {
512         if (portNbr <= 0) {
513             throw new IllegalArgumentException("portNbr must be greater than 0");
514         }
515         sendCommand(String.format(CMD_VOLUME_FORMAT, portNbr, "sta"));
516     }
517
518     /**
519      * Refreshes the specified hdmi port's mirroring status
520      *
521      * @param hdmiPortNbr a greater than zero hdmi port number
522      */
523     void refreshPortMirror(int hdmiPortNbr) {
524         if (hdmiPortNbr <= 0) {
525             throw new IllegalArgumentException("hdmiPortNbr must be greater than 0");
526         }
527         sendCommand(String.format(CMD_PORT_MIRROR_STATUS_FORMAT, hdmiPortNbr));
528     }
529
530     /**
531      * Mutes/Unmutes the specified audio port.
532      *
533      * @param portNbr a greater than zero port number
534      * @param mute true to mute, false to unmute
535      */
536     void setVolumeMute(int portNbr, boolean mute) {
537         if (portNbr <= 0) {
538             throw new IllegalArgumentException("portNbr must be greater than 0");
539         }
540         sendCommand(String.format(CMD_VOLUME_MUTE_FORMAT, portNbr, mute ? "on" : "off"));
541     }
542
543     /**
544      * Refreshes the volume mute for the given audio port.
545      *
546      * @param portNbr a greater than zero port number
547      */
548     void refreshVolumeMute(int portNbr) {
549         if (portNbr <= 0) {
550             throw new IllegalArgumentException("portNbr must be greater than 0");
551         }
552         sendCommand(String.format(CMD_VOLUME_MUTE_FORMAT, portNbr, "sta"));
553     }
554
555     /**
556      * Turn on/off the front panel IR.
557      *
558      * @param on true for on, false otherwise
559      */
560     void setIrOn(boolean on) {
561         sendCommand(on ? CMD_IRON : CMD_IROFF);
562     }
563
564     /**
565      * Refreshes the input port setting on the specified output port.
566      *
567      * @param portNbr a greater than zero port number
568      */
569     void refreshPortStatus(int portNbr) {
570         if (portNbr <= 0) {
571             throw new IllegalArgumentException("portNbr must be greater than 0");
572         }
573         sendCommand(String.format(CMD_PORT_STATUS_FORMAT, portNbr));
574     }
575
576     /**
577      * Refreshes all of the input port settings for all of the output ports.
578      */
579     private void refreshAllPortStatuses() {
580         sendCommand(CMD_PORT_STATUS);
581     }
582
583     /**
584      * Saves the current Input/Output scheme to the specified preset number.
585      *
586      * @param presetNbr a greater than 0 preset number
587      */
588     void saveIoSettings(int presetNbr) {
589         if (presetNbr <= 0) {
590             throw new IllegalArgumentException("presetNbr must be greater than 0");
591         }
592         sendCommand(String.format(CMD_SAVEIO_FORMAT, presetNbr));
593     }
594
595     /**
596      * Recalls the Input/Output scheme for the specified preset number.
597      *
598      * @param presetNbr a greater than 0 preset number
599      */
600     void recallIoSettings(int presetNbr) {
601         if (presetNbr <= 0) {
602             throw new IllegalArgumentException("presetNbr must be greater than 0");
603         }
604         sendCommand(String.format(CMD_RECALLIO_FORMAT, presetNbr));
605     }
606
607     /**
608      * Clears the Input/Output scheme for the specified preset number.
609      *
610      * @param presetNbr a greater than 0 preset number
611      */
612     void clearIoSettings(int presetNbr) {
613         if (presetNbr <= 0) {
614             throw new IllegalArgumentException("presetNbr must be greater than 0");
615         }
616         sendCommand(String.format(CMD_CLEARIO_FORMAT, presetNbr));
617     }
618
619     /**
620      * Resets the matrix back to defaults.
621      */
622     void resetMatrix() {
623         sendCommand(CMD_MATRIX_RESET);
624     }
625
626     /**
627      * Sends the command and puts the thing into {@link ThingStatus#OFFLINE} if an IOException occurs
628      *
629      * @param command a non-null, non-empty command to send
630      */
631     private void sendCommand(String command) {
632         if (command == null) {
633             throw new IllegalArgumentException("command cannot be null");
634         }
635         if (command.trim().length() == 0) {
636             throw new IllegalArgumentException("command cannot be empty");
637         }
638         try {
639             session.sendCommand(command);
640         } catch (IOException e) {
641             callback.statusChanged(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
642                     "Exception occurred sending to Atlona: " + e);
643         }
644     }
645
646     /**
647      * Handles the switch power response. The first matching group should be "on" or "off"
648      *
649      * @param m the non-null {@link Matcher} that matched the response
650      * @param resp the possibly null, possibly empty actual response
651      */
652     private void handlePowerResponse(Matcher m, String resp) {
653         if (m == null) {
654             throw new IllegalArgumentException("m (matcher) cannot be null");
655         }
656         if (m.groupCount() == 1) {
657             switch (m.group(1)) {
658                 case "ON":
659                     callback.stateChanged(AtlonaPro3Utilities.createChannelID(AtlonaPro3Constants.GROUP_PRIMARY,
660                             AtlonaPro3Constants.CHANNEL_POWER), OnOffType.ON);
661                     break;
662                 case "OFF":
663                     callback.stateChanged(AtlonaPro3Utilities.createChannelID(AtlonaPro3Constants.GROUP_PRIMARY,
664                             AtlonaPro3Constants.CHANNEL_POWER), OnOffType.OFF);
665                     break;
666                 default:
667                     logger.warn("Invalid power response: '{}'", resp);
668             }
669         } else {
670             logger.warn("Invalid power response: '{}'", resp);
671         }
672     }
673
674     /**
675      * Handles the version (firmware) response. The first matching group should be the version
676      *
677      * @param m the non-null {@link Matcher} that matched the response
678      * @param resp the possibly null, possibly empty actual response
679      */
680     private void handleVersionResponse(Matcher m, String resp) {
681         if (m == null) {
682             throw new IllegalArgumentException("m (matcher) cannot be null");
683         }
684         if (m.groupCount() == 1) {
685             version = m.group(1);
686             callback.setProperty(AtlonaPro3Constants.PROPERTY_VERSION, version);
687         } else {
688             logger.warn("Invalid version response: '{}'", resp);
689         }
690     }
691
692     /**
693      * Handles the type (model) response. The first matching group should be the type.
694      *
695      * @param m the non-null {@link Matcher} that matched the response
696      * @param resp the possibly null, possibly empty actual response
697      */
698     private void handleTypeResponse(Matcher m, String resp) {
699         if (m == null) {
700             throw new IllegalArgumentException("m (matcher) cannot be null");
701         }
702         if (m.groupCount() == 1) {
703             modelType = resp;
704             callback.setProperty(AtlonaPro3Constants.PROPERTY_TYPE, modelType);
705         } else {
706             logger.warn("Invalid Type response: '{}'", resp);
707         }
708     }
709
710     /**
711      * Handles the panel lock response. The response is only on or off.
712      *
713      * @param resp the possibly null, possibly empty actual response
714      */
715     private void handlePanelLockResponse(String resp) {
716         callback.stateChanged(AtlonaPro3Utilities.createChannelID(AtlonaPro3Constants.GROUP_PRIMARY,
717                 AtlonaPro3Constants.CHANNEL_PANELLOCK), RSP_LOCK.equals(resp) ? OnOffType.ON : OnOffType.OFF);
718     }
719
720     /**
721      * Handles the port power response. The first two groups should be the port nbr and either "on" or "off"
722      *
723      * @param m the non-null {@link Matcher} that matched the response
724      * @param resp the possibly null, possibly empty actual response
725      */
726     private void handlePortPowerResponse(Matcher m, String resp) {
727         if (m == null) {
728             throw new IllegalArgumentException("m (matcher) cannot be null");
729         }
730         if (m.groupCount() == 2) {
731             try {
732                 int portNbr = Integer.parseInt(m.group(1));
733                 switch (m.group(2)) {
734                     case "on":
735                         callback.stateChanged(AtlonaPro3Utilities.createChannelID(AtlonaPro3Constants.GROUP_PORT,
736                                 portNbr, AtlonaPro3Constants.CHANNEL_PORTPOWER), OnOffType.ON);
737                         break;
738                     case "off":
739                         callback.stateChanged(AtlonaPro3Utilities.createChannelID(AtlonaPro3Constants.GROUP_PORT,
740                                 portNbr, AtlonaPro3Constants.CHANNEL_PORTPOWER), OnOffType.OFF);
741                         break;
742                     default:
743                         logger.warn("Invalid port power response: '{}'", resp);
744                 }
745             } catch (NumberFormatException e) {
746                 logger.warn("Invalid port power (can't parse number): '{}'", resp);
747             }
748         } else {
749             logger.warn("Invalid port power response: '{}'", resp);
750         }
751     }
752
753     /**
754      * Handles the port all response. Simply calls {@link #refreshAllPortStatuses()}
755      *
756      * @param resp ignored
757      */
758     private void handlePortAllResponse(String resp) {
759         refreshAllPortStatuses();
760     }
761
762     /**
763      * Handles the port output response. This matcher can have multiple groups separated by commas. Find each group and
764      * that group should have two groups within - an input port nbr and an output port number
765      *
766      * @param m the non-null {@link Matcher} that matched the response
767      * @param resp the possibly null, possibly empty actual response
768      */
769     private void handlePortOutputResponse(Matcher m, String resp) {
770         if (m == null) {
771             throw new IllegalArgumentException("m (matcher) cannot be null");
772         }
773
774         m.reset();
775         while (m.find()) {
776             try {
777                 int inPort = Integer.parseInt(m.group(1));
778                 int outPort = Integer.parseInt(m.group(2));
779
780                 callback.stateChanged(AtlonaPro3Utilities.createChannelID(AtlonaPro3Constants.GROUP_PORT, outPort,
781                         AtlonaPro3Constants.CHANNEL_PORTOUTPUT), new DecimalType(inPort));
782             } catch (NumberFormatException e) {
783                 logger.warn("Invalid port output response (can't parse number): '{}'", resp);
784             }
785         }
786     }
787
788     /**
789      * Handles the mirror response. The matcher should have two groups - an hdmi port number and an output port number.
790      *
791      * @param m the non-null {@link Matcher} that matched the response
792      * @param resp the possibly null, possibly empty actual response
793      */
794     private void handleMirrorResponse(Matcher m, String resp) {
795         if (m == null) {
796             throw new IllegalArgumentException("m (matcher) cannot be null");
797         }
798         if (m.groupCount() == 3) {
799             try {
800                 int hdmiPortNbr = Integer.parseInt(m.group(1));
801
802                 // could be "off" (if mirror off), "on"/"Out" (with 3rd group representing out)
803                 String oper = StringUtils.trimToEmpty(m.group(2)).toLowerCase();
804
805                 if (oper.equals("off")) {
806                     callback.stateChanged(AtlonaPro3Utilities.createChannelID(AtlonaPro3Constants.GROUP_MIRROR,
807                             hdmiPortNbr, AtlonaPro3Constants.CHANNEL_PORTMIRRORENABLED), OnOffType.OFF);
808                 } else {
809                     int outPortNbr = Integer.parseInt(m.group(3));
810                     callback.stateChanged(AtlonaPro3Utilities.createChannelID(AtlonaPro3Constants.GROUP_MIRROR,
811                             hdmiPortNbr, AtlonaPro3Constants.CHANNEL_PORTMIRROR), new DecimalType(outPortNbr));
812                     callback.stateChanged(AtlonaPro3Utilities.createChannelID(AtlonaPro3Constants.GROUP_MIRROR,
813                             hdmiPortNbr, AtlonaPro3Constants.CHANNEL_PORTMIRRORENABLED), OnOffType.ON);
814                 }
815             } catch (NumberFormatException e) {
816                 logger.warn("Invalid mirror response (can't parse number): '{}'", resp);
817             }
818         } else {
819             logger.warn("Invalid mirror response: '{}'", resp);
820         }
821     }
822
823     /**
824      * Handles the unmirror response. The first group should contain the hdmi port number
825      *
826      * @param m the non-null {@link Matcher} that matched the response
827      * @param resp the possibly null, possibly empty actual response
828      */
829     private void handleUnMirrorResponse(Matcher m, String resp) {
830         if (m == null) {
831             throw new IllegalArgumentException("m (matcher) cannot be null");
832         }
833         if (m.groupCount() == 1) {
834             try {
835                 int hdmiPortNbr = Integer.parseInt(m.group(1));
836                 callback.stateChanged(AtlonaPro3Utilities.createChannelID(AtlonaPro3Constants.GROUP_MIRROR, hdmiPortNbr,
837                         AtlonaPro3Constants.CHANNEL_PORTMIRROR), new DecimalType(0));
838             } catch (NumberFormatException e) {
839                 logger.warn("Invalid unmirror response (can't parse number): '{}'", resp);
840             }
841         } else {
842             logger.warn("Invalid unmirror response: '{}'", resp);
843         }
844     }
845
846     /**
847      * Handles the volume response. The first two group should be the audio port number and the level
848      *
849      * @param m the non-null {@link Matcher} that matched the response
850      * @param resp the possibly null, possibly empty actual response
851      */
852     private void handleVolumeResponse(Matcher m, String resp) {
853         if (m == null) {
854             throw new IllegalArgumentException("m (matcher) cannot be null");
855         }
856         if (m.groupCount() == 2) {
857             try {
858                 int portNbr = Integer.parseInt(m.group(1));
859                 double level = Double.parseDouble(m.group(2));
860                 callback.stateChanged(AtlonaPro3Utilities.createChannelID(AtlonaPro3Constants.GROUP_VOLUME, portNbr,
861                         AtlonaPro3Constants.CHANNEL_VOLUME), new DecimalType(level));
862             } catch (NumberFormatException e) {
863                 logger.warn("Invalid volume response (can't parse number): '{}'", resp);
864             }
865         } else {
866             logger.warn("Invalid volume response: '{}'", resp);
867         }
868     }
869
870     /**
871      * Handles the volume mute response. The first two group should be the audio port number and either "on" or "off
872      *
873      * @param m the non-null {@link Matcher} that matched the response
874      * @param resp the possibly null, possibly empty actual response
875      */
876     private void handleVolumeMuteResponse(Matcher m, String resp) {
877         if (m == null) {
878             throw new IllegalArgumentException("m (matcher) cannot be null");
879         }
880         if (m.groupCount() == 2) {
881             try {
882                 int portNbr = Integer.parseInt(m.group(1));
883                 switch (m.group(2)) {
884                     case "on":
885                         callback.stateChanged(AtlonaPro3Utilities.createChannelID(AtlonaPro3Constants.GROUP_VOLUME,
886                                 portNbr, AtlonaPro3Constants.CHANNEL_VOLUME_MUTE), OnOffType.ON);
887                         break;
888                     case "off":
889                         callback.stateChanged(AtlonaPro3Utilities.createChannelID(AtlonaPro3Constants.GROUP_VOLUME,
890                                 portNbr, AtlonaPro3Constants.CHANNEL_VOLUME_MUTE), OnOffType.OFF);
891                         break;
892                     default:
893                         logger.warn("Invalid volume mute response: '{}'", resp);
894                 }
895             } catch (NumberFormatException e) {
896                 logger.warn("Invalid volume mute (can't parse number): '{}'", resp);
897             }
898         } else {
899             logger.warn("Invalid volume mute response: '{}'", resp);
900         }
901     }
902
903     /**
904      * Handles the IR Response. The response is either on or off
905      *
906      * @param resp the possibly null, possibly empty actual response
907      */
908     private void handleIrLockResponse(String resp) {
909         callback.stateChanged(AtlonaPro3Utilities.createChannelID(AtlonaPro3Constants.GROUP_PRIMARY,
910                 AtlonaPro3Constants.CHANNEL_IRENABLE), RSP_IRON.equals(resp) ? OnOffType.ON : OnOffType.OFF);
911     }
912
913     /**
914      * Handles the Save IO Response. Should have one group specifying the preset number
915      *
916      * @param m the non-null {@link Matcher} that matched the response
917      * @param resp the possibly null, possibly empty actual response
918      */
919     private void handleSaveIoResponse(Matcher m, String resp) {
920         // nothing to handle
921     }
922
923     /**
924      * Handles the Recall IO Response. Should have one group specifying the preset number. After updating the Recall
925      * State, we refresh all the ports via {@link #refreshAllPortStatuses()}.
926      *
927      * @param m the non-null {@link Matcher} that matched the response
928      * @param resp the possibly null, possibly empty actual response
929      */
930     private void handleRecallIoResponse(Matcher m, String resp) {
931         refreshAllPortStatuses();
932     }
933
934     /**
935      * Handles the Clear IO Response. Should have one group specifying the preset number.
936      *
937      * @param m the non-null {@link Matcher} that matched the response
938      * @param resp the possibly null, possibly empty actual response
939      */
940     private void handleClearIoResponse(Matcher m, String resp) {
941         // nothing to handle
942     }
943
944     /**
945      * Handles the broadcast Response. Should have one group specifying the status.
946      *
947      * @param m the non-null {@link Matcher} that matched the response
948      * @param resp the possibly null, possibly empty actual response
949      */
950     private void handleBroadcastResponse(Matcher m, String resp) {
951         // nothing to handle
952     }
953
954     /**
955      * Handles the matrix reset response. The matrix will go offline immediately on a reset.
956      *
957      * @param resp the possibly null, possibly empty actual response
958      */
959     private void handleMatrixResetResponse(String resp) {
960         if (RSP_MATRIX_RESET.equals(resp)) {
961             callback.statusChanged(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
962                     "System is rebooting due to matrix reset");
963         }
964     }
965
966     /**
967      * Handles a command failure - we simply log the response as an error
968      *
969      * @param resp the possibly null, possibly empty actual response
970      */
971     private void handleCommandFailure(String resp) {
972         logger.info("{}", resp);
973     }
974
975     /**
976      * This callback is our normal response callback. Should be set into the {@link SocketSession} after the login
977      * process to handle normal responses.
978      *
979      * @author Tim Roberts
980      *
981      */
982     private class NormalResponseCallback implements SocketSessionListener {
983
984         @Override
985         public void responseReceived(String response) {
986             if (response == null || response == "") {
987                 return;
988             }
989
990             if (RSP_PING.equals(response)) {
991                 // ignore
992                 return;
993             }
994
995             Matcher m;
996
997             m = portStatusPattern.matcher(response);
998             if (m.find()) {
999                 handlePortOutputResponse(m, response);
1000                 return;
1001             }
1002
1003             m = powerStatusPattern.matcher(response);
1004             if (m.matches()) {
1005                 handlePowerResponse(m, response);
1006                 return;
1007             }
1008
1009             m = versionPattern.matcher(response);
1010             if (m.matches()) {
1011                 handleVersionResponse(m, response);
1012                 return;
1013             }
1014
1015             m = typePattern.matcher(response);
1016             if (m.matches()) {
1017                 handleTypeResponse(m, response);
1018                 return;
1019             }
1020
1021             m = portPowerPattern.matcher(response);
1022             if (m.matches()) {
1023                 handlePortPowerResponse(m, response);
1024                 return;
1025             }
1026
1027             m = volumePattern.matcher(response);
1028             if (m.matches()) {
1029                 handleVolumeResponse(m, response);
1030                 return;
1031             }
1032
1033             m = volumeMutePattern.matcher(response);
1034             if (m.matches()) {
1035                 handleVolumeMuteResponse(m, response);
1036                 return;
1037             }
1038
1039             m = portAllPattern.matcher(response);
1040             if (m.matches()) {
1041                 handlePortAllResponse(response);
1042                 return;
1043             }
1044
1045             m = portMirrorPattern.matcher(response);
1046             if (m.matches()) {
1047                 handleMirrorResponse(m, response);
1048                 return;
1049             }
1050
1051             m = portUnmirrorPattern.matcher(response);
1052             if (m.matches()) {
1053                 handleUnMirrorResponse(m, response);
1054                 return;
1055             }
1056
1057             m = saveIoPattern.matcher(response);
1058             if (m.matches()) {
1059                 handleSaveIoResponse(m, response);
1060                 return;
1061             }
1062
1063             m = recallIoPattern.matcher(response);
1064             if (m.matches()) {
1065                 handleRecallIoResponse(m, response);
1066                 return;
1067             }
1068
1069             m = clearIoPattern.matcher(response);
1070             if (m.matches()) {
1071                 handleClearIoResponse(m, response);
1072                 return;
1073             }
1074
1075             m = broadCastPattern.matcher(response);
1076             if (m.matches()) {
1077                 handleBroadcastResponse(m, response);
1078                 return;
1079             }
1080
1081             if (RSP_IRON.equals(response) || RSP_IROFF.equals(response)) {
1082                 handleIrLockResponse(response);
1083                 return;
1084             }
1085
1086             if (RSP_ALL.equals(response)) {
1087                 handlePortAllResponse(response);
1088                 return;
1089             }
1090
1091             if (RSP_LOCK.equals(response) || RSP_UNLOCK.equals(response)) {
1092                 handlePanelLockResponse(response);
1093                 return;
1094             }
1095
1096             if (RSP_MATRIX_RESET.equals(response)) {
1097                 handleMatrixResetResponse(response);
1098                 return;
1099             }
1100
1101             if (response.startsWith(RSP_FAILED)) {
1102                 handleCommandFailure(response);
1103                 return;
1104             }
1105
1106             logger.info("Unhandled response: {}", response);
1107         }
1108
1109         @Override
1110         public void responseException(Exception e) {
1111             callback.statusChanged(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
1112                     "Exception occurred reading from Atlona: " + e);
1113         }
1114     }
1115
1116     /**
1117      * Special callback used during the login process to not dispatch the responses to this class but rather give them
1118      * back at each call to {@link NoDispatchingCallback#getResponse()}
1119      *
1120      * @author Tim Roberts
1121      *
1122      */
1123     private class NoDispatchingCallback implements SocketSessionListener {
1124
1125         /**
1126          * Cache of responses that have occurred
1127          */
1128         private BlockingQueue<Object> responses = new ArrayBlockingQueue<>(5);
1129
1130         /**
1131          * Will return the next response from {@link #responses}. If the response is an exception, that exception will
1132          * be thrown instead.
1133          *
1134          * @return a non-null, possibly empty response
1135          * @throws Exception an exception if one occurred during reading
1136          */
1137         String getResponse() throws Exception {
1138             final Object lastResponse = responses.poll(5, TimeUnit.SECONDS);
1139             if (lastResponse instanceof String) {
1140                 return (String) lastResponse;
1141             } else if (lastResponse instanceof Exception) {
1142                 throw (Exception) lastResponse;
1143             } else if (lastResponse == null) {
1144                 throw new Exception("Didn't receive response in time");
1145             } else {
1146                 return lastResponse.toString();
1147             }
1148         }
1149
1150         @Override
1151         public void responseReceived(String response) {
1152             try {
1153                 responses.put(response);
1154             } catch (InterruptedException e) {
1155             }
1156         }
1157
1158         @Override
1159         public void responseException(Exception e) {
1160             try {
1161                 responses.put(e);
1162             } catch (InterruptedException e1) {
1163             }
1164         }
1165     }
1166 }