2 * Copyright (c) 2010-2023 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.ScheduledFuture;
17 import java.util.concurrent.TimeUnit;
18 import java.util.regex.Matcher;
19 import java.util.regex.Pattern;
21 import org.openhab.binding.atlona.internal.AtlonaHandlerCallback;
22 import org.openhab.binding.atlona.internal.StatefulHandlerCallback;
23 import org.openhab.binding.atlona.internal.handler.AtlonaHandler;
24 import org.openhab.binding.atlona.internal.net.SocketChannelSession;
25 import org.openhab.binding.atlona.internal.net.SocketSession;
26 import org.openhab.core.library.types.DecimalType;
27 import org.openhab.core.library.types.OnOffType;
28 import org.openhab.core.library.types.StringType;
29 import org.openhab.core.thing.ChannelUID;
30 import org.openhab.core.thing.Thing;
31 import org.openhab.core.thing.ThingStatus;
32 import org.openhab.core.thing.ThingStatusDetail;
33 import org.openhab.core.types.Command;
34 import org.openhab.core.types.RefreshType;
35 import org.openhab.core.types.State;
36 import org.slf4j.Logger;
37 import org.slf4j.LoggerFactory;
40 * The {@link org.openhab.binding.atlona.internal.pro3.AtlonaPro3Handler} is responsible for handling commands, which
41 * are sent to one of the channels.
43 * @author Tim Roberts - Initial contribution
45 public class AtlonaPro3Handler extends AtlonaHandler<AtlonaPro3Capabilities> {
47 private final Logger logger = LoggerFactory.getLogger(AtlonaPro3Handler.class);
50 * The {@link AtlonaPro3PortocolHandler} protocol handler
52 private AtlonaPro3PortocolHandler atlonaHandler;
55 * The {@link SocketSession} telnet session to the switch. Will be null if not connected.
57 private SocketSession session;
60 * The polling job to poll the actual state from the {@link #session}
62 private ScheduledFuture<?> polling;
65 * The retry connection event
67 private ScheduledFuture<?> retryConnection;
72 private ScheduledFuture<?> ping;
74 // List of all the groups patterns we recognize
75 private static final Pattern GROUP_PRIMARY_PATTERN = Pattern.compile("^" + AtlonaPro3Constants.GROUP_PRIMARY + "$");
76 private static final Pattern GROUP_PORT_PATTERN = Pattern
77 .compile("^" + AtlonaPro3Constants.GROUP_PORT + "(\\d{1,2})$");
78 private static final Pattern GROUP_MIRROR_PATTERN = Pattern
79 .compile("^" + AtlonaPro3Constants.GROUP_MIRROR + "(\\d{1,2})$");
80 private static final Pattern GROUP_VOLUME_PATTERN = Pattern
81 .compile("^" + AtlonaPro3Constants.GROUP_VOLUME + "(\\d{1,2})$");
83 // List of preset commands we recognize
84 private static final Pattern CMD_PRESETSAVE = Pattern
85 .compile("^" + AtlonaPro3Constants.CMD_PRESETSAVE + "(\\d{1,2})$");
86 private static final Pattern CMD_PRESETRECALL = Pattern
87 .compile("^" + AtlonaPro3Constants.CMD_PRESETRECALL + "(\\d{1,2})$");
88 private static final Pattern CMD_PRESETCLEAR = Pattern
89 .compile("^" + AtlonaPro3Constants.CMD_PRESETCLEAR + "(\\d{1,2})$");
91 // List of matrix commands we recognize
92 private static final Pattern CMD_MATRIXRESET = Pattern.compile("^" + AtlonaPro3Constants.CMD_MATRIXRESET + "$");
93 private static final Pattern CMD_MATRIXRESETPORTS = Pattern
94 .compile("^" + AtlonaPro3Constants.CMD_MATRIXRESETPORTS + "$");
95 private static final Pattern CMD_MATRIXPORTALL = Pattern
96 .compile("^" + AtlonaPro3Constants.CMD_MATRIXPORTALL + "(\\d{1,2})$");
99 * Constructs the handler from the {@link org.openhab.core.thing.Thing} with the number of power ports and
100 * audio ports the switch supports.
102 * @param thing a non-null {@link org.openhab.core.thing.Thing} the handler is for
103 * @param capabilities a non-null {@link org.openhab.binding.atlona.internal.pro3.AtlonaPro3Capabilities}
105 public AtlonaPro3Handler(Thing thing, AtlonaPro3Capabilities capabilities) {
106 super(thing, capabilities);
109 throw new IllegalArgumentException("thing cannot be null");
116 * Handles commands to specific channels. This implementation will offload much of its work to the
117 * {@link AtlonaPro3PortocolHandler}. Basically we validate the type of command for the channel then call the
118 * {@link AtlonaPro3PortocolHandler} to handle the actual protocol. Special use case is the {@link RefreshType}
119 * where we call {{@link #handleRefresh(String)} to handle a refresh of the specific channel (which in turn calls
120 * {@link AtlonaPro3PortocolHandler} to handle the actual refresh
123 public void handleCommand(ChannelUID channelUID, Command command) {
124 if (command instanceof RefreshType) {
125 handleRefresh(channelUID);
129 final String group = channelUID.getGroupId().toLowerCase();
130 final String id = channelUID.getIdWithoutGroup().toLowerCase();
133 if ((m = GROUP_PRIMARY_PATTERN.matcher(group)).matches()) {
135 case AtlonaPro3Constants.CHANNEL_POWER:
136 if (command instanceof OnOffType onOffCommand) {
137 final boolean makeOn = onOffCommand == OnOffType.ON;
138 atlonaHandler.setPower(makeOn);
140 logger.debug("Received a POWER channel command with a non OnOffType: {}", command);
145 case AtlonaPro3Constants.CHANNEL_PANELLOCK:
146 if (command instanceof OnOffType onOffCommand) {
147 final boolean makeOn = onOffCommand == OnOffType.ON;
148 atlonaHandler.setPanelLock(makeOn);
150 logger.debug("Received a PANELLOCK channel command with a non OnOffType: {}", command);
154 case AtlonaPro3Constants.CHANNEL_IRENABLE:
155 if (command instanceof OnOffType onOffCommand) {
156 final boolean makeOn = onOffCommand == OnOffType.ON;
157 atlonaHandler.setIrOn(makeOn);
159 logger.debug("Received an IRLOCK channel command with a non OnOffType: {}", command);
163 case AtlonaPro3Constants.CHANNEL_MATRIXCMDS:
164 if (command instanceof StringType) {
165 final String matrixCmd = command.toString();
168 if ((cmd = CMD_MATRIXRESET.matcher(matrixCmd)).matches()) {
169 atlonaHandler.resetMatrix();
170 } else if ((cmd = CMD_MATRIXRESETPORTS.matcher(matrixCmd)).matches()) {
171 atlonaHandler.resetAllPorts();
172 } else if ((cmd = CMD_MATRIXPORTALL.matcher(matrixCmd)).matches()) {
173 if (cmd.groupCount() == 1) {
174 final int portNbr = Integer.parseInt(cmd.group(1));
175 atlonaHandler.setPortAll(portNbr);
177 logger.debug("Unknown matirx set port command: '{}'", matrixCmd);
181 logger.debug("Unknown matrix command: '{}'", cmd);
183 } catch (NumberFormatException e) {
184 logger.debug("Could not parse the port number from the command: '{}'", matrixCmd);
188 case AtlonaPro3Constants.CHANNEL_PRESETCMDS:
189 if (command instanceof StringType) {
190 final String presetCmd = command.toString();
193 if ((cmd = CMD_PRESETSAVE.matcher(presetCmd)).matches()) {
194 if (cmd.groupCount() == 1) {
195 final int presetNbr = Integer.parseInt(cmd.group(1));
196 atlonaHandler.saveIoSettings(presetNbr);
198 logger.debug("Unknown preset save command: '{}'", presetCmd);
200 } else if ((cmd = CMD_PRESETRECALL.matcher(presetCmd)).matches()) {
201 if (cmd.groupCount() == 1) {
202 final int presetNbr = Integer.parseInt(cmd.group(1));
203 atlonaHandler.recallIoSettings(presetNbr);
205 logger.debug("Unknown preset recall command: '{}'", presetCmd);
207 } else if ((cmd = CMD_PRESETCLEAR.matcher(presetCmd)).matches()) {
208 if (cmd.groupCount() == 1) {
209 final int presetNbr = Integer.parseInt(cmd.group(1));
210 atlonaHandler.clearIoSettings(presetNbr);
212 logger.debug("Unknown preset clear command: '{}'", presetCmd);
216 logger.debug("Unknown preset command: '{}'", cmd);
218 } catch (NumberFormatException e) {
219 logger.debug("Could not parse the preset number from the command: '{}'", presetCmd);
225 logger.debug("Unknown/Unsupported Primary Channel: {}", channelUID.getAsString());
228 } else if ((m = GROUP_PORT_PATTERN.matcher(group)).matches()) {
229 if (m.groupCount() == 1) {
231 final int portNbr = Integer.parseInt(m.group(1));
234 case AtlonaPro3Constants.CHANNEL_PORTOUTPUT:
235 if (command instanceof DecimalType decimalCommand) {
236 final int inpNbr = decimalCommand.intValue();
237 atlonaHandler.setPortSwitch(inpNbr, portNbr);
239 logger.debug("Received a PORTOUTPUT channel command with a non DecimalType: {}",
245 case AtlonaPro3Constants.CHANNEL_PORTPOWER:
246 if (command instanceof OnOffType onOffCommand) {
247 final boolean makeOn = onOffCommand == OnOffType.ON;
248 atlonaHandler.setPortPower(portNbr, makeOn);
250 logger.debug("Received a PORTPOWER channel command with a non OnOffType: {}", command);
254 logger.debug("Unknown/Unsupported Port Channel: {}", channelUID.getAsString());
257 } catch (NumberFormatException e) {
258 logger.debug("Bad Port Channel (can't parse the port nbr): {}", channelUID.getAsString());
261 } else if ((m = GROUP_MIRROR_PATTERN.matcher(group)).matches()) {
262 if (m.groupCount() == 1) {
264 final int hdmiPortNbr = Integer.parseInt(m.group(1));
267 case AtlonaPro3Constants.CHANNEL_PORTMIRROR:
268 if (command instanceof DecimalType decimalCommand) {
269 final int outPortNbr = decimalCommand.intValue();
270 if (outPortNbr <= 0) {
271 atlonaHandler.removePortMirror(hdmiPortNbr);
273 atlonaHandler.setPortMirror(hdmiPortNbr, outPortNbr);
276 logger.debug("Received a PORTMIRROR channel command with a non DecimalType: {}",
281 case AtlonaPro3Constants.CHANNEL_PORTMIRRORENABLED:
282 if (command instanceof OnOffType) {
283 if (command == OnOffType.ON) {
284 final StatefulHandlerCallback callback = (StatefulHandlerCallback) atlonaHandler
286 final State state = callback.getState(AtlonaPro3Constants.CHANNEL_PORTMIRROR);
288 if (state instanceof DecimalType decimalCommand) {
289 outPortNbr = decimalCommand.intValue();
291 atlonaHandler.setPortMirror(hdmiPortNbr, outPortNbr);
293 atlonaHandler.removePortMirror(hdmiPortNbr);
296 logger.debug("Received a PORTMIRROR channel command with a non DecimalType: {}",
302 logger.debug("Unknown/Unsupported Mirror Channel: {}", channelUID.getAsString());
305 } catch (NumberFormatException e) {
306 logger.debug("Bad Mirror Channel (can't parse the port nbr): {}", channelUID.getAsString());
309 } else if ((m = GROUP_VOLUME_PATTERN.matcher(group)).matches()) {
310 if (m.groupCount() == 1) {
312 final int portNbr = Integer.parseInt(m.group(1));
315 case AtlonaPro3Constants.CHANNEL_VOLUME_MUTE:
316 if (command instanceof OnOffType onOffCommand) {
317 atlonaHandler.setVolumeMute(portNbr, onOffCommand == OnOffType.ON);
319 logger.debug("Received a VOLUME MUTE channel command with a non OnOffType: {}",
324 case AtlonaPro3Constants.CHANNEL_VOLUME:
325 if (command instanceof DecimalType decimalCommand) {
326 final int level = decimalCommand.intValue();
327 atlonaHandler.setVolume(portNbr, level);
329 logger.debug("Received a VOLUME channel command with a non DecimalType: {}", command);
334 logger.debug("Unknown/Unsupported Volume Channel: {}", channelUID.getAsString());
337 } catch (NumberFormatException e) {
338 logger.debug("Bad Volume Channel (can't parse the port nbr): {}", channelUID.getAsString());
342 logger.debug("Unknown/Unsupported Channel: {}", channelUID.getAsString());
347 * Method that handles the {@link RefreshType} command specifically. Calls the {@link AtlonaPro3PortocolHandler} to
348 * handle the actual refresh based on the channel id.
350 * @param id a non-null, possibly empty channel id to refresh
352 private void handleRefresh(ChannelUID channelUID) {
353 if (getThing().getStatus() != ThingStatus.ONLINE) {
357 final String group = channelUID.getGroupId().toLowerCase();
358 final String id = channelUID.getIdWithoutGroup().toLowerCase();
359 final StatefulHandlerCallback callback = (StatefulHandlerCallback) atlonaHandler.getCallback();
362 if ((m = GROUP_PRIMARY_PATTERN.matcher(group)).matches()) {
364 case AtlonaPro3Constants.CHANNEL_POWER:
365 callback.removeState(AtlonaPro3Utilities.createChannelID(group, id));
366 atlonaHandler.refreshPower();
373 } else if ((m = GROUP_PORT_PATTERN.matcher(group)).matches()) {
374 if (m.groupCount() == 1) {
376 final int portNbr = Integer.parseInt(m.group(1));
377 callback.removeState(AtlonaPro3Utilities.createChannelID(group, portNbr, id));
380 case AtlonaPro3Constants.CHANNEL_PORTOUTPUT:
381 atlonaHandler.refreshPortStatus(portNbr);
384 case AtlonaPro3Constants.CHANNEL_PORTPOWER:
385 atlonaHandler.refreshPortPower(portNbr);
390 } catch (NumberFormatException e) {
391 logger.debug("Bad Port Channel (can't parse the port nbr): {}", channelUID.getAsString());
395 } else if ((m = GROUP_MIRROR_PATTERN.matcher(group)).matches()) {
396 if (m.groupCount() == 1) {
398 final int hdmiPortNbr = Integer.parseInt(m.group(1));
399 callback.removeState(AtlonaPro3Utilities.createChannelID(group, hdmiPortNbr, id));
400 atlonaHandler.refreshPortMirror(hdmiPortNbr);
401 } catch (NumberFormatException e) {
402 logger.debug("Bad Mirror Channel (can't parse the port nbr): {}", channelUID.getAsString());
406 } else if ((m = GROUP_VOLUME_PATTERN.matcher(group)).matches()) {
407 if (m.groupCount() == 1) {
409 final int portNbr = Integer.parseInt(m.group(1));
410 callback.removeState(AtlonaPro3Utilities.createChannelID(group, portNbr, id));
413 case AtlonaPro3Constants.CHANNEL_VOLUME_MUTE:
414 atlonaHandler.refreshVolumeMute(portNbr);
416 case AtlonaPro3Constants.CHANNEL_VOLUME:
417 atlonaHandler.refreshVolumeStatus(portNbr);
423 } catch (NumberFormatException e) {
424 logger.debug("Bad Volume Channel (can't parse the port nbr): {}", channelUID.getAsString());
434 * Initializes the handler. This initialization will read/validate the configuration, then will create the
435 * {@link SocketSession}, initialize the {@link AtlonaPro3PortocolHandler} and will attempt to connect to the switch
436 * (via {{@link #retryConnect()}.
439 public void initialize() {
440 final AtlonaPro3Config config = getAtlonaConfig();
442 if (config == null) {
446 if (config.getIpAddress() == null || config.getIpAddress().trim().length() == 0) {
447 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
448 "IP Address of Atlona Pro3 is missing from configuration");
452 session = new SocketChannelSession(getThing().getUID().getAsString(), config.getIpAddress(), 23);
453 atlonaHandler = new AtlonaPro3PortocolHandler(session, config, getCapabilities(),
454 new StatefulHandlerCallback(new AtlonaHandlerCallback() {
456 public void stateChanged(String channelId, State state) {
457 updateState(channelId, state);
461 public void statusChanged(ThingStatus status, ThingStatusDetail detail, String msg) {
462 updateStatus(status, detail, msg);
464 if (status != ThingStatus.ONLINE) {
470 public void setProperty(String propertyName, String propertyValue) {
471 getThing().setProperty(propertyName, propertyValue);
475 // Try initial connection in a scheduled task
476 this.scheduler.schedule(this::connect, 1, TimeUnit.SECONDS);
480 * Attempts to connect to the switch. If successfully connect, the {@link AtlonaPro3PortocolHandler#login()} will be
481 * called to log into the switch (if needed). Once completed, a polling job will be created to poll the switch's
482 * actual state and a ping job to ping the server. If a connection cannot be established (or login failed), the
483 * connection attempt will be retried later (via {@link #retryConnect()})
485 private void connect() {
486 String response = "Server is offline - will try to reconnect later";
488 // clear listeners to avoid any 'old' listener from handling initial messages
489 session.clearListeners();
492 if (this.getCapabilities().isUHDModel()) {
493 response = atlonaHandler.loginUHD();
495 response = atlonaHandler.loginHD();
498 if (response == null) {
499 final AtlonaPro3Config config = getAtlonaConfig();
500 if (config != null) {
501 polling = this.scheduler.scheduleWithFixedDelay(() -> {
502 final ThingStatus status = getThing().getStatus();
503 if (status == ThingStatus.ONLINE) {
504 if (session.isConnected()) {
505 atlonaHandler.refreshAll();
507 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
508 "Atlona PRO3 has disconnected. Will try to reconnect later.");
510 } else if (status == ThingStatus.OFFLINE) {
513 }, config.getPolling(), config.getPolling(), TimeUnit.SECONDS);
515 ping = this.scheduler.scheduleWithFixedDelay(() -> {
516 final ThingStatus status = getThing().getStatus();
517 if (status == ThingStatus.ONLINE) {
518 if (session.isConnected()) {
519 atlonaHandler.ping();
522 }, config.getPing(), config.getPing(), TimeUnit.SECONDS);
524 updateStatus(ThingStatus.ONLINE);
529 } catch (Exception e) {
533 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, response);
538 * Attempts to disconnect from the session and will optionally retry the connection attempt. The {@link #polling}
539 * will be cancelled, the {@link #ping} will be cancelled and both set to null then the {@link #session} will be
542 * @param retryConnection true to retry connection attempts after the disconnect
544 private void disconnect(boolean retryConnection) {
546 if (polling != null) {
547 polling.cancel(true);
557 if (session != null) {
559 session.disconnect();
560 } catch (IOException e) {
561 // ignore - we don't care
565 if (retryConnection) {
571 * Retries the connection attempt - schedules a job in {@link AtlonaPro3Config#getRetryPolling()} seconds to call
573 * {@link #connect()} method. If a retry attempt is pending, the request is ignored.
575 private void retryConnect() {
576 if (retryConnection == null) {
577 final AtlonaPro3Config config = getAtlonaConfig();
578 if (config != null) {
579 logger.info("Will try to reconnect in {} seconds", config.getRetryPolling());
580 retryConnection = this.scheduler.schedule(() -> {
581 retryConnection = null;
583 }, config.getRetryPolling(), TimeUnit.SECONDS);
586 logger.debug("RetryConnection called when a retry connection is pending - ignoring request");
591 * Simple gets the {@link AtlonaPro3Config} from the {@link Thing} and will set the status to offline if not found.
593 * @return {@link AtlonaPro3Config}
595 private AtlonaPro3Config getAtlonaConfig() {
596 return getThing().getConfiguration().as(AtlonaPro3Config.class);
602 * Disposes of the handler. Will simply call {@link #disconnect(boolean)} to disconnect and NOT retry the
606 public void dispose() {