]> git.basschouten.com Git - openhab-addons.git/blob
f8548736c1a5447e1296f5d8db36ed1783f80150
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2023 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
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
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.binding.heos.internal.api;
14
15 import static org.openhab.binding.heos.internal.handler.FutureUtil.cancel;
16
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;
25
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;
36
37 import com.google.gson.JsonSyntaxException;
38
39 /**
40  * The {@link HeosSystem} is handling the main commands, which are
41  * sent and received by the HEOS system.
42  *
43  * @author Johannes Einig - Initial contribution
44  */
45 @NonNullByDefault
46 public class HeosSystem {
47     private final Logger logger = LoggerFactory.getLogger(HeosSystem.class);
48
49     private static final int START_DELAY_SEC = 30;
50     private static final long LAST_EVENT_THRESHOLD = TimeUnit.HOURS.toMillis(2);
51
52     private final ScheduledExecutorService scheduler;
53     private @Nullable ExecutorService singleThreadExecutor;
54
55     private final HeosEventController eventController = new HeosEventController(this);
56
57     private final Telnet eventLine = new Telnet();
58     private final HeosSendCommand eventSendCommand = new HeosSendCommand(eventLine);
59
60     private final Telnet commandLine = new Telnet();
61     private final HeosSendCommand sendCommand = new HeosSendCommand(commandLine);
62
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);
69             return;
70         }
71         try {
72             executor.submit(() -> eventController.handleEvent(parser.parseEvent(newValue)));
73         } catch (JsonSyntaxException e) {
74             logger.debug("Failed processing event JSON", e);
75         }
76     };
77
78     private @Nullable ScheduledFuture<?> keepAliveJob;
79     private @Nullable ScheduledFuture<?> reconnectJob;
80
81     public HeosSystem(ScheduledExecutorService scheduler) {
82         this.scheduler = scheduler;
83     }
84
85     /**
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}.
89      *
90      * @param connectionIP
91      * @param connectionPort
92      * @param heartbeat
93      * @return {@code true} if connection is established else returns {@code false}
94      */
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());
101         }
102
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);
106         }
107
108         startHeartBeat(heartbeat);
109         startEventListener();
110
111         return new HeosFacade(this, eventController);
112     }
113
114     boolean isConnected() {
115         return sendCommand.isConnected() && eventSendCommand.isConnected();
116     }
117
118     /**
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}
124      */
125     void startHeartBeat(int heartbeatPulse) {
126         keepAliveJob = scheduler.scheduleWithFixedDelay(new KeepAliveRunnable(), START_DELAY_SEC, heartbeatPulse,
127                 TimeUnit.SECONDS);
128     }
129
130     synchronized void startEventListener() throws IOException, ReadException {
131         logger.debug("HEOS System Event Listener is starting....");
132         eventSendCommand.startInputListener(HeosCommands.registerChangeEventOn());
133
134         logger.debug("HEOS System Event Listener successfully started");
135         eventLine.getReadResultListener().addPropertyChangeListener(eventProcessor);
136     }
137
138     void closeConnection() {
139         logger.debug("Shutting down HEOS Heart Beat");
140         cancel(keepAliveJob);
141         cancel(this.reconnectJob, false);
142
143         eventLine.getReadResultListener().removePropertyChangeListener(eventProcessor);
144         eventSendCommand.stopInputListener(HeosCommands.registerChangeEventOff());
145         eventSendCommand.disconnect();
146         sendCommand.disconnect();
147         @Nullable
148         ExecutorService executor = this.singleThreadExecutor;
149         if (executor != null && executor.isShutdown()) {
150             executor.shutdownNow();
151         }
152     }
153
154     HeosResponseObject<Void> send(String command) throws IOException, ReadException {
155         return send(command, Void.class);
156     }
157
158     synchronized <T> HeosResponseObject<T> send(String command, Class<T> clazz) throws IOException, ReadException {
159         return sendCommand.send(command, clazz);
160     }
161
162     /**
163      * A class which provides a runnable for the HEOS Heart Beat
164      *
165      * @author Johannes Einig
166      */
167     private class KeepAliveRunnable implements Runnable {
168
169         @Override
170         public void run() {
171             try {
172                 if (sendCommand.isHostReachable()) {
173                     long timeSinceLastEvent = System.currentTimeMillis() - eventController.getLastEventTime();
174                     logger.debug("Time since latest event: {} s", timeSinceLastEvent / 1000);
175
176                     if (timeSinceLastEvent > LAST_EVENT_THRESHOLD) {
177                         logger.debug("Events haven't been received for too long");
178                         resetEventStream();
179                         return;
180                     }
181
182                     logger.debug("Sending HEOS Heart Beat");
183                     HeosResponseObject<Void> response = send(HeosCommands.heartbeat());
184                     if (response.result) {
185                         return;
186                     }
187                 }
188                 logger.debug("Connection to HEOS Network lost!");
189
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());
196             }
197             restartConnection();
198         }
199
200         private void restartConnection() {
201             reset(a -> eventController.connectionToSystemLost());
202         }
203
204         private void resetEventStream() {
205             reset(a -> eventController.eventStreamTimeout());
206         }
207
208         private void reset(Consumer<@Nullable Void> method) {
209             closeConnection();
210             method.accept(null);
211
212             cancel(HeosSystem.this.reconnectJob, false);
213             reconnectJob = scheduler.scheduleWithFixedDelay(this::reconnect, 1, 5, TimeUnit.SECONDS);
214         }
215
216         private void reconnect() {
217             logger.debug("Trying to reconnect to HEOS Network...");
218             if (!sendCommand.isHostReachable()) {
219                 return;
220             }
221
222             cancel(HeosSystem.this.reconnectJob, false);
223             logger.debug("Reconnecting to Bridge");
224             scheduler.schedule(eventController::systemReachable, 15, TimeUnit.SECONDS);
225         }
226     }
227 }