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.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(ChannelUID)} to handle a refresh of the specific channel (which in turn
121 * {@link AtlonaPro3PortocolHandler} to handle the actual refresh
124 public void handleCommand(ChannelUID channelUID, Command command) {
125 if (command instanceof RefreshType) {
126 handleRefresh(channelUID);
130 final String group = channelUID.getGroupId().toLowerCase();
131 final String id = channelUID.getIdWithoutGroup().toLowerCase();
134 if ((m = GROUP_PRIMARY_PATTERN.matcher(group)).matches()) {
136 case AtlonaPro3Constants.CHANNEL_POWER:
137 if (command instanceof OnOffType onOffCommand) {
138 final boolean makeOn = onOffCommand == OnOffType.ON;
139 atlonaHandler.setPower(makeOn);
141 logger.debug("Received a POWER channel command with a non OnOffType: {}", command);
146 case AtlonaPro3Constants.CHANNEL_PANELLOCK:
147 if (command instanceof OnOffType onOffCommand) {
148 final boolean makeOn = onOffCommand == OnOffType.ON;
149 atlonaHandler.setPanelLock(makeOn);
151 logger.debug("Received a PANELLOCK channel command with a non OnOffType: {}", command);
155 case AtlonaPro3Constants.CHANNEL_IRENABLE:
156 if (command instanceof OnOffType onOffCommand) {
157 final boolean makeOn = onOffCommand == OnOffType.ON;
158 atlonaHandler.setIrOn(makeOn);
160 logger.debug("Received an IRLOCK channel command with a non OnOffType: {}", command);
164 case AtlonaPro3Constants.CHANNEL_MATRIXCMDS:
165 if (command instanceof StringType) {
166 final String matrixCmd = command.toString();
169 if ((cmd = CMD_MATRIXRESET.matcher(matrixCmd)).matches()) {
170 atlonaHandler.resetMatrix();
171 } else if ((cmd = CMD_MATRIXRESETPORTS.matcher(matrixCmd)).matches()) {
172 atlonaHandler.resetAllPorts();
173 } else if ((cmd = CMD_MATRIXPORTALL.matcher(matrixCmd)).matches()) {
174 if (cmd.groupCount() == 1) {
175 final int portNbr = Integer.parseInt(cmd.group(1));
176 atlonaHandler.setPortAll(portNbr);
178 logger.debug("Unknown matirx set port command: '{}'", matrixCmd);
182 logger.debug("Unknown matrix command: '{}'", cmd);
184 } catch (NumberFormatException e) {
185 logger.debug("Could not parse the port number from the command: '{}'", matrixCmd);
189 case AtlonaPro3Constants.CHANNEL_PRESETCMDS:
190 if (command instanceof StringType) {
191 final String presetCmd = command.toString();
194 if ((cmd = CMD_PRESETSAVE.matcher(presetCmd)).matches()) {
195 if (cmd.groupCount() == 1) {
196 final int presetNbr = Integer.parseInt(cmd.group(1));
197 atlonaHandler.saveIoSettings(presetNbr);
199 logger.debug("Unknown preset save command: '{}'", presetCmd);
201 } else if ((cmd = CMD_PRESETRECALL.matcher(presetCmd)).matches()) {
202 if (cmd.groupCount() == 1) {
203 final int presetNbr = Integer.parseInt(cmd.group(1));
204 atlonaHandler.recallIoSettings(presetNbr);
206 logger.debug("Unknown preset recall command: '{}'", presetCmd);
208 } else if ((cmd = CMD_PRESETCLEAR.matcher(presetCmd)).matches()) {
209 if (cmd.groupCount() == 1) {
210 final int presetNbr = Integer.parseInt(cmd.group(1));
211 atlonaHandler.clearIoSettings(presetNbr);
213 logger.debug("Unknown preset clear command: '{}'", presetCmd);
217 logger.debug("Unknown preset command: '{}'", cmd);
219 } catch (NumberFormatException e) {
220 logger.debug("Could not parse the preset number from the command: '{}'", presetCmd);
226 logger.debug("Unknown/Unsupported Primary Channel: {}", channelUID.getAsString());
229 } else if ((m = GROUP_PORT_PATTERN.matcher(group)).matches()) {
230 if (m.groupCount() == 1) {
232 final int portNbr = Integer.parseInt(m.group(1));
235 case AtlonaPro3Constants.CHANNEL_PORTOUTPUT:
236 if (command instanceof DecimalType decimalCommand) {
237 final int inpNbr = decimalCommand.intValue();
238 atlonaHandler.setPortSwitch(inpNbr, portNbr);
240 logger.debug("Received a PORTOUTPUT channel command with a non DecimalType: {}",
246 case AtlonaPro3Constants.CHANNEL_PORTPOWER:
247 if (command instanceof OnOffType onOffCommand) {
248 final boolean makeOn = onOffCommand == OnOffType.ON;
249 atlonaHandler.setPortPower(portNbr, makeOn);
251 logger.debug("Received a PORTPOWER channel command with a non OnOffType: {}", command);
255 logger.debug("Unknown/Unsupported Port Channel: {}", channelUID.getAsString());
258 } catch (NumberFormatException e) {
259 logger.debug("Bad Port Channel (can't parse the port nbr): {}", channelUID.getAsString());
262 } else if ((m = GROUP_MIRROR_PATTERN.matcher(group)).matches()) {
263 if (m.groupCount() == 1) {
265 final int hdmiPortNbr = Integer.parseInt(m.group(1));
268 case AtlonaPro3Constants.CHANNEL_PORTMIRROR:
269 if (command instanceof DecimalType decimalCommand) {
270 final int outPortNbr = decimalCommand.intValue();
271 if (outPortNbr <= 0) {
272 atlonaHandler.removePortMirror(hdmiPortNbr);
274 atlonaHandler.setPortMirror(hdmiPortNbr, outPortNbr);
277 logger.debug("Received a PORTMIRROR channel command with a non DecimalType: {}",
282 case AtlonaPro3Constants.CHANNEL_PORTMIRRORENABLED:
283 if (command instanceof OnOffType) {
284 if (command == OnOffType.ON) {
285 final StatefulHandlerCallback callback = (StatefulHandlerCallback) atlonaHandler
287 final State state = callback.getState(AtlonaPro3Constants.CHANNEL_PORTMIRROR);
289 if (state instanceof DecimalType decimalCommand) {
290 outPortNbr = decimalCommand.intValue();
292 atlonaHandler.setPortMirror(hdmiPortNbr, outPortNbr);
294 atlonaHandler.removePortMirror(hdmiPortNbr);
297 logger.debug("Received a PORTMIRROR channel command with a non DecimalType: {}",
303 logger.debug("Unknown/Unsupported Mirror Channel: {}", channelUID.getAsString());
306 } catch (NumberFormatException e) {
307 logger.debug("Bad Mirror Channel (can't parse the port nbr): {}", channelUID.getAsString());
310 } else if ((m = GROUP_VOLUME_PATTERN.matcher(group)).matches()) {
311 if (m.groupCount() == 1) {
313 final int portNbr = Integer.parseInt(m.group(1));
316 case AtlonaPro3Constants.CHANNEL_VOLUME_MUTE:
317 if (command instanceof OnOffType onOffCommand) {
318 atlonaHandler.setVolumeMute(portNbr, onOffCommand == OnOffType.ON);
320 logger.debug("Received a VOLUME MUTE channel command with a non OnOffType: {}",
325 case AtlonaPro3Constants.CHANNEL_VOLUME:
326 if (command instanceof DecimalType decimalCommand) {
327 final int level = decimalCommand.intValue();
328 atlonaHandler.setVolume(portNbr, level);
330 logger.debug("Received a VOLUME channel command with a non DecimalType: {}", command);
335 logger.debug("Unknown/Unsupported Volume Channel: {}", channelUID.getAsString());
338 } catch (NumberFormatException e) {
339 logger.debug("Bad Volume Channel (can't parse the port nbr): {}", channelUID.getAsString());
343 logger.debug("Unknown/Unsupported Channel: {}", channelUID.getAsString());
348 * Method that handles the {@link RefreshType} command specifically. Calls the {@link AtlonaPro3PortocolHandler} to
349 * handle the actual refresh based on the channel id.
351 * @param id a non-null, possibly empty channel id to refresh
353 private void handleRefresh(ChannelUID channelUID) {
354 if (getThing().getStatus() != ThingStatus.ONLINE) {
358 final String group = channelUID.getGroupId().toLowerCase();
359 final String id = channelUID.getIdWithoutGroup().toLowerCase();
360 final StatefulHandlerCallback callback = (StatefulHandlerCallback) atlonaHandler.getCallback();
363 if ((m = GROUP_PRIMARY_PATTERN.matcher(group)).matches()) {
365 case AtlonaPro3Constants.CHANNEL_POWER:
366 callback.removeState(AtlonaPro3Utilities.createChannelID(group, id));
367 atlonaHandler.refreshPower();
374 } else if ((m = GROUP_PORT_PATTERN.matcher(group)).matches()) {
375 if (m.groupCount() == 1) {
377 final int portNbr = Integer.parseInt(m.group(1));
378 callback.removeState(AtlonaPro3Utilities.createChannelID(group, portNbr, id));
381 case AtlonaPro3Constants.CHANNEL_PORTOUTPUT:
382 atlonaHandler.refreshPortStatus(portNbr);
385 case AtlonaPro3Constants.CHANNEL_PORTPOWER:
386 atlonaHandler.refreshPortPower(portNbr);
391 } catch (NumberFormatException e) {
392 logger.debug("Bad Port Channel (can't parse the port nbr): {}", channelUID.getAsString());
396 } else if ((m = GROUP_MIRROR_PATTERN.matcher(group)).matches()) {
397 if (m.groupCount() == 1) {
399 final int hdmiPortNbr = Integer.parseInt(m.group(1));
400 callback.removeState(AtlonaPro3Utilities.createChannelID(group, hdmiPortNbr, id));
401 atlonaHandler.refreshPortMirror(hdmiPortNbr);
402 } catch (NumberFormatException e) {
403 logger.debug("Bad Mirror Channel (can't parse the port nbr): {}", channelUID.getAsString());
407 } else if ((m = GROUP_VOLUME_PATTERN.matcher(group)).matches()) {
408 if (m.groupCount() == 1) {
410 final int portNbr = Integer.parseInt(m.group(1));
411 callback.removeState(AtlonaPro3Utilities.createChannelID(group, portNbr, id));
414 case AtlonaPro3Constants.CHANNEL_VOLUME_MUTE:
415 atlonaHandler.refreshVolumeMute(portNbr);
417 case AtlonaPro3Constants.CHANNEL_VOLUME:
418 atlonaHandler.refreshVolumeStatus(portNbr);
424 } catch (NumberFormatException e) {
425 logger.debug("Bad Volume Channel (can't parse the port nbr): {}", channelUID.getAsString());
435 * Initializes the handler. This initialization will read/validate the configuration, then will create the
436 * {@link SocketSession}, initialize the {@link AtlonaPro3PortocolHandler} and will attempt to connect to the switch
437 * (via {{@link #retryConnect()}.
440 public void initialize() {
441 final AtlonaPro3Config config = getAtlonaConfig();
443 if (config == null) {
447 if (config.getIpAddress() == null || config.getIpAddress().trim().length() == 0) {
448 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
449 "IP Address of Atlona Pro3 is missing from configuration");
453 session = new SocketChannelSession(getThing().getUID().getAsString(), config.getIpAddress(), 23);
454 atlonaHandler = new AtlonaPro3PortocolHandler(session, config, getCapabilities(),
455 new StatefulHandlerCallback(new AtlonaHandlerCallback() {
457 public void stateChanged(String channelId, State state) {
458 updateState(channelId, state);
462 public void statusChanged(ThingStatus status, ThingStatusDetail detail, String msg) {
463 updateStatus(status, detail, msg);
465 if (status != ThingStatus.ONLINE) {
471 public void setProperty(String propertyName, String propertyValue) {
472 getThing().setProperty(propertyName, propertyValue);
476 // Try initial connection in a scheduled task
477 this.scheduler.schedule(this::connect, 1, TimeUnit.SECONDS);
481 * Attempts to connect to the switch. If successfully connect, the {@link AtlonaPro3PortocolHandler#login()} will be
482 * called to log into the switch (if needed). Once completed, a polling job will be created to poll the switch's
483 * actual state and a ping job to ping the server. If a connection cannot be established (or login failed), the
484 * connection attempt will be retried later (via {@link #retryConnect()})
486 private void connect() {
487 String response = "Server is offline - will try to reconnect later";
489 // clear listeners to avoid any 'old' listener from handling initial messages
490 session.clearListeners();
493 if (this.getCapabilities().isUHDModel()) {
494 response = atlonaHandler.loginUHD();
496 response = atlonaHandler.loginHD();
499 if (response == null) {
500 final AtlonaPro3Config config = getAtlonaConfig();
501 if (config != null) {
502 polling = this.scheduler.scheduleWithFixedDelay(() -> {
503 final ThingStatus status = getThing().getStatus();
504 if (status == ThingStatus.ONLINE) {
505 if (session.isConnected()) {
506 atlonaHandler.refreshAll();
508 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
509 "Atlona PRO3 has disconnected. Will try to reconnect later.");
511 } else if (status == ThingStatus.OFFLINE) {
514 }, config.getPolling(), config.getPolling(), TimeUnit.SECONDS);
516 ping = this.scheduler.scheduleWithFixedDelay(() -> {
517 final ThingStatus status = getThing().getStatus();
518 if (status == ThingStatus.ONLINE) {
519 if (session.isConnected()) {
520 atlonaHandler.ping();
523 }, config.getPing(), config.getPing(), TimeUnit.SECONDS);
525 updateStatus(ThingStatus.ONLINE);
530 } catch (Exception e) {
534 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, response);
539 * Attempts to disconnect from the session and will optionally retry the connection attempt. The {@link #polling}
540 * will be cancelled, the {@link #ping} will be cancelled and both set to null then the {@link #session} will be
543 * @param retryConnection true to retry connection attempts after the disconnect
545 private void disconnect(boolean retryConnection) {
547 if (polling != null) {
548 polling.cancel(true);
558 if (session != null) {
560 session.disconnect();
561 } catch (IOException e) {
562 // ignore - we don't care
566 if (retryConnection) {
572 * Retries the connection attempt - schedules a job in {@link AtlonaPro3Config#getRetryPolling()} seconds to call
574 * {@link #connect()} method. If a retry attempt is pending, the request is ignored.
576 private void retryConnect() {
577 if (retryConnection == null) {
578 final AtlonaPro3Config config = getAtlonaConfig();
579 if (config != null) {
580 logger.info("Will try to reconnect in {} seconds", config.getRetryPolling());
581 retryConnection = this.scheduler.schedule(() -> {
582 retryConnection = null;
584 }, config.getRetryPolling(), TimeUnit.SECONDS);
587 logger.debug("RetryConnection called when a retry connection is pending - ignoring request");
592 * Simple gets the {@link AtlonaPro3Config} from the {@link Thing} and will set the status to offline if not found.
594 * @return {@link AtlonaPro3Config}
596 private AtlonaPro3Config getAtlonaConfig() {
597 return getThing().getConfiguration().as(AtlonaPro3Config.class);
603 * Disposes of the handler. Will simply call {@link #disconnect(boolean)} to disconnect and NOT retry the
607 public void dispose() {