]> git.basschouten.com Git - openhab-addons.git/blob
5d672c83f3b9ee5a761d235426e44b3a1ad805a6
[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.folding.internal.handler;
14
15 import java.io.BufferedReader;
16 import java.io.IOException;
17 import java.io.InputStreamReader;
18 import java.lang.reflect.Type;
19 import java.math.BigDecimal;
20 import java.net.InetSocketAddress;
21 import java.net.Socket;
22 import java.util.HashMap;
23 import java.util.List;
24 import java.util.Map;
25 import java.util.concurrent.ScheduledFuture;
26 import java.util.concurrent.TimeUnit;
27
28 import org.openhab.binding.folding.internal.discovery.FoldingDiscoveryProxy;
29 import org.openhab.core.library.types.OnOffType;
30 import org.openhab.core.thing.Bridge;
31 import org.openhab.core.thing.ChannelUID;
32 import org.openhab.core.thing.ThingStatus;
33 import org.openhab.core.thing.ThingStatusDetail;
34 import org.openhab.core.thing.binding.BaseBridgeHandler;
35 import org.openhab.core.types.Command;
36 import org.openhab.core.types.RefreshType;
37 import org.slf4j.Logger;
38 import org.slf4j.LoggerFactory;
39
40 import com.google.gson.Gson;
41 import com.google.gson.reflect.TypeToken;
42 import com.google.gson.stream.JsonReader;
43
44 /**
45  * The {@link FoldingClientHandler} connects to a single Folding@home client,
46  * and controls it. The Client handler can also act as a bridge for the
47  * {@link SlotHandler}.
48  *
49  * @author Marius Bjørnstad - Initial contribution
50  */
51 public class FoldingClientHandler extends BaseBridgeHandler {
52
53     private Logger logger = LoggerFactory.getLogger(FoldingClientHandler.class);
54
55     private ScheduledFuture<?> refreshJob;
56
57     private boolean initializing = true;
58
59     private Socket activeSocket;
60     private BufferedReader socketReader;
61     private Gson gson;
62
63     private volatile int idRefresh = 0;
64
65     private Map<String, SlotUpdateListener> slotUpdateListeners = new HashMap<>();
66
67     public FoldingClientHandler(Bridge thing) {
68         super(thing);
69         gson = new Gson();
70     }
71
72     @Override
73     public void handleCommand(ChannelUID channelUID, Command command) {
74         try {
75             if (command instanceof RefreshType) {
76                 refresh();
77             } else if (channelUID.getId().equals("run")) {
78                 if (command == OnOffType.ON) {
79                     sendCommand("unpause");
80                 } else if (command == OnOffType.OFF) {
81                     sendCommand("pause");
82                 }
83                 refresh();
84                 delayedRefresh();
85             } else if (channelUID.getId().equals("finish")) {
86                 if (command == OnOffType.ON) {
87                     sendCommand("finish");
88                 } else if (command == OnOffType.OFF) {
89                     sendCommand("unpause");
90                 }
91                 refresh();
92                 delayedRefresh();
93             }
94         } catch (IOException e) {
95             logger.debug("Input/output error while handing command", e);
96             disconnected();
97         }
98     }
99
100     @Override
101     public void initialize() {
102         BigDecimal period = (BigDecimal) getThing().getConfiguration().get("polling");
103         if (period != null && period.longValue() != 0) {
104             refreshJob = scheduler.scheduleWithFixedDelay(this::refresh, 5, period.longValue(), TimeUnit.SECONDS);
105         } else {
106             refresh();
107         }
108     }
109
110     @Override
111     public synchronized void dispose() {
112         if (refreshJob != null) {
113             refreshJob.cancel(true);
114         }
115         closeSocket();
116     }
117
118     public synchronized void refresh() {
119         initializing = false;
120         List<SlotInfo> slotList = null;
121         try {
122             Socket s = getSocket();
123             s.getOutputStream().write(("slot-info\r\n").getBytes());
124             socketReader.readLine(); // Discard PyON header
125             JsonReader jr = new JsonReader(socketReader);
126             jr.setLenient(true);
127             Type slotListType = new TypeToken<List<SlotInfo>>() {
128             }.getType();
129
130             slotList = gson.fromJson(jr, slotListType);
131         } catch (IOException e) {
132             logger.debug("Input/error while refreshing Folding client state", e);
133             disconnected();
134             return;
135         }
136         boolean running = false, finishing = true;
137         for (SlotInfo si : slotList) {
138             finishing &= "FINISHING".equals(si.status);
139             running |= "FINISHING".equals(si.status) || "RUNNING".equals(si.status);
140             SlotUpdateListener listener = slotUpdateListeners.get(si.id);
141             if (listener != null) {
142                 listener.refreshed(si);
143             } else {
144                 logger.debug("Providing a new discovery result for slot {}", si.id);
145                 String host = (String) getThing().getConfiguration().get("host");
146                 FoldingDiscoveryProxy.getInstance().newSlot(getThing().getUID(), host, si.id, si.description);
147             }
148         }
149         updateState(getThing().getChannel("run").getUID(), running ? OnOffType.ON : OnOffType.OFF);
150         updateState(getThing().getChannel("finish").getUID(), finishing ? OnOffType.ON : OnOffType.OFF);
151     }
152
153     public void delayedRefresh() {
154         final int iRefresh = ++idRefresh;
155         refreshJob = scheduler.schedule(() -> {
156             if (iRefresh == idRefresh) { // Make a best effort to not run multiple deferred refresh
157                 refresh();
158             }
159         }, 5, TimeUnit.SECONDS);
160     }
161
162     void closeSocket() {
163         if (activeSocket != null && activeSocket.isConnected()) {
164             try {
165                 socketReader.close();
166             } catch (IOException e) {
167             }
168         }
169         socketReader = null;
170         activeSocket = null;
171     }
172
173     private void disconnected() {
174         closeSocket();
175         updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
176     }
177
178     private synchronized Socket getSocket() throws IOException {
179         if (activeSocket == null) {
180             String cfgHost = (String) getThing().getConfiguration().get("host");
181             BigDecimal cfgPort = (BigDecimal) getThing().getConfiguration().get("port");
182             String password = (String) getThing().getConfiguration().get("password");
183             if (cfgHost == null || cfgHost.isEmpty()) {
184                 throw new IOException("Host was not configured");
185             } else if (cfgPort == null || cfgPort.intValue() == 0) {
186                 throw new IOException("Port was not configured");
187             }
188             activeSocket = new Socket();
189             activeSocket.connect(new InetSocketAddress(cfgHost, cfgPort.intValue()), 2000);
190             socketReader = new BufferedReader(new InputStreamReader(activeSocket.getInputStream()));
191             readUntilPrompt(activeSocket); // Discard initial banner message
192             if (password != null) {
193                 activeSocket.getOutputStream().write(("auth \"" + password + "\"\r\n").getBytes());
194                 if (readUntilPrompt(activeSocket).startsWith("OK")) {
195                     updateStatus(ThingStatus.ONLINE);
196                 } else {
197                     updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Incorrect password");
198                 }
199             } else {
200                 updateStatus(ThingStatus.ONLINE);
201             }
202         }
203         return activeSocket;
204     }
205
206     private synchronized String readUntilPrompt(Socket s) throws IOException {
207         boolean havePrompt1 = false;
208         StringBuilder response = new StringBuilder();
209         try {
210             while (true) {
211                 int c = socketReader.read();
212                 if (havePrompt1) {
213                     if (c == ' ') {
214                         return response.toString();
215                     } else {
216                         response.append((char) c);
217                     }
218                 }
219                 response.append((char) c);
220                 havePrompt1 = (c == '>');
221             }
222         } catch (IOException e) {
223             disconnected();
224             throw e;
225         }
226     }
227
228     public synchronized void sendCommand(String command) throws IOException {
229         try {
230             Socket s = getSocket();
231             s.getOutputStream().write((command + "\r\n").getBytes());
232             readUntilPrompt(s);
233         } catch (IOException e) {
234             disconnected();
235             throw e;
236         }
237     }
238
239     public void registerSlot(String id, SlotUpdateListener slotListener) {
240         slotUpdateListeners.put(id, slotListener);
241         if (!initializing) {
242             delayedRefresh();
243         }
244     }
245 }