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.mynice.internal.handler;
15 import static org.openhab.core.thing.Thing.*;
16 import static org.openhab.core.types.RefreshType.REFRESH;
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;
24 import java.util.Optional;
26 import java.util.concurrent.CopyOnWriteArrayList;
27 import java.util.concurrent.ScheduledFuture;
28 import java.util.concurrent.TimeUnit;
30 import javax.net.ssl.SSLSocket;
31 import javax.net.ssl.SSLSocketFactory;
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;
55 * The {@link It4WifiHandler} is responsible for handling commands, which are
56 * sent to one of the channels.
58 * @author Gaƫl L'hopital - Initial contribution
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
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;
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();
78 public It4WifiHandler(Bridge thing, SSLSocketFactory socketFactory) {
80 this.socketFactory = socketFactory;
84 public Collection<Class<? extends ThingHandlerService>> getServices() {
85 return Set.of(MyNiceDiscoveryService.class);
88 public void registerDataListener(MyNiceDataListener dataListener) {
89 dataListeners.add(dataListener);
90 notifyListeners(devices);
93 public void unregisterDataListener(MyNiceDataListener dataListener) {
94 dataListeners.remove(dataListener);
98 public void handleCommand(ChannelUID channelUID, Command command) {
99 if (REFRESH.equals(command)) {
100 sendCommand(CommandType.INFO);
105 public void initialize() {
106 if (getConfigAs(It4WifiConfiguration.class).username.isBlank()) {
107 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "@text/conf-error-no-username");
110 updateStatus(ThingStatus.UNKNOWN);
111 scheduler.execute(() -> startConnector());
115 public void dispose() {
116 dataListeners.clear();
120 sslSocket.ifPresent(socket -> {
123 } catch (IOException e) {
124 logger.warn("Error closing sslsocket : {}", e.getMessage());
127 sslSocket = Optional.empty();
129 connector.ifPresent(c -> scheduler.execute(() -> c.interrupt()));
130 connector = Optional.empty();
133 private void startConnector() {
134 It4WifiConfiguration config = getConfigAs(It4WifiConfiguration.class);
137 logger.debug("Initiating connection to IT4Wifi {} on port {}...", config.hostname, SERVER_PORT);
139 SSLSocket localSocket = (SSLSocket) socketFactory.createSocket(config.hostname, SERVER_PORT);
140 sslSocket = Optional.of(localSocket);
141 localSocket.startHandshake();
143 It4WifiConnector localConnector = new It4WifiConnector(this, localSocket);
144 connector = Optional.of(localConnector);
145 localConnector.start();
147 reqBuilder = new RequestBuilder(config.macAddress, config.username);
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");
156 private void freeKeepAlive() {
157 keepAliveJob.ifPresent(job -> job.cancel(true));
158 keepAliveJob = Optional.empty();
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);
167 if (event instanceof Response responseEvent) {
168 handleResponse(responseEvent);
170 notifyListeners(event.getDevices());
175 private void handleResponse(Response response) {
176 switch (response.type) {
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);
185 if (keepAliveJob.isEmpty()) { // means we are not connected
186 switch (response.authentication.perm) {
188 sendCommand(CommandType.CONNECT);
191 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_PENDING,
192 "@text/conf-pending-validation");
193 scheduler.schedule(() -> handShaked(), 15, TimeUnit.SECONDS);
199 String sc = response.authentication.sc;
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);
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));
215 notifyListeners(response.getDevices());
218 notifyListeners(response.getDevices());
221 logger.debug("Change command accepted");
224 logger.warn("Unhandled response type : {}", response.type);
228 public void handShaked() {
229 handshakeAttempts = 0;
230 It4WifiConfiguration config = getConfigAs(It4WifiConfiguration.class);
231 sendCommand(config.password.isBlank() ? CommandType.PAIR : CommandType.VERIFY);
234 private void notifyListeners(List<Device> list) {
236 dataListeners.forEach(listener -> listener.onDataFetched(devices));
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."));
244 public void sendCommand(CommandType command) {
245 sendCommand(reqBuilder.buildMessage(command));
248 public void sendCommand(String id, String command) {
249 sendCommand(reqBuilder.buildMessage(id, command.toLowerCase()));
252 public void sendCommand(String id, T4Command t4) {
253 sendCommand(reqBuilder.buildMessage(id, t4));
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)) {
260 if (handshakeAttempts++ <= MAX_HANDSHAKE_ATTEMPTS) {
261 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, message);
264 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "@text/error-handshake-limit");