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.sinope.handler;
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;
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;
54 * The {@link SinopeGatewayHandler} is responsible for handling commands for the Sinopé Gateway.
56 * @author Pascal Larin - Initial contribution
59 public class SinopeGatewayHandler extends ConfigStatusBridgeHandler {
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<>();
67 private @Nullable Socket clientSocket;
68 private boolean searching; // In searching mode..
69 private @Nullable ScheduledFuture<?> pollSearch;
71 public SinopeGatewayHandler(final Bridge bridge) {
76 public void initialize() {
77 logger.debug("Initializing Sinope Gateway");
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()) {
93 updateStatus(ThingStatus.ONLINE);
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.");
103 public void dispose() {
106 if (clientSocket != null) {
108 clientSocket.close();
109 } catch (IOException e) {
110 logger.warn("Unexpected error when closing connection to gateway", e);
115 synchronized void schedulePoll() {
119 if (pollFuture != null) {
120 pollFuture.cancel(false);
122 logger.debug("Scheduling poll for {} s out, then every {} s", FIRST_POLL_INTERVAL, refreshInterval);
123 pollFuture = scheduler.scheduleWithFixedDelay(() -> poll(), FIRST_POLL_INTERVAL, refreshInterval,
127 synchronized void stopPoll() {
128 if (pollFuture != null && !pollFuture.isCancelled()) {
129 pollFuture.cancel(true);
134 private synchronized void poll() {
135 if (!thermostatHandlers.isEmpty()) {
136 logger.debug("Polling for state");
138 if (connectToBridge()) {
139 logger.debug("Connected to bridge");
140 for (SinopeThermostatHandler sinopeThermostatHandler : thermostatHandlers) {
141 sinopeThermostatHandler.update();
144 } catch (IOException e) {
145 setCommunicationError(true);
146 logger.debug("Polling issue", e);
149 logger.debug("nothing to poll");
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;
166 public synchronized byte[] newSeq() {
167 return ByteBuffer.allocate(4).order(ByteOrder.LITTLE_ENDIAN).putInt(seq++).array();
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());
176 SinopeAnswer answ = command.getReplyAnswer(inputStream);
181 synchronized SinopeAnswer execute(SinopeDataRequest command) throws UnknownHostException, IOException {
182 Socket clientSocket = this.getClientSocket();
184 OutputStream outToServer = clientSocket.getOutputStream();
185 InputStream inputStream = clientSocket.getInputStream();
186 if (logger.isDebugEnabled()) {
187 int leftBytes = inputStream.available();
189 logger.debug("Hum... some leftovers: {} bytes", leftBytes);
192 outToServer.write(command.getPayload());
194 SinopeDataAnswer answ = command.getReplyAnswer(inputStream);
196 while (answ.getMore() == 0x01) {
197 answ = command.getReplyAnswer(inputStream);
202 public boolean registerThermostatHandler(SinopeThermostatHandler thermostatHandler) {
203 return thermostatHandlers.add(thermostatHandler);
206 public boolean unregisterThermostatHandler(SinopeThermostatHandler thermostatHandler) {
207 return thermostatHandlers.remove(thermostatHandler);
211 public void handleCommand(ChannelUID channelUID, Command command) {
215 public Collection<ConfigStatusMessage> getConfigStatus() {
216 Collection<ConfigStatusMessage> configStatusMessages = new LinkedList<>();
218 SinopeConfig config = getConfigAs(SinopeConfig.class);
219 if (config.hostname == null) {
220 configStatusMessages.add(ConfigStatusMessage.Builder.error(SinopeBindingConstants.CONFIG_PROPERTY_HOST)
221 .withMessageKeySuffix(SinopeConfigStatusMessage.HOST_MISSING.getMessageKey())
222 .withArguments(SinopeBindingConstants.CONFIG_PROPERTY_HOST).build());
224 if (config.port == null) {
225 configStatusMessages.add(ConfigStatusMessage.Builder.error(SinopeBindingConstants.CONFIG_PROPERTY_PORT)
226 .withMessageKeySuffix(SinopeConfigStatusMessage.PORT_MISSING.getMessageKey())
227 .withArguments(SinopeBindingConstants.CONFIG_PROPERTY_PORT).build());
230 if (config.gatewayId == null || SinopeConfig.convert(config.gatewayId) == null) {
232 .add(ConfigStatusMessage.Builder.error(SinopeBindingConstants.CONFIG_PROPERTY_GATEWAY_ID)
233 .withMessageKeySuffix(SinopeConfigStatusMessage.GATEWAY_ID_INVALID.getMessageKey())
234 .withArguments(SinopeBindingConstants.CONFIG_PROPERTY_GATEWAY_ID).build());
236 if (config.apiKey == null || SinopeConfig.convert(config.apiKey) == null) {
237 configStatusMessages.add(ConfigStatusMessage.Builder.error(SinopeBindingConstants.CONFIG_PROPERTY_API_KEY)
238 .withMessageKeySuffix(SinopeConfigStatusMessage.API_KEY_INVALID.getMessageKey())
239 .withArguments(SinopeBindingConstants.CONFIG_PROPERTY_API_KEY).build());
242 return configStatusMessages;
245 public void startSearch(final SinopeThingsDiscoveryService sinopeThingsDiscoveryService)
246 throws UnknownHostException, IOException {
247 // Stopping current polling
249 this.searching = true;
250 pollSearch = scheduler.schedule(() -> search(sinopeThingsDiscoveryService), FIRST_POLL_INTERVAL,
254 private void search(final SinopeThingsDiscoveryService sinopeThingsDiscoveryService) {
256 if (connectToBridge()) {
257 logger.debug("Successful login");
259 while (clientSocket != null && clientSocket.isConnected() && !clientSocket.isClosed()) {
260 SinopeDeviceReportAnswer answ;
261 answ = new SinopeDeviceReportAnswer(clientSocket.getInputStream());
262 logger.debug("Got report answer: {}", answ);
263 logger.debug("Your device id is: {}", ByteUtil.toString(answ.getDeviceId()));
264 sinopeThingsDiscoveryService.newThermostat(answ.getDeviceId());
267 if (clientSocket != null && !clientSocket.isClosed()) {
268 clientSocket.close();
273 } catch (UnknownHostException e) {
274 logger.warn("Unexpected error when searching for new devices", e);
275 } catch (IOException e) {
276 logger.debug("Network connection error, expected when ending search", e);
282 public void stopSearch() throws IOException {
283 this.searching = false;
284 if (this.pollSearch != null && !this.pollSearch.isCancelled()) {
285 this.pollSearch.cancel(true);
286 this.pollSearch = null;
288 if (this.clientSocket != null && this.clientSocket.isConnected()) {
289 this.clientSocket.close();
290 this.clientSocket = null;
296 public @Nullable Socket getClientSocket() throws UnknownHostException, IOException {
297 if (connectToBridge()) {
300 throw new IOException("Could not create a socket to the gateway. Check host/ip/gateway Id");
303 public void setCommunicationError(boolean hasError) {
305 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
308 updateStatus(ThingStatus.ONLINE);