2 * Copyright (c) 2010-2020 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
7 * This program and the accompanying materials are made available under the
8 * terms of the Eclipse Public License 2.0 which is available at
9 * http://www.eclipse.org/legal/epl-2.0
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.atlona.internal.pro3;
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;
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;
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]).
39 * @author Tim Roberts - Initial contribution
40 * @author Michael Lobstein - Add support for AT-PRO3HD66M
42 class AtlonaPro3PortocolHandler {
43 private final Logger logger = LoggerFactory.getLogger(AtlonaPro3PortocolHandler.class);
46 * The {@link SocketSession} used by this protocol handler
48 private final SocketSession session;
51 * The {@link AtlonaPro3Config} configuration used by this handler
53 private final AtlonaPro3Config config;
56 * The {@link AtlonaPro3Capabilities} of the PRO3 model
58 private final AtlonaPro3Capabilities capabilities;
61 * The {@link AtlonaPro3Handler} to call back to update status and state
63 private final AtlonaHandlerCallback callback;
66 * The model type identified by the switch. We save it for faster refreshes since it will not change
68 private String modelType;
71 * The version (firmware) identified by the switch. We save it for faster refreshes since it will not change between
74 private String version;
77 * A special (invalid) command used internally by this handler to identify whether the switch wants a login or not
78 * (see {@link #login()})
80 private static final String NOTVALID_USER_OR_CMD = "notvalid$934%912";
82 // ------------------------------------------------------------------------------------------------
83 // The following are the various command formats specified by the Atlona protocol
84 private static final String CMD_POWERON = "PWON";
85 private static final String CMD_POWEROFF = "PWOFF";
86 private static final String CMD_POWER_STATUS = "PWSTA";
87 private static final String CMD_VERSION = "Version";
88 private static final String CMD_TYPE = "Type";
89 private static final String CMD_PANELLOCK = "Lock";
90 private static final String CMD_PANELUNLOCK = "Unlock";
91 private static final String CMD_PORT_RESETALL = "All#";
92 private static final String CMD_PORT_POWER_FORMAT = "x%d$ %s";
93 private static final String CMD_PORT_ALL_FORMAT = "x%dAll";
94 private static final String CMD_PORT_SWITCH_FORMAT = "x%dAVx%d";
95 private static final String CMD_PORT_MIRROR_FORMAT = "MirrorHdmi%d Out%d";
96 private static final String CMD_PORT_MIRROR_STATUS_FORMAT = "MirrorHdmi%d sta";
97 private static final String CMD_PORT_UNMIRROR_FORMAT = "UnMirror%d";
98 private static final String CMD_VOLUME_FORMAT = "VOUT%d %s";
99 private static final String CMD_VOLUME_MUTE_FORMAT = "VOUTMute%d %s";
100 private static final String CMD_IROFF = "IROFF";
101 private static final String CMD_IRON = "IRON";
102 private static final String CMD_PORT_STATUS = "Status";
103 private static final String CMD_PORT_STATUS_FORMAT = "Statusx%d";
104 private static final String CMD_SAVEIO_FORMAT = "Save%d";
105 private static final String CMD_RECALLIO_FORMAT = "Recall%d";
106 private static final String CMD_CLEARIO_FORMAT = "Clear%d";
107 private static final String CMD_MATRIX_RESET = "Mreset";
108 private static final String CMD_BROADCAST_ON = "Broadcast on";
110 // ------------------------------------------------------------------------------------------------
111 // The following are the various responses specified by the Atlona protocol
112 private static final String RSP_FAILED = "Command FAILED:";
114 private static final String RSP_LOGIN = "Login";
115 private static final String RSP_PASSWORD = "Password";
117 private final Pattern powerStatusPattern = Pattern.compile("PW(\\w+)");
118 private final Pattern versionPattern = Pattern.compile("Firmware (.*)");
119 private final Pattern typePattern = Pattern.compile("AT-UHD-PRO3-(\\d+)M");
120 private static final String RSP_ALL = "All#";
121 private static final String RSP_LOCK = "Lock";
122 private static final String RSP_UNLOCK = "Unlock";
123 private final Pattern portStatusPattern = Pattern.compile("x(\\d+)AVx(\\d+),?+");
124 private final Pattern portPowerPattern = Pattern.compile("x(\\d+)\\$ (\\w+)");
125 private final Pattern portAllPattern = Pattern.compile("x(\\d+)All");
126 private final Pattern portMirrorPattern = Pattern.compile("MirrorHdmi(\\d+) (\\p{Alpha}+)(\\d*)");
127 private final Pattern portUnmirrorPattern = Pattern.compile("UnMirror(\\d+)");
128 private final Pattern volumePattern = Pattern.compile("VOUT(\\d+) (-?\\d+)");
129 private final Pattern volumeMutePattern = Pattern.compile("VOUTMute(\\d+) (\\w+)");
130 private static final String RSP_IROFF = "IROFF";
131 private static final String RSP_IRON = "IRON";
132 private final Pattern saveIoPattern = Pattern.compile("Save(\\d+)");
133 private final Pattern recallIoPattern = Pattern.compile("Recall(\\d+)");
134 private final Pattern clearIoPattern = Pattern.compile("Clear(\\d+)");
135 private final Pattern broadCastPattern = Pattern.compile("Broadcast (\\w+)");
136 private static final String RSP_MATRIX_RESET = "Mreset";
138 // Constants added to support the HD models
139 private static final String RSP_WELCOME = "Welcome to TELNET";
140 private static final String RSP_LOGIN_PLEASE = "Login Please";
141 private static final String RSP_USERNAME = "Username";
142 private static final String RSP_TRY_AGAIN = "Please Try Again";
143 private final Pattern versionHdPattern = Pattern.compile("V(.*)");
144 private final Pattern typeHdPattern = Pattern.compile("AT-PRO3HD(\\d+)M");
146 // ------------------------------------------------------------------------------------------------
147 // The following isn't part of the atlona protocol and is generated by us
148 private static final String CMD_PING = "ping";
149 private static final String RSP_PING = "Command FAILED: (ping)";
152 * Constructs the protocol handler from given parameters
154 * @param session a non-null {@link SocketSession} (may be connected or disconnected)
155 * @param config a non-null {@link AtlonaPro3Config}
156 * @param capabilities a non-null {@link AtlonaPro3Capabilities}
157 * @param callback a non-null {@link AtlonaHandlerCallback} to update state and status
159 AtlonaPro3PortocolHandler(SocketSession session, AtlonaPro3Config config, AtlonaPro3Capabilities capabilities,
160 AtlonaHandlerCallback callback) {
161 if (session == null) {
162 throw new IllegalArgumentException("session cannot be null");
165 if (config == null) {
166 throw new IllegalArgumentException("config cannot be null");
169 if (capabilities == null) {
170 throw new IllegalArgumentException("capabilities cannot be null");
173 if (callback == null) {
174 throw new IllegalArgumentException("callback cannot be null");
177 this.session = session;
178 this.config = config;
179 this.capabilities = capabilities;
180 this.callback = callback;
184 * Attempts to log into the switch when prompted by the switch. Please see code comments on the exact protocol for
187 * @return a null if logged in successfully (or if switch didn't require login). Non-null if an exception occurred.
188 * @throws IOException an IO exception occurred during login
190 String loginUHD() throws Exception {
191 logger.debug("Logging into atlona switch");
192 // Void to make sure we retrieve them
196 NoDispatchingCallback callback = new NoDispatchingCallback();
197 session.addListener(callback);
199 // Burn the initial (empty) return
202 response = callback.getResponse();
203 if (!response.equals("")) {
204 logger.debug("Altona protocol violation - didn't start with an inital empty response: '{}'", response);
206 } catch (Exception e) {
207 // ignore - may not having given us an initial ""
210 // At this point - we are not sure if it's:
211 // 1) waiting for a command input
212 // or 2) has sent a "Login: " prompt
213 // By sending a string that doesn't exist as a command or user
214 // we can tell which by the response to the invalid command
215 session.sendCommand(NOTVALID_USER_OR_CMD);
217 // Command failed - Altona not configured with IPLogin - return success
218 response = callback.getResponse();
219 if (response.startsWith(RSP_FAILED)) {
220 logger.debug("Altona didn't require a login");
225 // We should have been presented with a new "\r\nLogin: "
226 response = callback.getResponse();
227 if (!response.equals("")) {
228 logger.debug("Altona protocol violation - didn't start with an inital empty response: '{}'", response);
231 // Get the new "Login: " prompt response
232 response = callback.getResponse();
233 if (response.equals(RSP_LOGIN)) {
234 if (config.getUserName() == null || config.getUserName().trim().length() == 0) {
235 return "Atlona PRO3 has enabled Telnet/IP Login but no username was provided in the configuration.";
238 // Send the username and wait for a ": " response
239 session.sendCommand(config.getUserName());
241 return "Altona protocol violation - wasn't initially a command failure or login prompt: " + response;
244 // We should have gotten the password response
245 response = callback.getResponse();
247 // Burn the empty response if we got one (
248 if (response.equals("")) {
249 response = callback.getResponse();
251 if (!response.equals(RSP_PASSWORD)) {
252 // If we got another login response, username wasn't valid
253 if (response.equals(RSP_LOGIN)) {
254 return "Username " + config.getUserName() + " is not a valid user on the atlona";
256 return "Altona protocol violation - invalid response to a login: " + response;
259 // Make sure we have a password
260 if (config.getPassword() == null || config.getPassword().trim().length() == 0) {
261 return "Atlona PRO3 has enabled Telnet/IP Login but no password was provided in the configuration.";
265 session.sendCommand(config.getPassword());
266 response = callback.getResponse();
268 // At this point, we don't know if we received a
269 // 1) "\r\n" and waiting for a command
270 // or 2) "\r\nLogin: " if the password is invalid
271 // Send an invalid command to see if we get the failed command response
273 // First make sure we had an empty response (the "\r\n" part)
274 if (!response.equals("")) {
275 logger.debug("Altona protocol violation - not an empty response after password: '{}'", response);
278 // Now send an invalid command
279 session.sendCommand(NOTVALID_USER_OR_CMD);
281 // If we get an invalid command response - we are logged in
282 response = callback.getResponse();
283 if (response.startsWith(RSP_FAILED)) {
288 // Nope - password invalid
289 return "Password was invalid - please check your atlona setup";
293 * Attempts to log into the older HD model switches using a slightly different protocol
295 * @return a null if logged in successfully (or if switch didn't require login). Non-null if an exception occurred.
296 * @throws IOException an IO exception occurred during login
298 String loginHD() throws Exception {
299 logger.debug("Logging into atlona switch");
300 // Void to make sure we retrieve them
304 NoDispatchingCallback callback = new NoDispatchingCallback();
305 session.addListener(callback);
307 // Burn the initial (empty) return
310 response = callback.getResponse();
311 if (!response.equals("")) {
312 logger.debug("Altona protocol violation - didn't start with an inital empty response: '{}'", response);
314 } catch (Exception e) {
315 // ignore - may not having given us an initial ""
318 response = callback.getResponse();
319 if (response.startsWith(RSP_WELCOME)) {
320 logger.debug("Altona AT-PRO3HD66M didn't require a login");
324 if (!response.startsWith(RSP_LOGIN_PLEASE)) {
325 logger.debug("Altona protocol violation - didn't start with login prompt '{}'", response);
327 // Since we were not logged in automatically, a user name is required from the configuration
328 if (config.getUserName() == null || config.getUserName().trim().length() == 0) {
329 return "Atlona PRO3 has enabled Telnet/IP Login but no username was provided in the configuration.";
332 // Make sure we have a password too
333 if (config.getPassword() == null || config.getPassword().trim().length() == 0) {
334 return "Atlona PRO3 has enabled Telnet/IP Login but no password was provided in the configuration.";
337 // Check for an empty response after the login prompt (the "\r\n" part)
338 response = callback.getResponse();
339 if (!response.equals("")) {
340 logger.debug("Altona protocol violation - not an empty response after password: '{}'", response);
343 // Send the username and wait for a ": " response
344 session.sendCommand(config.getUserName());
346 // We should have gotten the username response
347 response = callback.getResponse();
348 if (!response.startsWith(RSP_USERNAME)) {
349 logger.debug("Altona protocol violation - invalid response to username: '{}'", response);
354 session.sendCommand(config.getPassword());
355 response = callback.getResponse();
356 } catch (Exception e) {
357 return "Password was invalid - please check your atlona setup";
360 if (response.startsWith(RSP_TRY_AGAIN)) {
361 return "Username " + config.getUserName() + " is not a valid user on the atlona";
364 if (response.startsWith(RSP_PASSWORD)) {
365 // After the correct password is sent, several empty responses are sent before the welcome message
366 for (int i = 0; i < 8; i++) {
367 response = callback.getResponse();
369 // If we get a welcome message, login was successful
370 if (response.startsWith(RSP_WELCOME)) {
377 return "Authentication failed - please check your atlona setup";
381 * Post successful login stuff - mark us online and refresh from the switch
383 private void postLogin() {
384 logger.debug("Atlona switch now connected");
385 session.clearListeners();
386 session.addListener(new NormalResponseCallback());
387 callback.statusChanged(ThingStatus.ONLINE, ThingStatusDetail.NONE, null);
389 if (capabilities.isUHDModel()) {
390 // Set broadcast to on to receive notifications when
391 // routing changes (via the webpage, or presets or IR, etc)
392 sendCommand(CMD_BROADCAST_ON);
395 // setup the most likely state of these switches (there is no protocol to get them)
400 * Returns the callback being used by this handler
402 * @return a non-null {@link AtlonaHandlerCallback}
404 AtlonaHandlerCallback getCallback() {
409 * Pings the server with an (invalid) ping command to keep the connection alive
412 if (capabilities.isUHDModel()) {
413 sendCommand(CMD_PING);
415 // the HD model does not reflect the invalid command string back in the response for us to match later
416 sendCommand(CMD_VERSION);
421 * Refreshes the state from the switch itself. This will retrieve all the state (that we can get) from the switch.
424 logger.debug("Refreshing matrix state");
425 if (version == null) {
428 callback.setProperty(AtlonaPro3Constants.PROPERTY_VERSION, version);
431 if (modelType == null) {
434 callback.setProperty(AtlonaPro3Constants.PROPERTY_TYPE, modelType);
438 if (capabilities.isUHDModel()) {
439 refreshAllPortStatuses();
442 final int nbrPowerPorts = capabilities.getNbrPowerPorts();
443 for (int x = 1; x <= nbrPowerPorts; x++) {
447 final int nbrAudioPorts = capabilities.getNbrAudioPorts();
448 for (int x = 1; x <= nbrAudioPorts; x++) {
449 refreshVolumeStatus(x);
450 refreshVolumeMute(x);
453 for (int x : capabilities.getHdmiPorts()) {
454 refreshPortStatus(x);
459 * Sets the power to the switch
461 * @param on true if on, false otherwise
463 void setPower(boolean on) {
464 sendCommand(on ? CMD_POWERON : CMD_POWEROFF);
468 * Queries the switch about it's power state
470 void refreshPower() {
471 sendCommand(CMD_POWER_STATUS);
475 * Queries the switch about it's version (firmware)
477 void refreshVersion() {
478 sendCommand(CMD_VERSION);
482 * Queries the switch about it's type (model)
485 sendCommand(CMD_TYPE);
489 * Sets whether the front panel is locked or not
491 * @param locked true if locked, false otherwise
493 void setPanelLock(boolean locked) {
494 sendCommand(locked ? CMD_PANELLOCK : CMD_PANELUNLOCK);
498 * Resets all ports back to their default state.
500 void resetAllPorts() {
501 sendCommand(CMD_PORT_RESETALL);
505 * Sets whether the specified port is powered (i.e. outputing).
507 * @param portNbr a greater than zero port number
508 * @param on true if powered.
510 void setPortPower(int portNbr, boolean on) {
512 throw new IllegalArgumentException("portNbr must be greater than 0");
514 sendCommand(String.format(CMD_PORT_POWER_FORMAT, portNbr, on ? "on" : "off"));
518 * Refreshes whether the specified port is powered (i.e. outputing).
520 * @param portNbr a greater than zero port number
522 void refreshPortPower(int portNbr) {
524 throw new IllegalArgumentException("portNbr must be greater than 0");
526 sendCommand(String.format(CMD_PORT_POWER_FORMAT, portNbr, "sta"));
530 * Sets all the output ports to the specified input port.
532 * @param portNbr a greater than zero port number
534 void setPortAll(int portNbr) {
536 throw new IllegalArgumentException("portNbr must be greater than 0");
538 sendCommand(String.format(CMD_PORT_ALL_FORMAT, portNbr));
542 * Sets the input port number to the specified output port number.
544 * @param inPortNbr a greater than zero port number
545 * @param outPortNbr a greater than zero port number
547 void setPortSwitch(int inPortNbr, int outPortNbr) {
548 if (inPortNbr <= 0) {
549 throw new IllegalArgumentException("inPortNbr must be greater than 0");
551 if (outPortNbr <= 0) {
552 throw new IllegalArgumentException("outPortNbr must be greater than 0");
554 sendCommand(String.format(CMD_PORT_SWITCH_FORMAT, inPortNbr, outPortNbr));
558 * Sets the hdmi port number to mirror the specified output port number.
560 * @param hdmiPortNbr a greater than zero port number
561 * @param outPortNbr a greater than zero port number
563 void setPortMirror(int hdmiPortNbr, int outPortNbr) {
564 if (hdmiPortNbr <= 0) {
565 throw new IllegalArgumentException("hdmiPortNbr must be greater than 0");
567 if (outPortNbr <= 0) {
568 throw new IllegalArgumentException("outPortNbr must be greater than 0");
571 if (capabilities.getHdmiPorts().contains(hdmiPortNbr)) {
572 sendCommand(String.format(CMD_PORT_MIRROR_FORMAT, hdmiPortNbr, outPortNbr));
574 logger.info("Trying to set port mirroring on a non-hdmi port: {}", hdmiPortNbr);
579 * Disabled mirroring on the specified hdmi port number.
581 * @param hdmiPortNbr a greater than zero port number
582 * @param outPortNbr a greater than zero port number
584 void removePortMirror(int hdmiPortNbr) {
585 if (hdmiPortNbr <= 0) {
586 throw new IllegalArgumentException("hdmiPortNbr must be greater than 0");
589 if (capabilities.getHdmiPorts().contains(hdmiPortNbr)) {
590 sendCommand(String.format(CMD_PORT_UNMIRROR_FORMAT, hdmiPortNbr));
592 logger.info("Trying to remove port mirroring on a non-hdmi port: {}", hdmiPortNbr);
597 * Sets the volume level on the specified audio port.
599 * @param portNbr a greater than zero port number
600 * @param level a volume level in decibels (must range from -79 to +15)
602 void setVolume(int portNbr, double level) {
604 throw new IllegalArgumentException("portNbr must be greater than 0");
606 if (level < -79 || level > 15) {
607 throw new IllegalArgumentException("level must be between -79 to +15");
609 sendCommand(String.format(CMD_VOLUME_FORMAT, portNbr, level));
613 * Refreshes the volume level for the given audio port.
615 * @param portNbr a greater than zero port number
617 void refreshVolumeStatus(int portNbr) {
619 throw new IllegalArgumentException("portNbr must be greater than 0");
621 sendCommand(String.format(CMD_VOLUME_FORMAT, portNbr, "sta"));
625 * Refreshes the specified hdmi port's mirroring status
627 * @param hdmiPortNbr a greater than zero hdmi port number
629 void refreshPortMirror(int hdmiPortNbr) {
630 if (hdmiPortNbr <= 0) {
631 throw new IllegalArgumentException("hdmiPortNbr must be greater than 0");
633 sendCommand(String.format(CMD_PORT_MIRROR_STATUS_FORMAT, hdmiPortNbr));
637 * Mutes/Unmutes the specified audio port.
639 * @param portNbr a greater than zero port number
640 * @param mute true to mute, false to unmute
642 void setVolumeMute(int portNbr, boolean mute) {
644 throw new IllegalArgumentException("portNbr must be greater than 0");
646 sendCommand(String.format(CMD_VOLUME_MUTE_FORMAT, portNbr, mute ? "on" : "off"));
650 * Refreshes the volume mute for the given audio port.
652 * @param portNbr a greater than zero port number
654 void refreshVolumeMute(int portNbr) {
656 throw new IllegalArgumentException("portNbr must be greater than 0");
658 sendCommand(String.format(CMD_VOLUME_MUTE_FORMAT, portNbr, "sta"));
662 * Turn on/off the front panel IR.
664 * @param on true for on, false otherwise
666 void setIrOn(boolean on) {
667 sendCommand(on ? CMD_IRON : CMD_IROFF);
671 * Refreshes the input port setting on the specified output port.
673 * @param portNbr a greater than zero port number
675 void refreshPortStatus(int portNbr) {
677 throw new IllegalArgumentException("portNbr must be greater than 0");
679 sendCommand(String.format(CMD_PORT_STATUS_FORMAT, portNbr));
683 * Refreshes all of the input port settings for all of the output ports.
685 private void refreshAllPortStatuses() {
686 sendCommand(CMD_PORT_STATUS);
690 * Saves the current Input/Output scheme to the specified preset number.
692 * @param presetNbr a greater than 0 preset number
694 void saveIoSettings(int presetNbr) {
695 if (presetNbr <= 0) {
696 throw new IllegalArgumentException("presetNbr must be greater than 0");
698 sendCommand(String.format(CMD_SAVEIO_FORMAT, presetNbr));
702 * Recalls the Input/Output scheme for the specified preset number.
704 * @param presetNbr a greater than 0 preset number
706 void recallIoSettings(int presetNbr) {
707 if (presetNbr <= 0) {
708 throw new IllegalArgumentException("presetNbr must be greater than 0");
710 sendCommand(String.format(CMD_RECALLIO_FORMAT, presetNbr));
714 * Clears the Input/Output scheme for the specified preset number.
716 * @param presetNbr a greater than 0 preset number
718 void clearIoSettings(int presetNbr) {
719 if (presetNbr <= 0) {
720 throw new IllegalArgumentException("presetNbr must be greater than 0");
722 sendCommand(String.format(CMD_CLEARIO_FORMAT, presetNbr));
726 * Resets the matrix back to defaults.
729 sendCommand(CMD_MATRIX_RESET);
733 * Sends the command and puts the thing into {@link ThingStatus#OFFLINE} if an IOException occurs
735 * @param command a non-null, non-empty command to send
737 private void sendCommand(String command) {
738 if (command == null) {
739 throw new IllegalArgumentException("command cannot be null");
741 if (command.trim().length() == 0) {
742 throw new IllegalArgumentException("command cannot be empty");
745 session.sendCommand(command);
746 } catch (IOException e) {
747 callback.statusChanged(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
748 "Exception occurred sending to Atlona: " + e);
753 * Handles the switch power response. The first matching group should be "on" or "off"
755 * @param m the non-null {@link Matcher} that matched the response
756 * @param resp the possibly null, possibly empty actual response
758 private void handlePowerResponse(Matcher m, String resp) {
760 throw new IllegalArgumentException("m (matcher) cannot be null");
762 if (m.groupCount() == 1) {
763 switch (m.group(1)) {
765 callback.stateChanged(AtlonaPro3Utilities.createChannelID(AtlonaPro3Constants.GROUP_PRIMARY,
766 AtlonaPro3Constants.CHANNEL_POWER), OnOffType.ON);
769 callback.stateChanged(AtlonaPro3Utilities.createChannelID(AtlonaPro3Constants.GROUP_PRIMARY,
770 AtlonaPro3Constants.CHANNEL_POWER), OnOffType.OFF);
773 logger.warn("Invalid power response: '{}'", resp);
776 logger.warn("Invalid power response: '{}'", resp);
781 * Handles the version (firmware) response. The first matching group should be the version
783 * @param m the non-null {@link Matcher} that matched the response
784 * @param resp the possibly null, possibly empty actual response
786 private void handleVersionResponse(Matcher m, String resp) {
788 throw new IllegalArgumentException("m (matcher) cannot be null");
790 if (m.groupCount() == 1) {
791 version = m.group(1);
792 callback.setProperty(AtlonaPro3Constants.PROPERTY_VERSION, version);
794 logger.warn("Invalid version response: '{}'", resp);
799 * Handles the type (model) response. The first matching group should be the type.
801 * @param m the non-null {@link Matcher} that matched the response
802 * @param resp the possibly null, possibly empty actual response
804 private void handleTypeResponse(Matcher m, String resp) {
806 throw new IllegalArgumentException("m (matcher) cannot be null");
808 if (m.groupCount() == 1) {
810 callback.setProperty(AtlonaPro3Constants.PROPERTY_TYPE, modelType);
812 logger.warn("Invalid Type response: '{}'", resp);
817 * Handles the panel lock response. The response is only on or off.
819 * @param resp the possibly null, possibly empty actual response
821 private void handlePanelLockResponse(String resp) {
822 callback.stateChanged(AtlonaPro3Utilities.createChannelID(AtlonaPro3Constants.GROUP_PRIMARY,
823 AtlonaPro3Constants.CHANNEL_PANELLOCK), RSP_LOCK.equals(resp) ? OnOffType.ON : OnOffType.OFF);
827 * Handles the port power response. The first two groups should be the port nbr and either "on" or "off"
829 * @param m the non-null {@link Matcher} that matched the response
830 * @param resp the possibly null, possibly empty actual response
832 private void handlePortPowerResponse(Matcher m, String resp) {
834 throw new IllegalArgumentException("m (matcher) cannot be null");
836 if (m.groupCount() == 2) {
838 int portNbr = Integer.parseInt(m.group(1));
839 switch (m.group(2)) {
841 callback.stateChanged(AtlonaPro3Utilities.createChannelID(AtlonaPro3Constants.GROUP_PORT,
842 portNbr, AtlonaPro3Constants.CHANNEL_PORTPOWER), OnOffType.ON);
845 callback.stateChanged(AtlonaPro3Utilities.createChannelID(AtlonaPro3Constants.GROUP_PORT,
846 portNbr, AtlonaPro3Constants.CHANNEL_PORTPOWER), OnOffType.OFF);
849 logger.warn("Invalid port power response: '{}'", resp);
851 } catch (NumberFormatException e) {
852 logger.warn("Invalid port power (can't parse number): '{}'", resp);
855 logger.warn("Invalid port power response: '{}'", resp);
860 * Handles the port all response. Simply calls {@link #refreshAllPortStatuses()}
862 * @param resp ignored
864 private void handlePortAllResponse(String resp) {
865 refreshAllPortStatuses();
869 * Handles the port output response. This matcher can have multiple groups separated by commas. Find each group and
870 * that group should have two groups within - an input port nbr and an output port number
872 * @param m the non-null {@link Matcher} that matched the response
873 * @param resp the possibly null, possibly empty actual response
875 private void handlePortOutputResponse(Matcher m, String resp) {
877 throw new IllegalArgumentException("m (matcher) cannot be null");
883 int inPort = Integer.parseInt(m.group(1));
884 int outPort = Integer.parseInt(m.group(2));
886 callback.stateChanged(AtlonaPro3Utilities.createChannelID(AtlonaPro3Constants.GROUP_PORT, outPort,
887 AtlonaPro3Constants.CHANNEL_PORTOUTPUT), new DecimalType(inPort));
888 } catch (NumberFormatException e) {
889 logger.warn("Invalid port output response (can't parse number): '{}'", resp);
895 * Handles the mirror response. The matcher should have two groups - an hdmi port number and an output port number.
897 * @param m the non-null {@link Matcher} that matched the response
898 * @param resp the possibly null, possibly empty actual response
900 private void handleMirrorResponse(Matcher m, String resp) {
902 throw new IllegalArgumentException("m (matcher) cannot be null");
904 if (m.groupCount() == 3) {
906 int hdmiPortNbr = Integer.parseInt(m.group(1));
908 // could be "off" (if mirror off), "on"/"Out" (with 3rd group representing out)
909 String oper = StringUtils.trimToEmpty(m.group(2)).toLowerCase();
911 if (oper.equals("off")) {
912 callback.stateChanged(AtlonaPro3Utilities.createChannelID(AtlonaPro3Constants.GROUP_MIRROR,
913 hdmiPortNbr, AtlonaPro3Constants.CHANNEL_PORTMIRRORENABLED), OnOffType.OFF);
915 int outPortNbr = Integer.parseInt(m.group(3));
916 callback.stateChanged(AtlonaPro3Utilities.createChannelID(AtlonaPro3Constants.GROUP_MIRROR,
917 hdmiPortNbr, AtlonaPro3Constants.CHANNEL_PORTMIRROR), new DecimalType(outPortNbr));
918 callback.stateChanged(AtlonaPro3Utilities.createChannelID(AtlonaPro3Constants.GROUP_MIRROR,
919 hdmiPortNbr, AtlonaPro3Constants.CHANNEL_PORTMIRRORENABLED), OnOffType.ON);
921 } catch (NumberFormatException e) {
922 logger.warn("Invalid mirror response (can't parse number): '{}'", resp);
925 logger.warn("Invalid mirror response: '{}'", resp);
930 * Handles the unmirror response. The first group should contain the hdmi port number
932 * @param m the non-null {@link Matcher} that matched the response
933 * @param resp the possibly null, possibly empty actual response
935 private void handleUnMirrorResponse(Matcher m, String resp) {
937 throw new IllegalArgumentException("m (matcher) cannot be null");
939 if (m.groupCount() == 1) {
941 int hdmiPortNbr = Integer.parseInt(m.group(1));
942 callback.stateChanged(AtlonaPro3Utilities.createChannelID(AtlonaPro3Constants.GROUP_MIRROR, hdmiPortNbr,
943 AtlonaPro3Constants.CHANNEL_PORTMIRROR), new DecimalType(0));
944 } catch (NumberFormatException e) {
945 logger.warn("Invalid unmirror response (can't parse number): '{}'", resp);
948 logger.warn("Invalid unmirror response: '{}'", resp);
953 * Handles the volume response. The first two group should be the audio port number and the level
955 * @param m the non-null {@link Matcher} that matched the response
956 * @param resp the possibly null, possibly empty actual response
958 private void handleVolumeResponse(Matcher m, String resp) {
960 throw new IllegalArgumentException("m (matcher) cannot be null");
962 if (m.groupCount() == 2) {
964 int portNbr = Integer.parseInt(m.group(1));
965 double level = Double.parseDouble(m.group(2));
966 callback.stateChanged(AtlonaPro3Utilities.createChannelID(AtlonaPro3Constants.GROUP_VOLUME, portNbr,
967 AtlonaPro3Constants.CHANNEL_VOLUME), new DecimalType(level));
968 } catch (NumberFormatException e) {
969 logger.warn("Invalid volume response (can't parse number): '{}'", resp);
972 logger.warn("Invalid volume response: '{}'", resp);
977 * Handles the volume mute response. The first two group should be the audio port number and either "on" or "off
979 * @param m the non-null {@link Matcher} that matched the response
980 * @param resp the possibly null, possibly empty actual response
982 private void handleVolumeMuteResponse(Matcher m, String resp) {
984 throw new IllegalArgumentException("m (matcher) cannot be null");
986 if (m.groupCount() == 2) {
988 int portNbr = Integer.parseInt(m.group(1));
989 switch (m.group(2)) {
991 callback.stateChanged(AtlonaPro3Utilities.createChannelID(AtlonaPro3Constants.GROUP_VOLUME,
992 portNbr, AtlonaPro3Constants.CHANNEL_VOLUME_MUTE), OnOffType.ON);
995 callback.stateChanged(AtlonaPro3Utilities.createChannelID(AtlonaPro3Constants.GROUP_VOLUME,
996 portNbr, AtlonaPro3Constants.CHANNEL_VOLUME_MUTE), OnOffType.OFF);
999 logger.warn("Invalid volume mute response: '{}'", resp);
1001 } catch (NumberFormatException e) {
1002 logger.warn("Invalid volume mute (can't parse number): '{}'", resp);
1005 logger.warn("Invalid volume mute response: '{}'", resp);
1010 * Handles the IR Response. The response is either on or off
1012 * @param resp the possibly null, possibly empty actual response
1014 private void handleIrLockResponse(String resp) {
1015 callback.stateChanged(AtlonaPro3Utilities.createChannelID(AtlonaPro3Constants.GROUP_PRIMARY,
1016 AtlonaPro3Constants.CHANNEL_IRENABLE), RSP_IRON.equals(resp) ? OnOffType.ON : OnOffType.OFF);
1020 * Handles the Save IO Response. Should have one group specifying the preset number
1022 * @param m the non-null {@link Matcher} that matched the response
1023 * @param resp the possibly null, possibly empty actual response
1025 private void handleSaveIoResponse(Matcher m, String resp) {
1026 // nothing to handle
1030 * Handles the Recall IO Response. Should have one group specifying the preset number. After updating the Recall
1031 * State, we refresh all the ports via {@link #refreshAllPortStatuses()}.
1033 * @param m the non-null {@link Matcher} that matched the response
1034 * @param resp the possibly null, possibly empty actual response
1036 private void handleRecallIoResponse(Matcher m, String resp) {
1037 refreshAllPortStatuses();
1041 * Handles the Clear IO Response. Should have one group specifying the preset number.
1043 * @param m the non-null {@link Matcher} that matched the response
1044 * @param resp the possibly null, possibly empty actual response
1046 private void handleClearIoResponse(Matcher m, String resp) {
1047 // nothing to handle
1051 * Handles the broadcast Response. Should have one group specifying the status.
1053 * @param m the non-null {@link Matcher} that matched the response
1054 * @param resp the possibly null, possibly empty actual response
1056 private void handleBroadcastResponse(Matcher m, String resp) {
1057 // nothing to handle
1061 * Handles the matrix reset response. The matrix will go offline immediately on a reset.
1063 * @param resp the possibly null, possibly empty actual response
1065 private void handleMatrixResetResponse(String resp) {
1066 if (RSP_MATRIX_RESET.equals(resp)) {
1067 callback.statusChanged(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
1068 "System is rebooting due to matrix reset");
1073 * Handles a command failure - we simply log the response as an error
1075 * @param resp the possibly null, possibly empty actual response
1077 private void handleCommandFailure(String resp) {
1078 logger.debug("{}", resp);
1082 * This callback is our normal response callback. Should be set into the {@link SocketSession} after the login
1083 * process to handle normal responses.
1085 * @author Tim Roberts
1088 private class NormalResponseCallback implements SocketSessionListener {
1091 public void responseReceived(String response) {
1092 if (response == null || response == "") {
1096 if (RSP_PING.equals(response)) {
1103 m = portStatusPattern.matcher(response);
1105 handlePortOutputResponse(m, response);
1109 m = powerStatusPattern.matcher(response);
1111 handlePowerResponse(m, response);
1115 m = versionPattern.matcher(response);
1117 handleVersionResponse(m, response);
1121 m = versionHdPattern.matcher(response);
1123 handleVersionResponse(m, response);
1127 m = typePattern.matcher(response);
1129 handleTypeResponse(m, response);
1133 m = typeHdPattern.matcher(response);
1135 handleTypeResponse(m, response);
1139 m = portPowerPattern.matcher(response);
1141 handlePortPowerResponse(m, response);
1145 m = volumePattern.matcher(response);
1147 handleVolumeResponse(m, response);
1151 m = volumeMutePattern.matcher(response);
1153 handleVolumeMuteResponse(m, response);
1157 m = portAllPattern.matcher(response);
1159 handlePortAllResponse(response);
1163 m = portMirrorPattern.matcher(response);
1165 handleMirrorResponse(m, response);
1169 m = portUnmirrorPattern.matcher(response);
1171 handleUnMirrorResponse(m, response);
1175 m = saveIoPattern.matcher(response);
1177 handleSaveIoResponse(m, response);
1181 m = recallIoPattern.matcher(response);
1183 handleRecallIoResponse(m, response);
1187 m = clearIoPattern.matcher(response);
1189 handleClearIoResponse(m, response);
1193 m = broadCastPattern.matcher(response);
1195 handleBroadcastResponse(m, response);
1199 if (RSP_IRON.equals(response) || RSP_IROFF.equals(response)) {
1200 handleIrLockResponse(response);
1204 if (RSP_ALL.equals(response)) {
1205 handlePortAllResponse(response);
1209 if (RSP_LOCK.equals(response) || RSP_UNLOCK.equals(response)) {
1210 handlePanelLockResponse(response);
1214 if (RSP_MATRIX_RESET.equals(response)) {
1215 handleMatrixResetResponse(response);
1219 if (response.startsWith(RSP_FAILED)) {
1220 handleCommandFailure(response);
1224 logger.debug("Unhandled response: {}", response);
1228 public void responseException(Exception e) {
1229 callback.statusChanged(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
1230 "Exception occurred reading from Atlona: " + e);
1235 * Special callback used during the login process to not dispatch the responses to this class but rather give them
1236 * back at each call to {@link NoDispatchingCallback#getResponse()}
1238 * @author Tim Roberts
1241 private class NoDispatchingCallback implements SocketSessionListener {
1244 * Cache of responses that have occurred
1246 private BlockingQueue<Object> responses = new ArrayBlockingQueue<>(5);
1249 * Will return the next response from {@link #responses}. If the response is an exception, that exception will
1250 * be thrown instead.
1252 * @return a non-null, possibly empty response
1253 * @throws Exception an exception if one occurred during reading
1255 String getResponse() throws Exception {
1256 final Object lastResponse = responses.poll(5, TimeUnit.SECONDS);
1257 if (lastResponse instanceof String) {
1258 return (String) lastResponse;
1259 } else if (lastResponse instanceof Exception) {
1260 throw (Exception) lastResponse;
1261 } else if (lastResponse == null) {
1262 throw new Exception("Didn't receive response in time");
1264 return lastResponse.toString();
1269 public void responseReceived(String response) {
1271 responses.put(response);
1272 } catch (InterruptedException e) {
1277 public void responseException(Exception e) {
1280 } catch (InterruptedException e1) {