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
41 class AtlonaPro3PortocolHandler {
42 private final Logger logger = LoggerFactory.getLogger(AtlonaPro3PortocolHandler.class);
45 * The {@link SocketSession} used by this protocol handler
47 private final SocketSession session;
50 * The {@link AtlonaPro3Config} configuration used by this handler
52 private final AtlonaPro3Config config;
55 * The {@link AtlonaPro3Capabilities} of the PRO3 model
57 private final AtlonaPro3Capabilities capabilities;
60 * The {@link AtlonaPro3Handler} to call back to update status and state
62 private final AtlonaHandlerCallback callback;
65 * The model type identified by the switch. We save it for faster refreshes since it will not change
67 private String modelType;
70 * The version (firmware) identified by the switch. We save it for faster refreshes since it will not change between
73 private String version;
76 * A special (invalid) command used internally by this handler to identify whether the switch wants a login or not
77 * (see {@link #login()})
79 private static final String NOTVALID_USER_OR_CMD = "notvalid$934%912";
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";
109 // ------------------------------------------------------------------------------------------------
110 // The following are the various responses specified by the Atlona protocol
111 private static final String RSP_FAILED = "Command FAILED:";
113 private static final String RSP_LOGIN = "Login";
114 private static final String RSP_PASSWORD = "Password";
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";
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)";
143 * Constructs the protocol handler from given parameters
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
150 AtlonaPro3PortocolHandler(SocketSession session, AtlonaPro3Config config, AtlonaPro3Capabilities capabilities,
151 AtlonaHandlerCallback callback) {
152 if (session == null) {
153 throw new IllegalArgumentException("session cannot be null");
156 if (config == null) {
157 throw new IllegalArgumentException("config cannot be null");
160 if (capabilities == null) {
161 throw new IllegalArgumentException("capabilities cannot be null");
164 if (callback == null) {
165 throw new IllegalArgumentException("callback cannot be null");
168 this.session = session;
169 this.config = config;
170 this.capabilities = capabilities;
171 this.callback = callback;
175 * Attempts to log into the switch when prompted by the switch. Please see code comments on the exact protocol for
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
181 String login() throws Exception {
182 logger.debug("Logging into atlona switch");
183 // Void to make sure we retrieve them
187 NoDispatchingCallback callback = new NoDispatchingCallback();
188 session.addListener(callback);
190 // Burn the initial (empty) return
193 response = callback.getResponse();
194 if (!response.equals("")) {
195 logger.info("Altona protocol violation - didn't start with an inital empty response: '{}'", response);
197 } catch (Exception e) {
198 // ignore - may not having given us an initial ""
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);
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");
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);
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.";
229 // Send the username and wait for a ": " response
230 session.sendCommand(config.getUserName());
232 return "Altona protocol violation - wasn't initially a command failure or login prompt: " + response;
235 // We should have gotten the password response
236 response = callback.getResponse();
238 // Burn the empty response if we got one (
239 if (response.equals("")) {
240 response = callback.getResponse();
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";
247 return "Altona protocol violation - invalid response to a login: " + response;
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.";
256 session.sendCommand(config.getPassword());
257 response = callback.getResponse();
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
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);
269 // Now send an invalid command
270 session.sendCommand(NOTVALID_USER_OR_CMD);
272 // If we get an invalid command response - we are logged in
273 response = callback.getResponse();
274 if (response.startsWith(RSP_FAILED)) {
279 // Nope - password invalid
280 return "Password was invalid - please check your atlona setup";
284 * Post successful login stuff - mark us online and refresh from the switch
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);
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);
296 // setup the most likely state of these switches (there is no protocol to get them)
301 * Returns the callback being used by this handler
303 * @return a non-null {@link AtlonaHandlerCallback}
305 AtlonaHandlerCallback getCallback() {
310 * Pings the server with an (invalid) ping command to keep the connection alive
313 sendCommand(CMD_PING);
317 * Refreshes the state from the switch itself. This will retrieve all the state (that we can get) from the switch.
320 logger.debug("Refreshing matrix state");
321 if (version == null) {
324 callback.setProperty(AtlonaPro3Constants.PROPERTY_VERSION, version);
327 if (modelType == null) {
330 callback.setProperty(AtlonaPro3Constants.PROPERTY_TYPE, modelType);
334 refreshAllPortStatuses();
336 final int nbrPowerPorts = capabilities.getNbrPowerPorts();
337 for (int x = 1; x <= nbrPowerPorts; x++) {
341 final int nbrAudioPorts = capabilities.getNbrAudioPorts();
342 for (int x = 1; x <= nbrAudioPorts; x++) {
343 refreshVolumeStatus(x);
344 refreshVolumeMute(x);
347 for (int x : capabilities.getHdmiPorts()) {
348 refreshPortStatus(x);
353 * Sets the power to the switch
355 * @param on true if on, false otherwise
357 void setPower(boolean on) {
358 sendCommand(on ? CMD_POWERON : CMD_POWEROFF);
362 * Queries the switch about it's power state
364 void refreshPower() {
365 sendCommand(CMD_POWER_STATUS);
369 * Queries the switch about it's version (firmware)
371 void refreshVersion() {
372 sendCommand(CMD_VERSION);
376 * Queries the switch about it's type (model)
379 sendCommand(CMD_TYPE);
383 * Sets whether the front panel is locked or not
385 * @param locked true if locked, false otherwise
387 void setPanelLock(boolean locked) {
388 sendCommand(locked ? CMD_PANELLOCK : CMD_PANELUNLOCK);
392 * Resets all ports back to their default state.
394 void resetAllPorts() {
395 sendCommand(CMD_PORT_RESETALL);
399 * Sets whether the specified port is powered (i.e. outputing).
401 * @param portNbr a greater than zero port number
402 * @param on true if powered.
404 void setPortPower(int portNbr, boolean on) {
406 throw new IllegalArgumentException("portNbr must be greater than 0");
408 sendCommand(String.format(CMD_PORT_POWER_FORMAT, portNbr, on ? "on" : "off"));
412 * Refreshes whether the specified port is powered (i.e. outputing).
414 * @param portNbr a greater than zero port number
416 void refreshPortPower(int portNbr) {
418 throw new IllegalArgumentException("portNbr must be greater than 0");
420 sendCommand(String.format(CMD_PORT_POWER_FORMAT, portNbr, "sta"));
424 * Sets all the output ports to the specified input port.
426 * @param portNbr a greater than zero port number
428 void setPortAll(int portNbr) {
430 throw new IllegalArgumentException("portNbr must be greater than 0");
432 sendCommand(String.format(CMD_PORT_ALL_FORMAT, portNbr));
436 * Sets the input port number to the specified output port number.
438 * @param inPortNbr a greater than zero port number
439 * @param outPortNbr a greater than zero port number
441 void setPortSwitch(int inPortNbr, int outPortNbr) {
442 if (inPortNbr <= 0) {
443 throw new IllegalArgumentException("inPortNbr must be greater than 0");
445 if (outPortNbr <= 0) {
446 throw new IllegalArgumentException("outPortNbr must be greater than 0");
448 sendCommand(String.format(CMD_PORT_SWITCH_FORMAT, inPortNbr, outPortNbr));
452 * Sets the hdmi port number to mirror the specified output port number.
454 * @param hdmiPortNbr a greater than zero port number
455 * @param outPortNbr a greater than zero port number
457 void setPortMirror(int hdmiPortNbr, int outPortNbr) {
458 if (hdmiPortNbr <= 0) {
459 throw new IllegalArgumentException("hdmiPortNbr must be greater than 0");
461 if (outPortNbr <= 0) {
462 throw new IllegalArgumentException("outPortNbr must be greater than 0");
465 if (capabilities.getHdmiPorts().contains(hdmiPortNbr)) {
466 sendCommand(String.format(CMD_PORT_MIRROR_FORMAT, hdmiPortNbr, outPortNbr));
468 logger.info("Trying to set port mirroring on a non-hdmi port: {}", hdmiPortNbr);
473 * Disabled mirroring on the specified hdmi port number.
475 * @param hdmiPortNbr a greater than zero port number
476 * @param outPortNbr a greater than zero port number
478 void removePortMirror(int hdmiPortNbr) {
479 if (hdmiPortNbr <= 0) {
480 throw new IllegalArgumentException("hdmiPortNbr must be greater than 0");
483 if (capabilities.getHdmiPorts().contains(hdmiPortNbr)) {
484 sendCommand(String.format(CMD_PORT_UNMIRROR_FORMAT, hdmiPortNbr));
486 logger.info("Trying to remove port mirroring on a non-hdmi port: {}", hdmiPortNbr);
491 * Sets the volume level on the specified audio port.
493 * @param portNbr a greater than zero port number
494 * @param level a volume level in decibels (must range from -79 to +15)
496 void setVolume(int portNbr, double level) {
498 throw new IllegalArgumentException("portNbr must be greater than 0");
500 if (level < -79 || level > 15) {
501 throw new IllegalArgumentException("level must be between -79 to +15");
503 sendCommand(String.format(CMD_VOLUME_FORMAT, portNbr, level));
507 * Refreshes the volume level for the given audio port.
509 * @param portNbr a greater than zero port number
511 void refreshVolumeStatus(int portNbr) {
513 throw new IllegalArgumentException("portNbr must be greater than 0");
515 sendCommand(String.format(CMD_VOLUME_FORMAT, portNbr, "sta"));
519 * Refreshes the specified hdmi port's mirroring status
521 * @param hdmiPortNbr a greater than zero hdmi port number
523 void refreshPortMirror(int hdmiPortNbr) {
524 if (hdmiPortNbr <= 0) {
525 throw new IllegalArgumentException("hdmiPortNbr must be greater than 0");
527 sendCommand(String.format(CMD_PORT_MIRROR_STATUS_FORMAT, hdmiPortNbr));
531 * Mutes/Unmutes the specified audio port.
533 * @param portNbr a greater than zero port number
534 * @param mute true to mute, false to unmute
536 void setVolumeMute(int portNbr, boolean mute) {
538 throw new IllegalArgumentException("portNbr must be greater than 0");
540 sendCommand(String.format(CMD_VOLUME_MUTE_FORMAT, portNbr, mute ? "on" : "off"));
544 * Refreshes the volume mute for the given audio port.
546 * @param portNbr a greater than zero port number
548 void refreshVolumeMute(int portNbr) {
550 throw new IllegalArgumentException("portNbr must be greater than 0");
552 sendCommand(String.format(CMD_VOLUME_MUTE_FORMAT, portNbr, "sta"));
556 * Turn on/off the front panel IR.
558 * @param on true for on, false otherwise
560 void setIrOn(boolean on) {
561 sendCommand(on ? CMD_IRON : CMD_IROFF);
565 * Refreshes the input port setting on the specified output port.
567 * @param portNbr a greater than zero port number
569 void refreshPortStatus(int portNbr) {
571 throw new IllegalArgumentException("portNbr must be greater than 0");
573 sendCommand(String.format(CMD_PORT_STATUS_FORMAT, portNbr));
577 * Refreshes all of the input port settings for all of the output ports.
579 private void refreshAllPortStatuses() {
580 sendCommand(CMD_PORT_STATUS);
584 * Saves the current Input/Output scheme to the specified preset number.
586 * @param presetNbr a greater than 0 preset number
588 void saveIoSettings(int presetNbr) {
589 if (presetNbr <= 0) {
590 throw new IllegalArgumentException("presetNbr must be greater than 0");
592 sendCommand(String.format(CMD_SAVEIO_FORMAT, presetNbr));
596 * Recalls the Input/Output scheme for the specified preset number.
598 * @param presetNbr a greater than 0 preset number
600 void recallIoSettings(int presetNbr) {
601 if (presetNbr <= 0) {
602 throw new IllegalArgumentException("presetNbr must be greater than 0");
604 sendCommand(String.format(CMD_RECALLIO_FORMAT, presetNbr));
608 * Clears the Input/Output scheme for the specified preset number.
610 * @param presetNbr a greater than 0 preset number
612 void clearIoSettings(int presetNbr) {
613 if (presetNbr <= 0) {
614 throw new IllegalArgumentException("presetNbr must be greater than 0");
616 sendCommand(String.format(CMD_CLEARIO_FORMAT, presetNbr));
620 * Resets the matrix back to defaults.
623 sendCommand(CMD_MATRIX_RESET);
627 * Sends the command and puts the thing into {@link ThingStatus#OFFLINE} if an IOException occurs
629 * @param command a non-null, non-empty command to send
631 private void sendCommand(String command) {
632 if (command == null) {
633 throw new IllegalArgumentException("command cannot be null");
635 if (command.trim().length() == 0) {
636 throw new IllegalArgumentException("command cannot be empty");
639 session.sendCommand(command);
640 } catch (IOException e) {
641 callback.statusChanged(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
642 "Exception occurred sending to Atlona: " + e);
647 * Handles the switch power response. The first matching group should be "on" or "off"
649 * @param m the non-null {@link Matcher} that matched the response
650 * @param resp the possibly null, possibly empty actual response
652 private void handlePowerResponse(Matcher m, String resp) {
654 throw new IllegalArgumentException("m (matcher) cannot be null");
656 if (m.groupCount() == 1) {
657 switch (m.group(1)) {
659 callback.stateChanged(AtlonaPro3Utilities.createChannelID(AtlonaPro3Constants.GROUP_PRIMARY,
660 AtlonaPro3Constants.CHANNEL_POWER), OnOffType.ON);
663 callback.stateChanged(AtlonaPro3Utilities.createChannelID(AtlonaPro3Constants.GROUP_PRIMARY,
664 AtlonaPro3Constants.CHANNEL_POWER), OnOffType.OFF);
667 logger.warn("Invalid power response: '{}'", resp);
670 logger.warn("Invalid power response: '{}'", resp);
675 * Handles the version (firmware) response. The first matching group should be the version
677 * @param m the non-null {@link Matcher} that matched the response
678 * @param resp the possibly null, possibly empty actual response
680 private void handleVersionResponse(Matcher m, String resp) {
682 throw new IllegalArgumentException("m (matcher) cannot be null");
684 if (m.groupCount() == 1) {
685 version = m.group(1);
686 callback.setProperty(AtlonaPro3Constants.PROPERTY_VERSION, version);
688 logger.warn("Invalid version response: '{}'", resp);
693 * Handles the type (model) response. The first matching group should be the type.
695 * @param m the non-null {@link Matcher} that matched the response
696 * @param resp the possibly null, possibly empty actual response
698 private void handleTypeResponse(Matcher m, String resp) {
700 throw new IllegalArgumentException("m (matcher) cannot be null");
702 if (m.groupCount() == 1) {
704 callback.setProperty(AtlonaPro3Constants.PROPERTY_TYPE, modelType);
706 logger.warn("Invalid Type response: '{}'", resp);
711 * Handles the panel lock response. The response is only on or off.
713 * @param resp the possibly null, possibly empty actual response
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);
721 * Handles the port power response. The first two groups should be the port nbr and either "on" or "off"
723 * @param m the non-null {@link Matcher} that matched the response
724 * @param resp the possibly null, possibly empty actual response
726 private void handlePortPowerResponse(Matcher m, String resp) {
728 throw new IllegalArgumentException("m (matcher) cannot be null");
730 if (m.groupCount() == 2) {
732 int portNbr = Integer.parseInt(m.group(1));
733 switch (m.group(2)) {
735 callback.stateChanged(AtlonaPro3Utilities.createChannelID(AtlonaPro3Constants.GROUP_PORT,
736 portNbr, AtlonaPro3Constants.CHANNEL_PORTPOWER), OnOffType.ON);
739 callback.stateChanged(AtlonaPro3Utilities.createChannelID(AtlonaPro3Constants.GROUP_PORT,
740 portNbr, AtlonaPro3Constants.CHANNEL_PORTPOWER), OnOffType.OFF);
743 logger.warn("Invalid port power response: '{}'", resp);
745 } catch (NumberFormatException e) {
746 logger.warn("Invalid port power (can't parse number): '{}'", resp);
749 logger.warn("Invalid port power response: '{}'", resp);
754 * Handles the port all response. Simply calls {@link #refreshAllPortStatuses()}
756 * @param resp ignored
758 private void handlePortAllResponse(String resp) {
759 refreshAllPortStatuses();
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
766 * @param m the non-null {@link Matcher} that matched the response
767 * @param resp the possibly null, possibly empty actual response
769 private void handlePortOutputResponse(Matcher m, String resp) {
771 throw new IllegalArgumentException("m (matcher) cannot be null");
777 int inPort = Integer.parseInt(m.group(1));
778 int outPort = Integer.parseInt(m.group(2));
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);
789 * Handles the mirror response. The matcher should have two groups - an hdmi port number and an output port number.
791 * @param m the non-null {@link Matcher} that matched the response
792 * @param resp the possibly null, possibly empty actual response
794 private void handleMirrorResponse(Matcher m, String resp) {
796 throw new IllegalArgumentException("m (matcher) cannot be null");
798 if (m.groupCount() == 3) {
800 int hdmiPortNbr = Integer.parseInt(m.group(1));
802 // could be "off" (if mirror off), "on"/"Out" (with 3rd group representing out)
803 String oper = StringUtils.trimToEmpty(m.group(2)).toLowerCase();
805 if (oper.equals("off")) {
806 callback.stateChanged(AtlonaPro3Utilities.createChannelID(AtlonaPro3Constants.GROUP_MIRROR,
807 hdmiPortNbr, AtlonaPro3Constants.CHANNEL_PORTMIRRORENABLED), OnOffType.OFF);
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);
815 } catch (NumberFormatException e) {
816 logger.warn("Invalid mirror response (can't parse number): '{}'", resp);
819 logger.warn("Invalid mirror response: '{}'", resp);
824 * Handles the unmirror response. The first group should contain the hdmi port number
826 * @param m the non-null {@link Matcher} that matched the response
827 * @param resp the possibly null, possibly empty actual response
829 private void handleUnMirrorResponse(Matcher m, String resp) {
831 throw new IllegalArgumentException("m (matcher) cannot be null");
833 if (m.groupCount() == 1) {
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);
842 logger.warn("Invalid unmirror response: '{}'", resp);
847 * Handles the volume response. The first two group should be the audio port number and the level
849 * @param m the non-null {@link Matcher} that matched the response
850 * @param resp the possibly null, possibly empty actual response
852 private void handleVolumeResponse(Matcher m, String resp) {
854 throw new IllegalArgumentException("m (matcher) cannot be null");
856 if (m.groupCount() == 2) {
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);
866 logger.warn("Invalid volume response: '{}'", resp);
871 * Handles the volume mute response. The first two group should be the audio port number and either "on" or "off
873 * @param m the non-null {@link Matcher} that matched the response
874 * @param resp the possibly null, possibly empty actual response
876 private void handleVolumeMuteResponse(Matcher m, String resp) {
878 throw new IllegalArgumentException("m (matcher) cannot be null");
880 if (m.groupCount() == 2) {
882 int portNbr = Integer.parseInt(m.group(1));
883 switch (m.group(2)) {
885 callback.stateChanged(AtlonaPro3Utilities.createChannelID(AtlonaPro3Constants.GROUP_VOLUME,
886 portNbr, AtlonaPro3Constants.CHANNEL_VOLUME_MUTE), OnOffType.ON);
889 callback.stateChanged(AtlonaPro3Utilities.createChannelID(AtlonaPro3Constants.GROUP_VOLUME,
890 portNbr, AtlonaPro3Constants.CHANNEL_VOLUME_MUTE), OnOffType.OFF);
893 logger.warn("Invalid volume mute response: '{}'", resp);
895 } catch (NumberFormatException e) {
896 logger.warn("Invalid volume mute (can't parse number): '{}'", resp);
899 logger.warn("Invalid volume mute response: '{}'", resp);
904 * Handles the IR Response. The response is either on or off
906 * @param resp the possibly null, possibly empty actual response
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);
914 * Handles the Save IO Response. Should have one group specifying the preset number
916 * @param m the non-null {@link Matcher} that matched the response
917 * @param resp the possibly null, possibly empty actual response
919 private void handleSaveIoResponse(Matcher m, String resp) {
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()}.
927 * @param m the non-null {@link Matcher} that matched the response
928 * @param resp the possibly null, possibly empty actual response
930 private void handleRecallIoResponse(Matcher m, String resp) {
931 refreshAllPortStatuses();
935 * Handles the Clear IO Response. Should have one group specifying the preset number.
937 * @param m the non-null {@link Matcher} that matched the response
938 * @param resp the possibly null, possibly empty actual response
940 private void handleClearIoResponse(Matcher m, String resp) {
945 * Handles the broadcast Response. Should have one group specifying the status.
947 * @param m the non-null {@link Matcher} that matched the response
948 * @param resp the possibly null, possibly empty actual response
950 private void handleBroadcastResponse(Matcher m, String resp) {
955 * Handles the matrix reset response. The matrix will go offline immediately on a reset.
957 * @param resp the possibly null, possibly empty actual response
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");
967 * Handles a command failure - we simply log the response as an error
969 * @param resp the possibly null, possibly empty actual response
971 private void handleCommandFailure(String resp) {
972 logger.info("{}", resp);
976 * This callback is our normal response callback. Should be set into the {@link SocketSession} after the login
977 * process to handle normal responses.
979 * @author Tim Roberts
982 private class NormalResponseCallback implements SocketSessionListener {
985 public void responseReceived(String response) {
986 if (response == null || response == "") {
990 if (RSP_PING.equals(response)) {
997 m = portStatusPattern.matcher(response);
999 handlePortOutputResponse(m, response);
1003 m = powerStatusPattern.matcher(response);
1005 handlePowerResponse(m, response);
1009 m = versionPattern.matcher(response);
1011 handleVersionResponse(m, response);
1015 m = typePattern.matcher(response);
1017 handleTypeResponse(m, response);
1021 m = portPowerPattern.matcher(response);
1023 handlePortPowerResponse(m, response);
1027 m = volumePattern.matcher(response);
1029 handleVolumeResponse(m, response);
1033 m = volumeMutePattern.matcher(response);
1035 handleVolumeMuteResponse(m, response);
1039 m = portAllPattern.matcher(response);
1041 handlePortAllResponse(response);
1045 m = portMirrorPattern.matcher(response);
1047 handleMirrorResponse(m, response);
1051 m = portUnmirrorPattern.matcher(response);
1053 handleUnMirrorResponse(m, response);
1057 m = saveIoPattern.matcher(response);
1059 handleSaveIoResponse(m, response);
1063 m = recallIoPattern.matcher(response);
1065 handleRecallIoResponse(m, response);
1069 m = clearIoPattern.matcher(response);
1071 handleClearIoResponse(m, response);
1075 m = broadCastPattern.matcher(response);
1077 handleBroadcastResponse(m, response);
1081 if (RSP_IRON.equals(response) || RSP_IROFF.equals(response)) {
1082 handleIrLockResponse(response);
1086 if (RSP_ALL.equals(response)) {
1087 handlePortAllResponse(response);
1091 if (RSP_LOCK.equals(response) || RSP_UNLOCK.equals(response)) {
1092 handlePanelLockResponse(response);
1096 if (RSP_MATRIX_RESET.equals(response)) {
1097 handleMatrixResetResponse(response);
1101 if (response.startsWith(RSP_FAILED)) {
1102 handleCommandFailure(response);
1106 logger.info("Unhandled response: {}", response);
1110 public void responseException(Exception e) {
1111 callback.statusChanged(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
1112 "Exception occurred reading from Atlona: " + e);
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()}
1120 * @author Tim Roberts
1123 private class NoDispatchingCallback implements SocketSessionListener {
1126 * Cache of responses that have occurred
1128 private BlockingQueue<Object> responses = new ArrayBlockingQueue<>(5);
1131 * Will return the next response from {@link #responses}. If the response is an exception, that exception will
1132 * be thrown instead.
1134 * @return a non-null, possibly empty response
1135 * @throws Exception an exception if one occurred during reading
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");
1146 return lastResponse.toString();
1151 public void responseReceived(String response) {
1153 responses.put(response);
1154 } catch (InterruptedException e) {
1159 public void responseException(Exception e) {
1162 } catch (InterruptedException e1) {