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.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.ExecutorService;
20 import java.util.concurrent.Executors;
21 import java.util.concurrent.ScheduledExecutorService;
22 import java.util.concurrent.ScheduledFuture;
23 import java.util.concurrent.TimeUnit;
24 import java.util.function.Consumer;
26 import org.eclipse.jdt.annotation.NonNullByDefault;
27 import org.eclipse.jdt.annotation.Nullable;
28 import org.openhab.binding.heos.internal.json.HeosJsonParser;
29 import org.openhab.binding.heos.internal.json.dto.HeosResponseObject;
30 import org.openhab.binding.heos.internal.resources.HeosCommands;
31 import org.openhab.binding.heos.internal.resources.HeosSendCommand;
32 import org.openhab.binding.heos.internal.resources.Telnet;
33 import org.openhab.binding.heos.internal.resources.Telnet.ReadException;
34 import org.slf4j.Logger;
35 import org.slf4j.LoggerFactory;
37 import com.google.gson.JsonSyntaxException;
40 * The {@link HeosSystem} is handling the main commands, which are
41 * sent and received by the HEOS system.
43 * @author Johannes Einig - Initial contribution
46 public class HeosSystem {
47 private final Logger logger = LoggerFactory.getLogger(HeosSystem.class);
49 private static final int START_DELAY_SEC = 30;
50 private static final long LAST_EVENT_THRESHOLD = TimeUnit.HOURS.toMillis(2);
52 private final ScheduledExecutorService scheduler;
53 private @Nullable ExecutorService singleThreadExecutor;
55 private final HeosEventController eventController = new HeosEventController(this);
57 private final Telnet eventLine = new Telnet();
58 private final HeosSendCommand eventSendCommand = new HeosSendCommand(eventLine);
60 private final Telnet commandLine = new Telnet();
61 private final HeosSendCommand sendCommand = new HeosSendCommand(commandLine);
63 private final HeosJsonParser parser = new HeosJsonParser();
64 private final PropertyChangeListener eventProcessor = evt -> {
65 String newValue = (String) evt.getNewValue();
66 ExecutorService executor = singleThreadExecutor;
67 if (executor == null) {
68 logger.debug("No executor available ignoring event: {}", newValue);
72 executor.submit(() -> eventController.handleEvent(parser.parseEvent(newValue)));
73 } catch (JsonSyntaxException e) {
74 logger.debug("Failed processing event JSON", e);
78 private @Nullable ScheduledFuture<?> keepAliveJob;
79 private @Nullable ScheduledFuture<?> reconnectJob;
81 public HeosSystem(ScheduledExecutorService scheduler) {
82 this.scheduler = scheduler;
86 * Establishes the connection to the HEOS-Network if IP and Port is
87 * set. The caller has to handle the retry to establish the connection
88 * if the method returns {@code false}.
91 * @param connectionPort
93 * @return {@code true} if connection is established else returns {@code false}
95 public HeosFacade establishConnection(String connectionIP, int connectionPort, int heartbeat)
96 throws IOException, ReadException {
97 singleThreadExecutor = Executors.newSingleThreadExecutor();
98 if (commandLine.connect(connectionIP, connectionPort)) {
99 logger.debug("HEOS command line connected at IP {} @ port {}", connectionIP, connectionPort);
100 send(HeosCommands.registerChangeEventOff());
103 if (eventLine.connect(connectionIP, connectionPort)) {
104 logger.debug("HEOS event line connected at IP {} @ port {}", connectionIP, connectionPort);
105 eventSendCommand.send(HeosCommands.registerChangeEventOff(), Void.class);
108 startHeartBeat(heartbeat);
109 startEventListener();
111 return new HeosFacade(this, eventController);
114 boolean isConnected() {
115 return sendCommand.isConnected() && eventSendCommand.isConnected();
119 * Starts the HEOS Heart Beat. This held the connection open even
120 * if no data is transmitted. If the connection to the HEOS system
121 * is lost, the method reconnects to the HEOS system by calling the
122 * {@code establishConnection()} method. If the connection is lost or
123 * reconnect the method fires a bridgeEvent via the {@code HeosEvenController.class}
125 void startHeartBeat(int heartbeatPulse) {
126 keepAliveJob = scheduler.scheduleWithFixedDelay(new KeepAliveRunnable(), START_DELAY_SEC, heartbeatPulse,
130 synchronized void startEventListener() throws IOException, ReadException {
131 logger.debug("HEOS System Event Listener is starting....");
132 eventSendCommand.startInputListener(HeosCommands.registerChangeEventOn());
134 logger.debug("HEOS System Event Listener successfully started");
135 eventLine.getReadResultListener().addPropertyChangeListener(eventProcessor);
138 void closeConnection() {
139 logger.debug("Shutting down HEOS Heart Beat");
140 cancel(keepAliveJob);
141 cancel(this.reconnectJob, false);
143 eventLine.getReadResultListener().removePropertyChangeListener(eventProcessor);
144 eventSendCommand.stopInputListener(HeosCommands.registerChangeEventOff());
145 eventSendCommand.disconnect();
146 sendCommand.disconnect();
148 ExecutorService executor = this.singleThreadExecutor;
149 if (executor != null && executor.isShutdown()) {
150 executor.shutdownNow();
154 HeosResponseObject<Void> send(String command) throws IOException, ReadException {
155 return send(command, Void.class);
158 synchronized <T> HeosResponseObject<T> send(String command, Class<T> clazz) throws IOException, ReadException {
159 return sendCommand.send(command, clazz);
163 * A class which provides a runnable for the HEOS Heart Beat
165 * @author Johannes Einig
167 private class KeepAliveRunnable implements Runnable {
172 if (sendCommand.isHostReachable()) {
173 long timeSinceLastEvent = System.currentTimeMillis() - eventController.getLastEventTime();
174 logger.debug("Time since latest event: {} s", timeSinceLastEvent / 1000);
176 if (timeSinceLastEvent > LAST_EVENT_THRESHOLD) {
177 logger.debug("Events haven't been received for too long");
182 logger.debug("Sending HEOS Heart Beat");
183 HeosResponseObject<Void> response = send(HeosCommands.heartbeat());
184 if (response.result) {
188 logger.debug("Connection to HEOS Network lost!");
190 // catches a failure during a heart beat send message if connection was
191 // getting lost between last Heart Beat but Bridge is online again and not
192 // detected by isHostReachable()
193 } catch (ReadException | IOException e) {
194 logger.debug("Failed at {}", System.currentTimeMillis(), e);
195 logger.debug("Failure during HEOS Heart Beat command with message: {}", e.getMessage());
200 private void restartConnection() {
201 reset(a -> eventController.connectionToSystemLost());
204 private void resetEventStream() {
205 reset(a -> eventController.eventStreamTimeout());
208 private void reset(Consumer<@Nullable Void> method) {
212 cancel(HeosSystem.this.reconnectJob, false);
213 reconnectJob = scheduler.scheduleWithFixedDelay(this::reconnect, 1, 5, TimeUnit.SECONDS);
216 private void reconnect() {
217 logger.debug("Trying to reconnect to HEOS Network...");
218 if (!sendCommand.isHostReachable()) {
222 cancel(HeosSystem.this.reconnectJob, false);
223 logger.debug("Reconnecting to Bridge");
224 scheduler.schedule(eventController::systemReachable, 15, TimeUnit.SECONDS);