]> git.basschouten.com Git - openhab-addons.git/blob
7a7c083b7bcfb0e56b246de774700cf0bbb16e63
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2021 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.*;
20 import java.util.function.Consumer;
21
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;
32
33 import com.google.gson.JsonSyntaxException;
34
35 /**
36  * The {@link HeosSystem} is handling the main commands, which are
37  * sent and received by the HEOS system.
38  *
39  * @author Johannes Einig - Initial contribution
40  */
41 @NonNullByDefault
42 public class HeosSystem {
43     private final Logger logger = LoggerFactory.getLogger(HeosSystem.class);
44
45     private static final int START_DELAY_SEC = 30;
46     private static final long LAST_EVENT_THRESHOLD = TimeUnit.HOURS.toMillis(2);
47
48     private final ScheduledExecutorService scheduler;
49     private @Nullable ExecutorService singleThreadExecutor;
50
51     private final HeosEventController eventController = new HeosEventController(this);
52
53     private final Telnet eventLine = new Telnet();
54     private final HeosSendCommand eventSendCommand = new HeosSendCommand(eventLine);
55
56     private final Telnet commandLine = new Telnet();
57     private final HeosSendCommand sendCommand = new HeosSendCommand(commandLine);
58
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);
65             return;
66         }
67         try {
68             executor.submit(() -> eventController.handleEvent(parser.parseEvent(newValue)));
69         } catch (JsonSyntaxException e) {
70             logger.debug("Failed processing event JSON", e);
71         }
72     };
73
74     private @Nullable ScheduledFuture<?> keepAliveJob;
75     private @Nullable ScheduledFuture<?> reconnectJob;
76
77     public HeosSystem(ScheduledExecutorService scheduler) {
78         this.scheduler = scheduler;
79     }
80
81     /**
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}.
85      *
86      * @param connectionIP
87      * @param connectionPort
88      * @param heartbeat
89      * @return {@code true} if connection is established else returns {@code false}
90      */
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());
97         }
98
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);
102         }
103
104         startHeartBeat(heartbeat);
105         startEventListener();
106
107         return new HeosFacade(this, eventController);
108     }
109
110     boolean isConnected() {
111         return sendCommand.isConnected() && eventSendCommand.isConnected();
112     }
113
114     /**
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}
120      */
121     void startHeartBeat(int heartbeatPulse) {
122         keepAliveJob = scheduler.scheduleWithFixedDelay(new KeepAliveRunnable(), START_DELAY_SEC, heartbeatPulse,
123                 TimeUnit.SECONDS);
124     }
125
126     synchronized void startEventListener() throws IOException, ReadException {
127         logger.debug("HEOS System Event Listener is starting....");
128         eventSendCommand.startInputListener(HeosCommands.registerChangeEventOn());
129
130         logger.debug("HEOS System Event Listener successfully started");
131         eventLine.getReadResultListener().addPropertyChangeListener(eventProcessor);
132     }
133
134     void closeConnection() {
135         logger.debug("Shutting down HEOS Heart Beat");
136         cancel(keepAliveJob);
137         cancel(this.reconnectJob, false);
138
139         eventLine.getReadResultListener().removePropertyChangeListener(eventProcessor);
140         eventSendCommand.stopInputListener(HeosCommands.registerChangeEventOff());
141         eventSendCommand.disconnect();
142         sendCommand.disconnect();
143         @Nullable
144         ExecutorService executor = this.singleThreadExecutor;
145         if (executor != null && executor.isShutdown()) {
146             executor.shutdownNow();
147         }
148     }
149
150     HeosResponseObject<Void> send(String command) throws IOException, ReadException {
151         return send(command, Void.class);
152     }
153
154     synchronized <T> HeosResponseObject<T> send(String command, Class<T> clazz) throws IOException, ReadException {
155         return sendCommand.send(command, clazz);
156     }
157
158     /**
159      * A class which provides a runnable for the HEOS Heart Beat
160      *
161      * @author Johannes Einig
162      */
163     private class KeepAliveRunnable implements Runnable {
164
165         @Override
166         public void run() {
167             try {
168                 if (sendCommand.isHostReachable()) {
169                     long timeSinceLastEvent = System.currentTimeMillis() - eventController.getLastEventTime();
170                     logger.debug("Time since latest event: {} s", timeSinceLastEvent / 1000);
171
172                     if (timeSinceLastEvent > LAST_EVENT_THRESHOLD) {
173                         logger.debug("Events haven't been received for too long");
174                         resetEventStream();
175                         return;
176                     }
177
178                     logger.debug("Sending HEOS Heart Beat");
179                     HeosResponseObject<Void> response = send(HeosCommands.heartbeat());
180                     if (response.result) {
181                         return;
182                     }
183                 }
184                 logger.debug("Connection to HEOS Network lost!");
185
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());
192             }
193             restartConnection();
194         }
195
196         private void restartConnection() {
197             reset(a -> eventController.connectionToSystemLost());
198         }
199
200         private void resetEventStream() {
201             reset(a -> eventController.eventStreamTimeout());
202         }
203
204         private void reset(Consumer<@Nullable Void> method) {
205             closeConnection();
206             method.accept(null);
207
208             cancel(HeosSystem.this.reconnectJob, false);
209             reconnectJob = scheduler.scheduleWithFixedDelay(this::reconnect, 1, 5, TimeUnit.SECONDS);
210         }
211
212         private void reconnect() {
213             logger.debug("Trying to reconnect to HEOS Network...");
214             if (!sendCommand.isHostReachable()) {
215                 return;
216             }
217
218             cancel(HeosSystem.this.reconnectJob, false);
219             logger.debug("Reconnecting to Bridge");
220             scheduler.schedule(eventController::systemReachable, 15, TimeUnit.SECONDS);
221         }
222     }
223 }