2 * Copyright (c) 2010-2024 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.openhab.binding.atlona.internal.AtlonaHandlerCallback;
23 import org.openhab.binding.atlona.internal.net.SocketSession;
24 import org.openhab.binding.atlona.internal.net.SocketSessionListener;
25 import org.openhab.core.library.types.DecimalType;
26 import org.openhab.core.library.types.OnOffType;
27 import org.openhab.core.thing.ThingStatus;
28 import org.openhab.core.thing.ThingStatusDetail;
29 import org.slf4j.Logger;
30 import org.slf4j.LoggerFactory;
33 * This is the protocol handler for the PRO3 product line. This handler will issue the protocol commands and will
34 * process the responses from the PRO3 switch. This handler was written to respond to any response that can be sent from
35 * the TCP/IP session (either in response to our own commands or in response to external events [other TCP/IP sessions,
36 * web GUI, front panel keystrokes, etc]).
38 * @author Tim Roberts - Initial contribution
39 * @author Michael Lobstein - Add support for AT-PRO3HD66M
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 // Constants added to support the HD models
138 private static final String RSP_WELCOME = "Welcome to TELNET";
139 private static final String RSP_LOGIN_PLEASE = "Login Please";
140 private static final String RSP_USERNAME = "Username";
141 private static final String RSP_TRY_AGAIN = "Please Try Again";
142 private final Pattern versionHdPattern = Pattern.compile("V(.*)");
143 private final Pattern typeHdPattern = Pattern.compile("AT-PRO3HD(\\d+)M");
145 // ------------------------------------------------------------------------------------------------
146 // The following isn't part of the atlona protocol and is generated by us
147 private static final String CMD_PING = "ping";
148 private static final String RSP_PING = "Command FAILED: (ping)";
151 * Constructs the protocol handler from given parameters
153 * @param session a non-null {@link SocketSession} (may be connected or disconnected)
154 * @param config a non-null {@link AtlonaPro3Config}
155 * @param capabilities a non-null {@link AtlonaPro3Capabilities}
156 * @param callback a non-null {@link AtlonaHandlerCallback} to update state and status
158 AtlonaPro3PortocolHandler(SocketSession session, AtlonaPro3Config config, AtlonaPro3Capabilities capabilities,
159 AtlonaHandlerCallback callback) {
160 if (session == null) {
161 throw new IllegalArgumentException("session cannot be null");
164 if (config == null) {
165 throw new IllegalArgumentException("config cannot be null");
168 if (capabilities == null) {
169 throw new IllegalArgumentException("capabilities cannot be null");
172 if (callback == null) {
173 throw new IllegalArgumentException("callback cannot be null");
176 this.session = session;
177 this.config = config;
178 this.capabilities = capabilities;
179 this.callback = callback;
183 * Attempts to log into the switch when prompted by the switch. Please see code comments on the exact protocol for
186 * @return a null if logged in successfully (or if switch didn't require login). Non-null if an exception occurred.
187 * @throws IOException an IO exception occurred during login
189 String loginUHD() throws Exception {
190 logger.debug("Logging into atlona switch");
191 // Void to make sure we retrieve them
195 NoDispatchingCallback callback = new NoDispatchingCallback();
196 session.addListener(callback);
198 // Burn the initial (empty) return
201 response = callback.getResponse();
202 if (!"".equals(response)) {
203 logger.debug("Atlona protocol violation - didn't start with an inital empty response: '{}'", response);
205 } catch (Exception e) {
206 // ignore - may not having given us an initial ""
209 // At this point - we are not sure if it's:
210 // 1) waiting for a command input
211 // or 2) has sent a "Login: " prompt
212 // By sending a string that doesn't exist as a command or user
213 // we can tell which by the response to the invalid command
214 session.sendCommand(NOTVALID_USER_OR_CMD);
216 // Command failed - Atlona not configured with IPLogin - return success
217 response = callback.getResponse();
218 if (response.startsWith(RSP_FAILED)) {
219 logger.debug("Atlona didn't require a login");
224 // We should have been presented with a new "\r\nLogin: "
225 response = callback.getResponse();
226 if (!"".equals(response)) {
227 logger.debug("Atlona protocol violation - didn't start with an inital empty response: '{}'", response);
230 // Get the new "Login: " prompt response
231 response = callback.getResponse();
232 if (RSP_LOGIN.equals(response)) {
233 if (config.getUserName() == null || config.getUserName().trim().length() == 0) {
234 return "Atlona PRO3 has enabled Telnet/IP Login but no username was provided in the configuration.";
237 // Send the username and wait for a ": " response
238 session.sendCommand(config.getUserName());
240 return "Atlona protocol violation - wasn't initially a command failure or login prompt: " + response;
243 // We should have gotten the password response
244 response = callback.getResponse();
246 // Burn the empty response if we got one (
247 if ("".equals(response)) {
248 response = callback.getResponse();
250 if (!RSP_PASSWORD.equals(response)) {
251 // If we got another login response, username wasn't valid
252 if (RSP_LOGIN.equals(response)) {
253 return "Username " + config.getUserName() + " is not a valid user on the atlona";
255 return "Atlona protocol violation - invalid response to a login: " + response;
258 // Make sure we have a password
259 if (config.getPassword() == null || config.getPassword().trim().length() == 0) {
260 return "Atlona PRO3 has enabled Telnet/IP Login but no password was provided in the configuration.";
264 session.sendCommand(config.getPassword());
265 response = callback.getResponse();
267 // At this point, we don't know if we received a
268 // 1) "\r\n" and waiting for a command
269 // or 2) "\r\nLogin: " if the password is invalid
270 // Send an invalid command to see if we get the failed command response
272 // First make sure we had an empty response (the "\r\n" part)
273 if (!"".equals(response)) {
274 logger.debug("Atlona protocol violation - not an empty response after password: '{}'", response);
277 // Now send an invalid command
278 session.sendCommand(NOTVALID_USER_OR_CMD);
280 // If we get an invalid command response - we are logged in
281 response = callback.getResponse();
282 if (response.startsWith(RSP_FAILED)) {
287 // Nope - password invalid
288 return "Password was invalid - please check your atlona setup";
292 * Attempts to log into the older HD model switches using a slightly different protocol
294 * @return a null if logged in successfully (or if switch didn't require login). Non-null if an exception occurred.
295 * @throws IOException an IO exception occurred during login
297 String loginHD() throws Exception {
298 logger.debug("Logging into atlona switch");
299 // Void to make sure we retrieve them
303 NoDispatchingCallback callback = new NoDispatchingCallback();
304 session.addListener(callback);
306 // Burn the initial (empty) return
309 response = callback.getResponse();
310 if (!"".equals(response)) {
311 logger.debug("Atlona protocol violation - didn't start with an inital empty response: '{}'", response);
313 } catch (Exception e) {
314 // ignore - may not having given us an initial ""
317 response = callback.getResponse();
318 if (response.startsWith(RSP_WELCOME)) {
319 logger.debug("Atlona AT-PRO3HD66M didn't require a login");
323 if (!response.startsWith(RSP_LOGIN_PLEASE)) {
324 logger.debug("Atlona protocol violation - didn't start with login prompt '{}'", response);
326 // Since we were not logged in automatically, a user name is required from the configuration
327 if (config.getUserName() == null || config.getUserName().trim().length() == 0) {
328 return "Atlona PRO3 has enabled Telnet/IP Login but no username was provided in the configuration.";
331 // Make sure we have a password too
332 if (config.getPassword() == null || config.getPassword().trim().length() == 0) {
333 return "Atlona PRO3 has enabled Telnet/IP Login but no password was provided in the configuration.";
336 // Check for an empty response after the login prompt (the "\r\n" part)
337 response = callback.getResponse();
338 if (!"".equals(response)) {
339 logger.debug("Atlona protocol violation - not an empty response after password: '{}'", response);
342 // Send the username and wait for a ": " response
343 session.sendCommand(config.getUserName());
345 // We should have gotten the username response
346 response = callback.getResponse();
347 if (!response.startsWith(RSP_USERNAME)) {
348 logger.debug("Atlona protocol violation - invalid response to username: '{}'", response);
353 session.sendCommand(config.getPassword());
354 response = callback.getResponse();
355 } catch (Exception e) {
356 return "Password was invalid - please check your atlona setup";
359 if (response.startsWith(RSP_TRY_AGAIN)) {
360 return "Username " + config.getUserName() + " is not a valid user on the atlona";
363 if (response.startsWith(RSP_PASSWORD)) {
364 // After the correct password is sent, several empty responses are sent before the welcome message
365 for (int i = 0; i < 8; i++) {
366 response = callback.getResponse();
368 // If we get a welcome message, login was successful
369 if (response.startsWith(RSP_WELCOME)) {
376 return "Authentication failed - please check your atlona setup";
380 * Post successful login stuff - mark us online and refresh from the switch
382 private void postLogin() {
383 logger.debug("Atlona switch now connected");
384 session.clearListeners();
385 session.addListener(new NormalResponseCallback());
386 callback.statusChanged(ThingStatus.ONLINE, ThingStatusDetail.NONE, null);
388 if (capabilities.isUHDModel()) {
389 // Set broadcast to on to receive notifications when
390 // routing changes (via the webpage, or presets or IR, etc)
391 sendCommand(CMD_BROADCAST_ON);
394 // setup the most likely state of these switches (there is no protocol to get them)
399 * Returns the callback being used by this handler
401 * @return a non-null {@link AtlonaHandlerCallback}
403 AtlonaHandlerCallback getCallback() {
408 * Pings the server with an (invalid) ping command to keep the connection alive
411 if (capabilities.isUHDModel()) {
412 sendCommand(CMD_PING);
414 // the HD model does not reflect the invalid command string back in the response for us to match later
415 sendCommand(CMD_VERSION);
420 * Refreshes the state from the switch itself. This will retrieve all the state (that we can get) from the switch.
423 logger.debug("Refreshing matrix state");
424 if (version == null) {
427 callback.setProperty(AtlonaPro3Constants.PROPERTY_VERSION, version);
430 if (modelType == null) {
433 callback.setProperty(AtlonaPro3Constants.PROPERTY_TYPE, modelType);
437 if (capabilities.isUHDModel()) {
438 refreshAllPortStatuses();
441 final int nbrPowerPorts = capabilities.getNbrPowerPorts();
442 for (int x = 1; x <= nbrPowerPorts; x++) {
446 final int nbrAudioPorts = capabilities.getNbrAudioPorts();
447 for (int x = 1; x <= nbrAudioPorts; x++) {
448 refreshVolumeStatus(x);
449 refreshVolumeMute(x);
452 for (int x : capabilities.getHdmiPorts()) {
453 refreshPortStatus(x);
458 * Sets the power to the switch
460 * @param on true if on, false otherwise
462 void setPower(boolean on) {
463 sendCommand(on ? CMD_POWERON : CMD_POWEROFF);
467 * Queries the switch about it's power state
469 void refreshPower() {
470 sendCommand(CMD_POWER_STATUS);
474 * Queries the switch about it's version (firmware)
476 void refreshVersion() {
477 sendCommand(CMD_VERSION);
481 * Queries the switch about it's type (model)
484 sendCommand(CMD_TYPE);
488 * Sets whether the front panel is locked or not
490 * @param locked true if locked, false otherwise
492 void setPanelLock(boolean locked) {
493 sendCommand(locked ? CMD_PANELLOCK : CMD_PANELUNLOCK);
497 * Resets all ports back to their default state.
499 void resetAllPorts() {
500 sendCommand(CMD_PORT_RESETALL);
504 * Sets whether the specified port is powered (i.e. outputing).
506 * @param portNbr a greater than zero port number
507 * @param on true if powered.
509 void setPortPower(int portNbr, boolean on) {
511 throw new IllegalArgumentException("portNbr must be greater than 0");
513 sendCommand(String.format(CMD_PORT_POWER_FORMAT, portNbr, on ? "on" : "off"));
517 * Refreshes whether the specified port is powered (i.e. outputing).
519 * @param portNbr a greater than zero port number
521 void refreshPortPower(int portNbr) {
523 throw new IllegalArgumentException("portNbr must be greater than 0");
525 sendCommand(String.format(CMD_PORT_POWER_FORMAT, portNbr, "sta"));
529 * Sets all the output ports to the specified input port.
531 * @param portNbr a greater than zero port number
533 void setPortAll(int portNbr) {
535 throw new IllegalArgumentException("portNbr must be greater than 0");
537 sendCommand(String.format(CMD_PORT_ALL_FORMAT, portNbr));
541 * Sets the input port number to the specified output port number.
543 * @param inPortNbr a greater than zero port number
544 * @param outPortNbr a greater than zero port number
546 void setPortSwitch(int inPortNbr, int outPortNbr) {
547 if (inPortNbr <= 0) {
548 throw new IllegalArgumentException("inPortNbr must be greater than 0");
550 if (outPortNbr <= 0) {
551 throw new IllegalArgumentException("outPortNbr must be greater than 0");
553 sendCommand(String.format(CMD_PORT_SWITCH_FORMAT, inPortNbr, outPortNbr));
557 * Sets the hdmi port number to mirror the specified output port number.
559 * @param hdmiPortNbr a greater than zero port number
560 * @param outPortNbr a greater than zero port number
562 void setPortMirror(int hdmiPortNbr, int outPortNbr) {
563 if (hdmiPortNbr <= 0) {
564 throw new IllegalArgumentException("hdmiPortNbr must be greater than 0");
566 if (outPortNbr <= 0) {
567 throw new IllegalArgumentException("outPortNbr must be greater than 0");
570 if (capabilities.getHdmiPorts().contains(hdmiPortNbr)) {
571 sendCommand(String.format(CMD_PORT_MIRROR_FORMAT, hdmiPortNbr, outPortNbr));
573 logger.info("Trying to set port mirroring on a non-hdmi port: {}", hdmiPortNbr);
578 * Disabled mirroring on the specified hdmi port number.
580 * @param hdmiPortNbr a greater than zero port number
581 * @param outPortNbr a greater than zero port number
583 void removePortMirror(int hdmiPortNbr) {
584 if (hdmiPortNbr <= 0) {
585 throw new IllegalArgumentException("hdmiPortNbr must be greater than 0");
588 if (capabilities.getHdmiPorts().contains(hdmiPortNbr)) {
589 sendCommand(String.format(CMD_PORT_UNMIRROR_FORMAT, hdmiPortNbr));
591 logger.info("Trying to remove port mirroring on a non-hdmi port: {}", hdmiPortNbr);
596 * Sets the volume level on the specified audio port.
598 * @param portNbr a greater than zero port number
599 * @param level a volume level in decibels (must range from -79 to +15)
601 void setVolume(int portNbr, int level) {
603 throw new IllegalArgumentException("portNbr must be greater than 0");
605 if (level < -79 || level > 15) {
606 throw new IllegalArgumentException("level must be between -79 to +15");
608 sendCommand(String.format(CMD_VOLUME_FORMAT, portNbr, level));
612 * Refreshes the volume level for the given audio port.
614 * @param portNbr a greater than zero port number
616 void refreshVolumeStatus(int portNbr) {
618 throw new IllegalArgumentException("portNbr must be greater than 0");
620 sendCommand(String.format(CMD_VOLUME_FORMAT, portNbr, "sta"));
624 * Refreshes the specified hdmi port's mirroring status
626 * @param hdmiPortNbr a greater than zero hdmi port number
628 void refreshPortMirror(int hdmiPortNbr) {
629 if (hdmiPortNbr <= 0) {
630 throw new IllegalArgumentException("hdmiPortNbr must be greater than 0");
632 sendCommand(String.format(CMD_PORT_MIRROR_STATUS_FORMAT, hdmiPortNbr));
636 * Mutes/Unmutes the specified audio port.
638 * @param portNbr a greater than zero port number
639 * @param mute true to mute, false to unmute
641 void setVolumeMute(int portNbr, boolean mute) {
643 throw new IllegalArgumentException("portNbr must be greater than 0");
645 sendCommand(String.format(CMD_VOLUME_MUTE_FORMAT, portNbr, mute ? "on" : "off"));
649 * Refreshes the volume mute for the given audio port.
651 * @param portNbr a greater than zero port number
653 void refreshVolumeMute(int portNbr) {
655 throw new IllegalArgumentException("portNbr must be greater than 0");
657 sendCommand(String.format(CMD_VOLUME_MUTE_FORMAT, portNbr, "sta"));
661 * Turn on/off the front panel IR.
663 * @param on true for on, false otherwise
665 void setIrOn(boolean on) {
666 sendCommand(on ? CMD_IRON : CMD_IROFF);
670 * Refreshes the input port setting on the specified output port.
672 * @param portNbr a greater than zero port number
674 void refreshPortStatus(int portNbr) {
676 throw new IllegalArgumentException("portNbr must be greater than 0");
678 sendCommand(String.format(CMD_PORT_STATUS_FORMAT, portNbr));
682 * Refreshes all of the input port settings for all of the output ports.
684 private void refreshAllPortStatuses() {
685 sendCommand(CMD_PORT_STATUS);
689 * Saves the current Input/Output scheme to the specified preset number.
691 * @param presetNbr a greater than 0 preset number
693 void saveIoSettings(int presetNbr) {
694 if (presetNbr <= 0) {
695 throw new IllegalArgumentException("presetNbr must be greater than 0");
697 sendCommand(String.format(CMD_SAVEIO_FORMAT, presetNbr));
701 * Recalls the Input/Output scheme for the specified preset number.
703 * @param presetNbr a greater than 0 preset number
705 void recallIoSettings(int presetNbr) {
706 if (presetNbr <= 0) {
707 throw new IllegalArgumentException("presetNbr must be greater than 0");
709 sendCommand(String.format(CMD_RECALLIO_FORMAT, presetNbr));
713 * Clears the Input/Output scheme for the specified preset number.
715 * @param presetNbr a greater than 0 preset number
717 void clearIoSettings(int presetNbr) {
718 if (presetNbr <= 0) {
719 throw new IllegalArgumentException("presetNbr must be greater than 0");
721 sendCommand(String.format(CMD_CLEARIO_FORMAT, presetNbr));
725 * Resets the matrix back to defaults.
728 sendCommand(CMD_MATRIX_RESET);
732 * Sends the command and puts the thing into {@link ThingStatus#OFFLINE} if an IOException occurs
734 * @param command a non-null, non-empty command to send
736 private void sendCommand(String command) {
737 if (command == null) {
738 throw new IllegalArgumentException("command cannot be null");
740 if (command.trim().length() == 0) {
741 throw new IllegalArgumentException("command cannot be empty");
744 session.sendCommand(command);
745 } catch (IOException e) {
746 callback.statusChanged(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
747 "Exception occurred sending to Atlona: " + e);
752 * Handles the switch power response. The first matching group should be "on" or "off"
754 * @param m the non-null {@link Matcher} that matched the response
755 * @param resp the possibly null, possibly empty actual response
757 private void handlePowerResponse(Matcher m, String resp) {
759 throw new IllegalArgumentException("m (matcher) cannot be null");
761 if (m.groupCount() == 1) {
762 switch (m.group(1)) {
764 callback.stateChanged(AtlonaPro3Utilities.createChannelID(AtlonaPro3Constants.GROUP_PRIMARY,
765 AtlonaPro3Constants.CHANNEL_POWER), OnOffType.ON);
768 callback.stateChanged(AtlonaPro3Utilities.createChannelID(AtlonaPro3Constants.GROUP_PRIMARY,
769 AtlonaPro3Constants.CHANNEL_POWER), OnOffType.OFF);
772 logger.warn("Invalid power response: '{}'", resp);
775 logger.warn("Invalid power response: '{}'", resp);
780 * Handles the version (firmware) response. The first matching group should be the version
782 * @param m the non-null {@link Matcher} that matched the response
783 * @param resp the possibly null, possibly empty actual response
785 private void handleVersionResponse(Matcher m, String resp) {
787 throw new IllegalArgumentException("m (matcher) cannot be null");
789 if (m.groupCount() == 1) {
790 version = m.group(1);
791 callback.setProperty(AtlonaPro3Constants.PROPERTY_VERSION, version);
793 logger.warn("Invalid version response: '{}'", resp);
798 * Handles the type (model) response. The first matching group should be the type.
800 * @param m the non-null {@link Matcher} that matched the response
801 * @param resp the possibly null, possibly empty actual response
803 private void handleTypeResponse(Matcher m, String resp) {
805 throw new IllegalArgumentException("m (matcher) cannot be null");
807 if (m.groupCount() == 1) {
809 callback.setProperty(AtlonaPro3Constants.PROPERTY_TYPE, modelType);
811 logger.warn("Invalid Type response: '{}'", resp);
816 * Handles the panel lock response. The response is only on or off.
818 * @param resp the possibly null, possibly empty actual response
820 private void handlePanelLockResponse(String resp) {
821 callback.stateChanged(AtlonaPro3Utilities.createChannelID(AtlonaPro3Constants.GROUP_PRIMARY,
822 AtlonaPro3Constants.CHANNEL_PANELLOCK), OnOffType.from(RSP_LOCK.equals(resp)));
826 * Handles the port power response. The first two groups should be the port nbr and either "on" or "off"
828 * @param m the non-null {@link Matcher} that matched the response
829 * @param resp the possibly null, possibly empty actual response
831 private void handlePortPowerResponse(Matcher m, String resp) {
833 throw new IllegalArgumentException("m (matcher) cannot be null");
835 if (m.groupCount() == 2) {
837 int portNbr = Integer.parseInt(m.group(1));
838 switch (m.group(2)) {
840 callback.stateChanged(AtlonaPro3Utilities.createChannelID(AtlonaPro3Constants.GROUP_PORT,
841 portNbr, AtlonaPro3Constants.CHANNEL_PORTPOWER), OnOffType.ON);
844 callback.stateChanged(AtlonaPro3Utilities.createChannelID(AtlonaPro3Constants.GROUP_PORT,
845 portNbr, AtlonaPro3Constants.CHANNEL_PORTPOWER), OnOffType.OFF);
848 logger.warn("Invalid port power response: '{}'", resp);
850 } catch (NumberFormatException e) {
851 logger.warn("Invalid port power (can't parse number): '{}'", resp);
854 logger.warn("Invalid port power response: '{}'", resp);
859 * Handles the port all response. Simply calls {@link #refreshAllPortStatuses()}
861 * @param resp ignored
863 private void handlePortAllResponse(String resp) {
864 refreshAllPortStatuses();
868 * Handles the port output response. This matcher can have multiple groups separated by commas. Find each group and
869 * that group should have two groups within - an input port nbr and an output port number
871 * @param m the non-null {@link Matcher} that matched the response
872 * @param resp the possibly null, possibly empty actual response
874 private void handlePortOutputResponse(Matcher m, String resp) {
876 throw new IllegalArgumentException("m (matcher) cannot be null");
882 int inPort = Integer.parseInt(m.group(1));
883 int outPort = Integer.parseInt(m.group(2));
885 callback.stateChanged(AtlonaPro3Utilities.createChannelID(AtlonaPro3Constants.GROUP_PORT, outPort,
886 AtlonaPro3Constants.CHANNEL_PORTOUTPUT), new DecimalType(inPort));
887 } catch (NumberFormatException e) {
888 logger.warn("Invalid port output response (can't parse number): '{}'", resp);
894 * Handles the mirror response. The matcher should have two groups - an hdmi port number and an output port number.
896 * @param m the non-null {@link Matcher} that matched the response
897 * @param resp the possibly null, possibly empty actual response
899 private void handleMirrorResponse(Matcher m, String resp) {
901 throw new IllegalArgumentException("m (matcher) cannot be null");
903 if (m.groupCount() == 3) {
905 int hdmiPortNbr = Integer.parseInt(m.group(1));
907 // could be "off" (if mirror off), "on"/"Out" (with 3rd group representing out)
908 String oper = (m.group(2) == null ? "" : m.group(2).trim()).toLowerCase();
910 if ("off".equals(oper)) {
911 callback.stateChanged(AtlonaPro3Utilities.createChannelID(AtlonaPro3Constants.GROUP_MIRROR,
912 hdmiPortNbr, AtlonaPro3Constants.CHANNEL_PORTMIRRORENABLED), OnOffType.OFF);
914 int outPortNbr = Integer.parseInt(m.group(3));
915 callback.stateChanged(AtlonaPro3Utilities.createChannelID(AtlonaPro3Constants.GROUP_MIRROR,
916 hdmiPortNbr, AtlonaPro3Constants.CHANNEL_PORTMIRROR), new DecimalType(outPortNbr));
917 callback.stateChanged(AtlonaPro3Utilities.createChannelID(AtlonaPro3Constants.GROUP_MIRROR,
918 hdmiPortNbr, AtlonaPro3Constants.CHANNEL_PORTMIRRORENABLED), OnOffType.ON);
920 } catch (NumberFormatException e) {
921 logger.warn("Invalid mirror response (can't parse number): '{}'", resp);
924 logger.warn("Invalid mirror response: '{}'", resp);
929 * Handles the unmirror response. The first group should contain the hdmi port number
931 * @param m the non-null {@link Matcher} that matched the response
932 * @param resp the possibly null, possibly empty actual response
934 private void handleUnMirrorResponse(Matcher m, String resp) {
936 throw new IllegalArgumentException("m (matcher) cannot be null");
938 if (m.groupCount() == 1) {
940 int hdmiPortNbr = Integer.parseInt(m.group(1));
941 callback.stateChanged(AtlonaPro3Utilities.createChannelID(AtlonaPro3Constants.GROUP_MIRROR, hdmiPortNbr,
942 AtlonaPro3Constants.CHANNEL_PORTMIRROR), new DecimalType(0));
943 } catch (NumberFormatException e) {
944 logger.warn("Invalid unmirror response (can't parse number): '{}'", resp);
947 logger.warn("Invalid unmirror response: '{}'", resp);
952 * Handles the volume response. The first two group should be the audio port number and the level
954 * @param m the non-null {@link Matcher} that matched the response
955 * @param resp the possibly null, possibly empty actual response
957 private void handleVolumeResponse(Matcher m, String resp) {
959 throw new IllegalArgumentException("m (matcher) cannot be null");
961 if (m.groupCount() == 2) {
963 int portNbr = Integer.parseInt(m.group(1));
964 double level = Double.parseDouble(m.group(2));
965 callback.stateChanged(AtlonaPro3Utilities.createChannelID(AtlonaPro3Constants.GROUP_VOLUME, portNbr,
966 AtlonaPro3Constants.CHANNEL_VOLUME), new DecimalType(level));
967 } catch (NumberFormatException e) {
968 logger.warn("Invalid volume response (can't parse number): '{}'", resp);
971 logger.warn("Invalid volume response: '{}'", resp);
976 * Handles the volume mute response. The first two group should be the audio port number and either "on" or "off
978 * @param m the non-null {@link Matcher} that matched the response
979 * @param resp the possibly null, possibly empty actual response
981 private void handleVolumeMuteResponse(Matcher m, String resp) {
983 throw new IllegalArgumentException("m (matcher) cannot be null");
985 if (m.groupCount() == 2) {
987 int portNbr = Integer.parseInt(m.group(1));
988 switch (m.group(2)) {
990 callback.stateChanged(AtlonaPro3Utilities.createChannelID(AtlonaPro3Constants.GROUP_VOLUME,
991 portNbr, AtlonaPro3Constants.CHANNEL_VOLUME_MUTE), OnOffType.ON);
994 callback.stateChanged(AtlonaPro3Utilities.createChannelID(AtlonaPro3Constants.GROUP_VOLUME,
995 portNbr, AtlonaPro3Constants.CHANNEL_VOLUME_MUTE), OnOffType.OFF);
998 logger.warn("Invalid volume mute response: '{}'", resp);
1000 } catch (NumberFormatException e) {
1001 logger.warn("Invalid volume mute (can't parse number): '{}'", resp);
1004 logger.warn("Invalid volume mute response: '{}'", resp);
1009 * Handles the IR Response. The response is either on or off
1011 * @param resp the possibly null, possibly empty actual response
1013 private void handleIrLockResponse(String resp) {
1014 callback.stateChanged(AtlonaPro3Utilities.createChannelID(AtlonaPro3Constants.GROUP_PRIMARY,
1015 AtlonaPro3Constants.CHANNEL_IRENABLE), OnOffType.from(RSP_IRON.equals(resp)));
1019 * Handles the Save IO Response. Should have one group specifying the preset number
1021 * @param m the non-null {@link Matcher} that matched the response
1022 * @param resp the possibly null, possibly empty actual response
1024 private void handleSaveIoResponse(Matcher m, String resp) {
1025 // nothing to handle
1029 * Handles the Recall IO Response. Should have one group specifying the preset number. After updating the Recall
1030 * State, we refresh all the ports via {@link #refreshAllPortStatuses()}.
1032 * @param m the non-null {@link Matcher} that matched the response
1033 * @param resp the possibly null, possibly empty actual response
1035 private void handleRecallIoResponse(Matcher m, String resp) {
1036 refreshAllPortStatuses();
1040 * Handles the Clear IO Response. Should have one group specifying the preset number.
1042 * @param m the non-null {@link Matcher} that matched the response
1043 * @param resp the possibly null, possibly empty actual response
1045 private void handleClearIoResponse(Matcher m, String resp) {
1046 // nothing to handle
1050 * Handles the broadcast Response. Should have one group specifying the status.
1052 * @param m the non-null {@link Matcher} that matched the response
1053 * @param resp the possibly null, possibly empty actual response
1055 private void handleBroadcastResponse(Matcher m, String resp) {
1056 // nothing to handle
1060 * Handles the matrix reset response. The matrix will go offline immediately on a reset.
1062 * @param resp the possibly null, possibly empty actual response
1064 private void handleMatrixResetResponse(String resp) {
1065 if (RSP_MATRIX_RESET.equals(resp)) {
1066 callback.statusChanged(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
1067 "System is rebooting due to matrix reset");
1072 * Handles a command failure - we simply log the response as an error
1074 * @param resp the possibly null, possibly empty actual response
1076 private void handleCommandFailure(String resp) {
1077 logger.debug("{}", resp);
1081 * This callback is our normal response callback. Should be set into the {@link SocketSession} after the login
1082 * process to handle normal responses.
1084 * @author Tim Roberts
1087 private class NormalResponseCallback implements SocketSessionListener {
1090 public void responseReceived(String response) {
1091 if (response.isEmpty()) {
1095 if (RSP_PING.equals(response)) {
1102 m = portStatusPattern.matcher(response);
1104 handlePortOutputResponse(m, response);
1108 m = powerStatusPattern.matcher(response);
1110 handlePowerResponse(m, response);
1114 m = versionPattern.matcher(response);
1116 handleVersionResponse(m, response);
1120 m = versionHdPattern.matcher(response);
1121 if (!capabilities.isUHDModel() && m.matches()) {
1122 handleVersionResponse(m, response);
1126 m = typePattern.matcher(response);
1128 handleTypeResponse(m, response);
1132 m = typeHdPattern.matcher(response);
1134 handleTypeResponse(m, response);
1138 m = portPowerPattern.matcher(response);
1140 handlePortPowerResponse(m, response);
1144 m = volumePattern.matcher(response);
1146 handleVolumeResponse(m, response);
1150 m = volumeMutePattern.matcher(response);
1152 handleVolumeMuteResponse(m, response);
1156 m = portAllPattern.matcher(response);
1158 handlePortAllResponse(response);
1162 m = portMirrorPattern.matcher(response);
1164 handleMirrorResponse(m, response);
1168 m = portUnmirrorPattern.matcher(response);
1170 handleUnMirrorResponse(m, response);
1174 m = saveIoPattern.matcher(response);
1176 handleSaveIoResponse(m, response);
1180 m = recallIoPattern.matcher(response);
1182 handleRecallIoResponse(m, response);
1186 m = clearIoPattern.matcher(response);
1188 handleClearIoResponse(m, response);
1192 m = broadCastPattern.matcher(response);
1194 handleBroadcastResponse(m, response);
1198 if (RSP_IRON.equals(response) || RSP_IROFF.equals(response)) {
1199 handleIrLockResponse(response);
1203 if (RSP_ALL.equals(response)) {
1204 handlePortAllResponse(response);
1208 if (RSP_LOCK.equals(response) || RSP_UNLOCK.equals(response)) {
1209 handlePanelLockResponse(response);
1213 if (RSP_MATRIX_RESET.equals(response)) {
1214 handleMatrixResetResponse(response);
1218 if (response.startsWith(RSP_FAILED)) {
1219 handleCommandFailure(response);
1223 logger.debug("Unhandled response: {}", response);
1227 public void responseException(Exception e) {
1228 callback.statusChanged(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
1229 "Exception occurred reading from Atlona: " + e);
1234 * Special callback used during the login process to not dispatch the responses to this class but rather give them
1235 * back at each call to {@link NoDispatchingCallback#getResponse()}
1237 * @author Tim Roberts
1240 private class NoDispatchingCallback implements SocketSessionListener {
1243 * Cache of responses that have occurred
1245 private BlockingQueue<Object> responses = new ArrayBlockingQueue<>(5);
1248 * Will return the next response from {@link #responses}. If the response is an exception, that exception will
1249 * be thrown instead.
1251 * @return a non-null, possibly empty response
1252 * @throws Exception an exception if one occurred during reading
1254 String getResponse() throws Exception {
1255 final Object lastResponse = responses.poll(5, TimeUnit.SECONDS);
1256 if (lastResponse instanceof String stringResponse) {
1257 return stringResponse;
1258 } else if (lastResponse instanceof Exception exceptionResponse) {
1259 throw exceptionResponse;
1260 } else if (lastResponse == null) {
1261 throw new Exception("Didn't receive response in time");
1263 return lastResponse.toString();
1268 public void responseReceived(String response) {
1270 responses.put(response);
1271 } catch (InterruptedException e) {
1276 public void responseException(Exception e) {
1279 } catch (InterruptedException e1) {