]> git.basschouten.com Git - openhab-addons.git/blob
84130e301c7ae7395c05e16fad217c49e50f3211
[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.mynice.internal.handler;
14
15 import static org.openhab.core.thing.Thing.*;
16 import static org.openhab.core.types.RefreshType.REFRESH;
17
18 import java.io.IOException;
19 import java.net.UnknownHostException;
20 import java.util.ArrayList;
21 import java.util.Collection;
22 import java.util.List;
23 import java.util.Map;
24 import java.util.Optional;
25 import java.util.Set;
26 import java.util.concurrent.CopyOnWriteArrayList;
27 import java.util.concurrent.ScheduledFuture;
28 import java.util.concurrent.TimeUnit;
29
30 import javax.net.ssl.SSLSocket;
31 import javax.net.ssl.SSLSocketFactory;
32
33 import org.eclipse.jdt.annotation.NonNullByDefault;
34 import org.openhab.binding.mynice.internal.config.It4WifiConfiguration;
35 import org.openhab.binding.mynice.internal.discovery.MyNiceDiscoveryService;
36 import org.openhab.binding.mynice.internal.xml.MyNiceXStream;
37 import org.openhab.binding.mynice.internal.xml.RequestBuilder;
38 import org.openhab.binding.mynice.internal.xml.dto.CommandType;
39 import org.openhab.binding.mynice.internal.xml.dto.Device;
40 import org.openhab.binding.mynice.internal.xml.dto.Event;
41 import org.openhab.binding.mynice.internal.xml.dto.Response;
42 import org.openhab.binding.mynice.internal.xml.dto.T4Command;
43 import org.openhab.core.config.core.Configuration;
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.BaseBridgeHandler;
49 import org.openhab.core.thing.binding.ThingHandlerService;
50 import org.openhab.core.types.Command;
51 import org.slf4j.Logger;
52 import org.slf4j.LoggerFactory;
53
54 /**
55  * The {@link It4WifiHandler} is responsible for handling commands, which are
56  * sent to one of the channels.
57  *
58  * @author GaĆ«l L'hopital - Initial contribution
59  */
60 @NonNullByDefault
61 public class It4WifiHandler extends BaseBridgeHandler {
62     private static final int SERVER_PORT = 443;
63     private static final int MAX_HANDSHAKE_ATTEMPTS = 3;
64     private static final int KEEPALIVE_DELAY_S = 235; // Timeout seems to be at 6 min
65
66     private final Logger logger = LoggerFactory.getLogger(It4WifiHandler.class);
67     private final List<MyNiceDataListener> dataListeners = new CopyOnWriteArrayList<>();
68     private final MyNiceXStream xstream = new MyNiceXStream();
69     private final SSLSocketFactory socketFactory;
70
71     private @NonNullByDefault({}) RequestBuilder reqBuilder;
72     private List<Device> devices = new ArrayList<>();
73     private int handshakeAttempts = 0;
74     private Optional<ScheduledFuture<?>> keepAliveJob = Optional.empty();
75     private Optional<It4WifiConnector> connector = Optional.empty();
76     private Optional<SSLSocket> sslSocket = Optional.empty();
77
78     public It4WifiHandler(Bridge thing, SSLSocketFactory socketFactory) {
79         super(thing);
80         this.socketFactory = socketFactory;
81     }
82
83     @Override
84     public Collection<Class<? extends ThingHandlerService>> getServices() {
85         return Set.of(MyNiceDiscoveryService.class);
86     }
87
88     public void registerDataListener(MyNiceDataListener dataListener) {
89         dataListeners.add(dataListener);
90         notifyListeners(devices);
91     }
92
93     public void unregisterDataListener(MyNiceDataListener dataListener) {
94         dataListeners.remove(dataListener);
95     }
96
97     @Override
98     public void handleCommand(ChannelUID channelUID, Command command) {
99         if (REFRESH.equals(command)) {
100             sendCommand(CommandType.INFO);
101         }
102     }
103
104     @Override
105     public void initialize() {
106         if (getConfigAs(It4WifiConfiguration.class).username.isBlank()) {
107             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "@text/conf-error-no-username");
108             return;
109         }
110         updateStatus(ThingStatus.UNKNOWN);
111         scheduler.execute(() -> startConnector());
112     }
113
114     @Override
115     public void dispose() {
116         dataListeners.clear();
117
118         freeKeepAlive();
119
120         sslSocket.ifPresent(socket -> {
121             try {
122                 socket.close();
123             } catch (IOException e) {
124                 logger.warn("Error closing sslsocket : {}", e.getMessage());
125             }
126         });
127         sslSocket = Optional.empty();
128
129         connector.ifPresent(c -> scheduler.execute(() -> c.interrupt()));
130         connector = Optional.empty();
131     }
132
133     private void startConnector() {
134         It4WifiConfiguration config = getConfigAs(It4WifiConfiguration.class);
135         freeKeepAlive();
136         try {
137             logger.debug("Initiating connection to IT4Wifi {} on port {}...", config.hostname, SERVER_PORT);
138
139             SSLSocket localSocket = (SSLSocket) socketFactory.createSocket(config.hostname, SERVER_PORT);
140             sslSocket = Optional.of(localSocket);
141             localSocket.startHandshake();
142
143             It4WifiConnector localConnector = new It4WifiConnector(this, localSocket);
144             connector = Optional.of(localConnector);
145             localConnector.start();
146
147             reqBuilder = new RequestBuilder(config.macAddress, config.username);
148             handShaked();
149         } catch (UnknownHostException e) {
150             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "@text/conf-error-hostname");
151         } catch (IOException e) {
152             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "@text/error-handshake-init");
153         }
154     }
155
156     private void freeKeepAlive() {
157         keepAliveJob.ifPresent(job -> job.cancel(true));
158         keepAliveJob = Optional.empty();
159     }
160
161     public void received(String command) {
162         logger.debug("Received : {}", command);
163         Event event = xstream.deserialize(command);
164         if (event.error != null) {
165             logger.warn("Error code {} received : {}", event.error.code, event.error.info);
166         } else {
167             if (event instanceof Response responseEvent) {
168                 handleResponse(responseEvent);
169             } else {
170                 notifyListeners(event.getDevices());
171             }
172         }
173     }
174
175     private void handleResponse(Response response) {
176         switch (response.type) {
177             case PAIR:
178                 Configuration thingConfig = editConfiguration();
179                 thingConfig.put(It4WifiConfiguration.PASSWORD, response.authentication.pwd);
180                 updateConfiguration(thingConfig);
181                 logger.info("Pairing key updated in Configuration.");
182                 sendCommand(CommandType.VERIFY);
183                 return;
184             case VERIFY:
185                 if (keepAliveJob.isEmpty()) { // means we are not connected
186                     switch (response.authentication.perm) {
187                         case admin, user:
188                             sendCommand(CommandType.CONNECT);
189                             return;
190                         case wait:
191                             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_PENDING,
192                                     "@text/conf-pending-validation");
193                             scheduler.schedule(() -> handShaked(), 15, TimeUnit.SECONDS);
194                             return;
195                     }
196                 }
197                 return;
198             case CONNECT:
199                 String sc = response.authentication.sc;
200                 if (sc != null) {
201                     It4WifiConfiguration config = getConfigAs(It4WifiConfiguration.class);
202                     reqBuilder.setChallenges(sc, response.authentication.id, config.password);
203                     keepAliveJob = Optional.of(scheduler.scheduleWithFixedDelay(() -> sendCommand(CommandType.VERIFY),
204                             KEEPALIVE_DELAY_S, KEEPALIVE_DELAY_S, TimeUnit.SECONDS));
205                     sendCommand(CommandType.INFO);
206                 }
207                 return;
208             case INFO:
209                 updateStatus(ThingStatus.ONLINE);
210                 if (thing.getProperties().isEmpty()) {
211                     updateProperties(Map.of(PROPERTY_VENDOR, response.intf.manuf, PROPERTY_MODEL_ID, response.intf.prod,
212                             PROPERTY_SERIAL_NUMBER, response.intf.serialNr, PROPERTY_HARDWARE_VERSION,
213                             response.intf.versionHW, PROPERTY_FIRMWARE_VERSION, response.intf.versionFW));
214                 }
215                 notifyListeners(response.getDevices());
216                 return;
217             case STATUS:
218                 notifyListeners(response.getDevices());
219                 return;
220             case CHANGE:
221                 logger.debug("Change command accepted");
222                 return;
223             default:
224                 logger.warn("Unhandled response type : {}", response.type);
225         }
226     }
227
228     public void handShaked() {
229         handshakeAttempts = 0;
230         It4WifiConfiguration config = getConfigAs(It4WifiConfiguration.class);
231         sendCommand(config.password.isBlank() ? CommandType.PAIR : CommandType.VERIFY);
232     }
233
234     private void notifyListeners(List<Device> list) {
235         devices = list;
236         dataListeners.forEach(listener -> listener.onDataFetched(devices));
237     }
238
239     private void sendCommand(String command) {
240         connector.ifPresentOrElse(c -> c.sendCommand(command),
241                 () -> logger.warn("Tried to send a command when IT4WifiConnector is not initialized."));
242     }
243
244     public void sendCommand(CommandType command) {
245         sendCommand(reqBuilder.buildMessage(command));
246     }
247
248     public void sendCommand(String id, String command) {
249         sendCommand(reqBuilder.buildMessage(id, command.toLowerCase()));
250     }
251
252     public void sendCommand(String id, T4Command t4) {
253         sendCommand(reqBuilder.buildMessage(id, t4));
254     }
255
256     public void communicationError(String message) {
257         // avoid a status update that would generates a WARN while we're already disconnecting
258         if (getThing().getStatus().equals(ThingStatus.ONLINE)) {
259             dispose();
260             if (handshakeAttempts++ <= MAX_HANDSHAKE_ATTEMPTS) {
261                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, message);
262                 startConnector();
263             } else {
264                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "@text/error-handshake-limit");
265             }
266         }
267     }
268 }