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.heos.internal.api;
15 import static org.openhab.binding.heos.internal.handler.FutureUtil.cancel;
17 import java.beans.PropertyChangeListener;
18 import java.io.IOException;
19 import java.util.concurrent.*;
20 import java.util.function.Consumer;
22 import org.eclipse.jdt.annotation.NonNullByDefault;
23 import org.eclipse.jdt.annotation.Nullable;
24 import org.openhab.binding.heos.internal.json.HeosJsonParser;
25 import org.openhab.binding.heos.internal.json.dto.HeosResponseObject;
26 import org.openhab.binding.heos.internal.resources.HeosCommands;
27 import org.openhab.binding.heos.internal.resources.HeosSendCommand;
28 import org.openhab.binding.heos.internal.resources.Telnet;
29 import org.openhab.binding.heos.internal.resources.Telnet.ReadException;
30 import org.slf4j.Logger;
31 import org.slf4j.LoggerFactory;
33 import com.google.gson.JsonSyntaxException;
36 * The {@link HeosSystem} is handling the main commands, which are
37 * sent and received by the HEOS system.
39 * @author Johannes Einig - Initial contribution
42 public class HeosSystem {
43 private final Logger logger = LoggerFactory.getLogger(HeosSystem.class);
45 private static final int START_DELAY_SEC = 30;
46 private static final long LAST_EVENT_THRESHOLD = TimeUnit.HOURS.toMillis(2);
48 private final ScheduledExecutorService scheduler;
49 private @Nullable ExecutorService singleThreadExecutor;
51 private final HeosEventController eventController = new HeosEventController(this);
53 private final Telnet eventLine = new Telnet();
54 private final HeosSendCommand eventSendCommand = new HeosSendCommand(eventLine);
56 private final Telnet commandLine = new Telnet();
57 private final HeosSendCommand sendCommand = new HeosSendCommand(commandLine);
59 private final HeosJsonParser parser = new HeosJsonParser();
60 private final PropertyChangeListener eventProcessor = evt -> {
61 String newValue = (String) evt.getNewValue();
62 ExecutorService executor = singleThreadExecutor;
63 if (executor == null) {
64 logger.debug("No executor available ignoring event: {}", newValue);
68 executor.submit(() -> eventController.handleEvent(parser.parseEvent(newValue)));
69 } catch (JsonSyntaxException e) {
70 logger.debug("Failed processing event JSON", e);
74 private @Nullable ScheduledFuture<?> keepAliveJob;
75 private @Nullable ScheduledFuture<?> reconnectJob;
77 public HeosSystem(ScheduledExecutorService scheduler) {
78 this.scheduler = scheduler;
82 * Establishes the connection to the HEOS-Network if IP and Port is
83 * set. The caller has to handle the retry to establish the connection
84 * if the method returns {@code false}.
87 * @param connectionPort
89 * @return {@code true} if connection is established else returns {@code false}
91 public HeosFacade establishConnection(String connectionIP, int connectionPort, int heartbeat)
92 throws IOException, ReadException {
93 singleThreadExecutor = Executors.newSingleThreadExecutor();
94 if (commandLine.connect(connectionIP, connectionPort)) {
95 logger.debug("HEOS command line connected at IP {} @ port {}", connectionIP, connectionPort);
96 send(HeosCommands.registerChangeEventOff());
99 if (eventLine.connect(connectionIP, connectionPort)) {
100 logger.debug("HEOS event line connected at IP {} @ port {}", connectionIP, connectionPort);
101 eventSendCommand.send(HeosCommands.registerChangeEventOff(), Void.class);
104 startHeartBeat(heartbeat);
105 startEventListener();
107 return new HeosFacade(this, eventController);
110 boolean isConnected() {
111 return sendCommand.isConnected() && eventSendCommand.isConnected();
115 * Starts the HEOS Heart Beat. This held the connection open even
116 * if no data is transmitted. If the connection to the HEOS system
117 * is lost, the method reconnects to the HEOS system by calling the
118 * {@code establishConnection()} method. If the connection is lost or
119 * reconnect the method fires a bridgeEvent via the {@code HeosEvenController.class}
121 void startHeartBeat(int heartbeatPulse) {
122 keepAliveJob = scheduler.scheduleWithFixedDelay(new KeepAliveRunnable(), START_DELAY_SEC, heartbeatPulse,
126 synchronized void startEventListener() throws IOException, ReadException {
127 logger.debug("HEOS System Event Listener is starting....");
128 eventSendCommand.startInputListener(HeosCommands.registerChangeEventOn());
130 logger.debug("HEOS System Event Listener successfully started");
131 eventLine.getReadResultListener().addPropertyChangeListener(eventProcessor);
134 void closeConnection() {
135 logger.debug("Shutting down HEOS Heart Beat");
136 cancel(keepAliveJob);
137 cancel(this.reconnectJob, false);
139 eventLine.getReadResultListener().removePropertyChangeListener(eventProcessor);
140 eventSendCommand.stopInputListener(HeosCommands.registerChangeEventOff());
141 eventSendCommand.disconnect();
142 sendCommand.disconnect();
144 ExecutorService executor = this.singleThreadExecutor;
145 if (executor != null && executor.isShutdown()) {
146 executor.shutdownNow();
150 HeosResponseObject<Void> send(String command) throws IOException, ReadException {
151 return send(command, Void.class);
154 synchronized <T> HeosResponseObject<T> send(String command, Class<T> clazz) throws IOException, ReadException {
155 return sendCommand.send(command, clazz);
159 * A class which provides a runnable for the HEOS Heart Beat
161 * @author Johannes Einig
163 private class KeepAliveRunnable implements Runnable {
168 if (sendCommand.isHostReachable()) {
169 long timeSinceLastEvent = System.currentTimeMillis() - eventController.getLastEventTime();
170 logger.debug("Time since latest event: {} s", timeSinceLastEvent / 1000);
172 if (timeSinceLastEvent > LAST_EVENT_THRESHOLD) {
173 logger.debug("Events haven't been received for too long");
178 logger.debug("Sending HEOS Heart Beat");
179 HeosResponseObject<Void> response = send(HeosCommands.heartbeat());
180 if (response.result) {
184 logger.debug("Connection to HEOS Network lost!");
186 // catches a failure during a heart beat send message if connection was
187 // getting lost between last Heart Beat but Bridge is online again and not
188 // detected by isHostReachable()
189 } catch (ReadException | IOException e) {
190 logger.debug("Failed at {}", System.currentTimeMillis(), e);
191 logger.debug("Failure during HEOS Heart Beat command with message: {}", e.getMessage());
196 private void restartConnection() {
197 reset(a -> eventController.connectionToSystemLost());
200 private void resetEventStream() {
201 reset(a -> eventController.eventStreamTimeout());
204 private void reset(Consumer<@Nullable Void> method) {
208 cancel(HeosSystem.this.reconnectJob, false);
209 reconnectJob = scheduler.scheduleWithFixedDelay(this::reconnect, 1, 5, TimeUnit.SECONDS);
212 private void reconnect() {
213 logger.debug("Trying to reconnect to HEOS Network...");
214 if (!sendCommand.isHostReachable()) {
218 cancel(HeosSystem.this.reconnectJob, false);
219 logger.debug("Reconnecting to Bridge");
220 scheduler.schedule(eventController::systemReachable, 15, TimeUnit.SECONDS);