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.folding.internal.handler;
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;
25 import java.util.concurrent.ScheduledFuture;
26 import java.util.concurrent.TimeUnit;
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;
40 import com.google.gson.Gson;
41 import com.google.gson.reflect.TypeToken;
42 import com.google.gson.stream.JsonReader;
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}.
49 * @author Marius Bjørnstad - Initial contribution
51 public class FoldingClientHandler extends BaseBridgeHandler {
53 private Logger logger = LoggerFactory.getLogger(FoldingClientHandler.class);
55 private ScheduledFuture<?> refreshJob;
57 private boolean initializing = true;
59 private Socket activeSocket;
60 private BufferedReader socketReader;
63 private volatile int idRefresh = 0;
65 private Map<String, SlotUpdateListener> slotUpdateListeners = new HashMap<>();
67 public FoldingClientHandler(Bridge thing) {
73 public void handleCommand(ChannelUID channelUID, Command command) {
75 if (command instanceof RefreshType) {
77 } else if (channelUID.getId().equals("run")) {
78 if (command == OnOffType.ON) {
79 sendCommand("unpause");
80 } else if (command == OnOffType.OFF) {
85 } else if (channelUID.getId().equals("finish")) {
86 if (command == OnOffType.ON) {
87 sendCommand("finish");
88 } else if (command == OnOffType.OFF) {
89 sendCommand("unpause");
94 } catch (IOException e) {
95 logger.debug("Input/output error while handing command", e);
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);
111 public synchronized void dispose() {
112 if (refreshJob != null) {
113 refreshJob.cancel(true);
118 public synchronized void refresh() {
119 initializing = false;
120 List<SlotInfo> slotList = null;
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);
127 Type slotListType = new TypeToken<List<SlotInfo>>() {
130 slotList = gson.fromJson(jr, slotListType);
131 } catch (IOException e) {
132 logger.debug("Input/error while refreshing Folding client state", e);
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);
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);
149 updateState(getThing().getChannel("run").getUID(), running ? OnOffType.ON : OnOffType.OFF);
150 updateState(getThing().getChannel("finish").getUID(), finishing ? OnOffType.ON : OnOffType.OFF);
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
159 }, 5, TimeUnit.SECONDS);
163 if (activeSocket != null && activeSocket.isConnected()) {
165 socketReader.close();
166 } catch (IOException e) {
173 private void disconnected() {
175 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
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");
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);
197 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Incorrect password");
200 updateStatus(ThingStatus.ONLINE);
206 private synchronized String readUntilPrompt(Socket s) throws IOException {
207 boolean havePrompt1 = false;
208 StringBuilder response = new StringBuilder();
211 int c = socketReader.read();
214 return response.toString();
216 response.append((char) c);
219 response.append((char) c);
220 havePrompt1 = (c == '>');
222 } catch (IOException e) {
228 public synchronized void sendCommand(String command) throws IOException {
230 Socket s = getSocket();
231 s.getOutputStream().write((command + "\r\n").getBytes());
233 } catch (IOException e) {
239 public void registerSlot(String id, SlotUpdateListener slotListener) {
240 slotUpdateListeners.put(id, slotListener);