]> git.basschouten.com Git - openhab-addons.git/blob
190c93a3f50f6e1ae2a021cd2bf42125c0c9fc5e
[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.sinope.handler;
14
15 import java.io.IOException;
16 import java.io.InputStream;
17 import java.io.OutputStream;
18 import java.net.Socket;
19 import java.net.UnknownHostException;
20 import java.nio.ByteBuffer;
21 import java.nio.ByteOrder;
22 import java.util.Collection;
23 import java.util.LinkedList;
24 import java.util.List;
25 import java.util.concurrent.CopyOnWriteArrayList;
26 import java.util.concurrent.ScheduledFuture;
27 import java.util.concurrent.TimeUnit;
28
29 import org.eclipse.jdt.annotation.NonNullByDefault;
30 import org.eclipse.jdt.annotation.Nullable;
31 import org.openhab.binding.sinope.SinopeBindingConstants;
32 import org.openhab.binding.sinope.internal.SinopeConfigStatusMessage;
33 import org.openhab.binding.sinope.internal.config.SinopeConfig;
34 import org.openhab.binding.sinope.internal.core.SinopeApiLoginAnswer;
35 import org.openhab.binding.sinope.internal.core.SinopeApiLoginRequest;
36 import org.openhab.binding.sinope.internal.core.SinopeDeviceReportAnswer;
37 import org.openhab.binding.sinope.internal.core.base.SinopeAnswer;
38 import org.openhab.binding.sinope.internal.core.base.SinopeDataAnswer;
39 import org.openhab.binding.sinope.internal.core.base.SinopeDataRequest;
40 import org.openhab.binding.sinope.internal.core.base.SinopeRequest;
41 import org.openhab.binding.sinope.internal.discovery.SinopeThingsDiscoveryService;
42 import org.openhab.binding.sinope.internal.util.ByteUtil;
43 import org.openhab.core.config.core.status.ConfigStatusMessage;
44 import org.openhab.core.thing.Bridge;
45 import org.openhab.core.thing.ChannelUID;
46 import org.openhab.core.thing.ThingStatus;
47 import org.openhab.core.thing.ThingStatusDetail;
48 import org.openhab.core.thing.binding.ConfigStatusBridgeHandler;
49 import org.openhab.core.types.Command;
50 import org.slf4j.Logger;
51 import org.slf4j.LoggerFactory;
52
53 /**
54  * The {@link SinopeGatewayHandler} is responsible for handling commands for the SinopĂ© Gateway.
55  *
56  * @author Pascal Larin - Initial contribution
57  */
58 @NonNullByDefault
59 public class SinopeGatewayHandler extends ConfigStatusBridgeHandler {
60
61     private static final int FIRST_POLL_INTERVAL = 1; // In second
62     private final Logger logger = LoggerFactory.getLogger(SinopeGatewayHandler.class);
63     private @Nullable ScheduledFuture<?> pollFuture;
64     private long refreshInterval; // In seconds
65     private final List<SinopeThermostatHandler> thermostatHandlers = new CopyOnWriteArrayList<>();
66     private int seq = 1;
67     private @Nullable Socket clientSocket;
68     private boolean searching; // In searching mode..
69     private @Nullable ScheduledFuture<?> pollSearch;
70
71     public SinopeGatewayHandler(final Bridge bridge) {
72         super(bridge);
73     }
74
75     @Override
76     public void initialize() {
77         logger.debug("Initializing Sinope Gateway");
78
79         try {
80             SinopeConfig config = getConfigAs(SinopeConfig.class);
81             refreshInterval = config.refresh;
82             if (config.hostname == null) {
83                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
84                         "Gateway hostname must be set");
85             } else if (config.port == null) {
86                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Gateway port must be set");
87             } else if (config.gatewayId == null || SinopeConfig.convert(config.gatewayId) == null) {
88                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Gateway Id must be set");
89             } else if (config.apiKey == null || SinopeConfig.convert(config.apiKey) == null) {
90                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Api Key must be set");
91             } else if (connectToBridge()) {
92                 schedulePoll();
93                 updateStatus(ThingStatus.ONLINE);
94                 return;
95             }
96         } catch (IOException e) {
97             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE,
98                     "Can't connect to gateway. Please make sure that another instance is not connected.");
99         }
100     }
101
102     @Override
103     public void dispose() {
104         super.dispose();
105         stopPoll();
106         if (clientSocket != null) {
107             try {
108                 clientSocket.close();
109             } catch (IOException e) {
110                 logger.warn("Unexpected error when closing connection to gateway", e);
111             }
112         }
113     }
114
115     synchronized void schedulePoll() {
116         if (searching) {
117             return;
118         }
119         if (pollFuture != null) {
120             pollFuture.cancel(false);
121         }
122         logger.debug("Scheduling poll for {} s out, then every {} s", FIRST_POLL_INTERVAL, refreshInterval);
123         pollFuture = scheduler.scheduleWithFixedDelay(() -> poll(), FIRST_POLL_INTERVAL, refreshInterval,
124                 TimeUnit.SECONDS);
125     }
126
127     synchronized void stopPoll() {
128         if (pollFuture != null && !pollFuture.isCancelled()) {
129             pollFuture.cancel(true);
130             pollFuture = null;
131         }
132     }
133
134     private synchronized void poll() {
135         if (!thermostatHandlers.isEmpty()) {
136             logger.debug("Polling for state");
137             try {
138                 if (connectToBridge()) {
139                     logger.debug("Connected to bridge");
140                     for (SinopeThermostatHandler sinopeThermostatHandler : thermostatHandlers) {
141                         sinopeThermostatHandler.update();
142                     }
143                 }
144             } catch (IOException e) {
145                 setCommunicationError(true);
146                 logger.debug("Polling issue", e);
147             }
148         } else {
149             logger.debug("nothing to poll");
150         }
151     }
152
153     boolean connectToBridge() throws UnknownHostException, IOException {
154         SinopeConfig config = getConfigAs(SinopeConfig.class);
155         if (this.clientSocket == null || !this.clientSocket.isConnected() || this.clientSocket.isClosed()) {
156             this.clientSocket = new Socket(config.hostname, config.port);
157             SinopeApiLoginRequest loginRequest = new SinopeApiLoginRequest(SinopeConfig.convert(config.gatewayId),
158                     SinopeConfig.convert(config.apiKey));
159             SinopeApiLoginAnswer loginAnswer = (SinopeApiLoginAnswer) execute(loginRequest);
160             setCommunicationError(false);
161             return loginAnswer.getStatus() == 0;
162         }
163         return true;
164     }
165
166     public synchronized byte[] newSeq() {
167         return ByteBuffer.allocate(4).order(ByteOrder.LITTLE_ENDIAN).putInt(seq++).array();
168     }
169
170     synchronized SinopeAnswer execute(SinopeRequest command) throws UnknownHostException, IOException {
171         Socket clientSocket = this.getClientSocket();
172         OutputStream outToServer = clientSocket.getOutputStream();
173         InputStream inputStream = clientSocket.getInputStream();
174         outToServer.write(command.getPayload());
175         outToServer.flush();
176         return command.getReplyAnswer(inputStream);
177     }
178
179     synchronized SinopeAnswer execute(SinopeDataRequest command) throws UnknownHostException, IOException {
180         Socket clientSocket = this.getClientSocket();
181
182         OutputStream outToServer = clientSocket.getOutputStream();
183         InputStream inputStream = clientSocket.getInputStream();
184         if (logger.isDebugEnabled()) {
185             int leftBytes = inputStream.available();
186             if (leftBytes > 0) {
187                 logger.debug("Hum... some leftovers: {} bytes", leftBytes);
188             }
189         }
190         outToServer.write(command.getPayload());
191
192         SinopeDataAnswer answ = command.getReplyAnswer(inputStream);
193
194         while (answ.getMore() == 0x01) {
195             answ = command.getReplyAnswer(inputStream);
196         }
197         return answ;
198     }
199
200     public boolean registerThermostatHandler(SinopeThermostatHandler thermostatHandler) {
201         return thermostatHandlers.add(thermostatHandler);
202     }
203
204     public boolean unregisterThermostatHandler(SinopeThermostatHandler thermostatHandler) {
205         return thermostatHandlers.remove(thermostatHandler);
206     }
207
208     @Override
209     public void handleCommand(ChannelUID channelUID, Command command) {
210     }
211
212     @Override
213     public Collection<ConfigStatusMessage> getConfigStatus() {
214         Collection<ConfigStatusMessage> configStatusMessages = new LinkedList<>();
215
216         SinopeConfig config = getConfigAs(SinopeConfig.class);
217         if (config.hostname == null) {
218             configStatusMessages.add(ConfigStatusMessage.Builder.error(SinopeBindingConstants.CONFIG_PROPERTY_HOST)
219                     .withMessageKeySuffix(SinopeConfigStatusMessage.HOST_MISSING.getMessageKey())
220                     .withArguments(SinopeBindingConstants.CONFIG_PROPERTY_HOST).build());
221         }
222         if (config.port == null) {
223             configStatusMessages.add(ConfigStatusMessage.Builder.error(SinopeBindingConstants.CONFIG_PROPERTY_PORT)
224                     .withMessageKeySuffix(SinopeConfigStatusMessage.PORT_MISSING.getMessageKey())
225                     .withArguments(SinopeBindingConstants.CONFIG_PROPERTY_PORT).build());
226         }
227
228         if (config.gatewayId == null || SinopeConfig.convert(config.gatewayId) == null) {
229             configStatusMessages
230                     .add(ConfigStatusMessage.Builder.error(SinopeBindingConstants.CONFIG_PROPERTY_GATEWAY_ID)
231                             .withMessageKeySuffix(SinopeConfigStatusMessage.GATEWAY_ID_INVALID.getMessageKey())
232                             .withArguments(SinopeBindingConstants.CONFIG_PROPERTY_GATEWAY_ID).build());
233         }
234         if (config.apiKey == null || SinopeConfig.convert(config.apiKey) == null) {
235             configStatusMessages.add(ConfigStatusMessage.Builder.error(SinopeBindingConstants.CONFIG_PROPERTY_API_KEY)
236                     .withMessageKeySuffix(SinopeConfigStatusMessage.API_KEY_INVALID.getMessageKey())
237                     .withArguments(SinopeBindingConstants.CONFIG_PROPERTY_API_KEY).build());
238         }
239
240         return configStatusMessages;
241     }
242
243     public void startSearch(final SinopeThingsDiscoveryService sinopeThingsDiscoveryService)
244             throws UnknownHostException, IOException {
245         // Stopping current polling
246         stopPoll();
247         this.searching = true;
248         pollSearch = scheduler.schedule(() -> search(sinopeThingsDiscoveryService), FIRST_POLL_INTERVAL,
249                 TimeUnit.SECONDS);
250     }
251
252     private void search(final SinopeThingsDiscoveryService sinopeThingsDiscoveryService) {
253         try {
254             if (connectToBridge()) {
255                 logger.debug("Successful login");
256                 try {
257                     while (clientSocket != null && clientSocket.isConnected() && !clientSocket.isClosed()) {
258                         SinopeDeviceReportAnswer answ;
259                         answ = new SinopeDeviceReportAnswer(clientSocket.getInputStream());
260                         logger.debug("Got report answer: {}", answ);
261                         logger.debug("Your device id is: {}", ByteUtil.toString(answ.getDeviceId()));
262                         sinopeThingsDiscoveryService.newThermostat(answ.getDeviceId());
263                     }
264                 } finally {
265                     if (clientSocket != null && !clientSocket.isClosed()) {
266                         clientSocket.close();
267                         clientSocket = null;
268                     }
269                 }
270             }
271         } catch (UnknownHostException e) {
272             logger.warn("Unexpected error when searching for new devices", e);
273         } catch (IOException e) {
274             logger.debug("Network connection error, expected when ending search", e);
275         } finally {
276             schedulePoll();
277         }
278     }
279
280     public void stopSearch() throws IOException {
281         this.searching = false;
282         if (this.pollSearch != null && !this.pollSearch.isCancelled()) {
283             this.pollSearch.cancel(true);
284             this.pollSearch = null;
285         }
286         if (this.clientSocket != null && this.clientSocket.isConnected()) {
287             this.clientSocket.close();
288             this.clientSocket = null;
289         }
290
291         schedulePoll();
292     }
293
294     public @Nullable Socket getClientSocket() throws UnknownHostException, IOException {
295         if (connectToBridge()) {
296             return clientSocket;
297         }
298         throw new IOException("Could not create a socket to the gateway. Check host/ip/gateway Id");
299     }
300
301     public void setCommunicationError(boolean hasError) {
302         if (hasError) {
303             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
304             clientSocket = null;
305         } else {
306             updateStatus(ThingStatus.ONLINE);
307             schedulePoll();
308         }
309     }
310 }