diff --git a/bundles/org.openhab.binding.mynice/README.md b/bundles/org.openhab.binding.mynice/README.md index d4c7e65e7..f8aa92909 100644 --- a/bundles/org.openhab.binding.mynice/README.md +++ b/bundles/org.openhab.binding.mynice/README.md @@ -46,14 +46,18 @@ Channels available for the gates are : | Channel | Type | Read/Write | Description | |-----------|--------|------------|----------------------------------------------------------| -| status | String | R | Description of the current status of the door (1) | +| status | String | R/W (1) | Description of the current status of the door (2) | | obstruct | Switch | R | Flags an obstruction, blocking the door | | moving | Switch | R | Indicates if the device is currently operating a command | -| command | String | W | Send a given command to the gate (2) | +| command | String | W | Send a given command to the gate (3) | | t4command | String | W | Send a T4 Command to the gate | +| courtesy | Switch | R/W | Status of the courtesy light (4) | -(1) : can be open, closed, opening, closing, stopped. -(2) : must be "stop","open","close" +(1) : Accepted commands are : STOP, MOVE +(2) : Valid status are : OPEN, CLOSED, OPENING, CLOSING, STOPPED +(3) : Accepted commands are : "stop","open","close" +(4) : There is no way to retrieve the current status of the courtesy light. It is supposed to be ON when the gate is moving and turned OFF once done. +The delay between the moving end and light being turned off is a configuration parameter of the `courtesy` channel. ### T4 Commands @@ -110,5 +114,6 @@ String NiceIT4WIFI_GateStatus "Gate Status" (gMyniceSwing) ["Statu String NiceIT4WIFI_Obstruction "Obstruction" (gMyniceSwing) {channel="mynice:swing:83eef09166:1:obstruct"} Switch NiceIT4WIFI_Moving "Moving" (gMyniceSwing) ["Status","Vibration"] {channel="mynice:swing:83eef09166:1:moving"} String NiceIT4WIFI_Command "Command" (gMyniceSwing) {channel="mynice:swing:83eef09166:1:command"} +Switch NiceIT4WIFI_Command "Courtesy Light" (gMyniceSwing) {channel="mynice:swing:83eef09166:1:courtesy"} ``` diff --git a/bundles/org.openhab.binding.mynice/src/main/java/org/openhab/binding/mynice/internal/MyNiceBindingConstants.java b/bundles/org.openhab.binding.mynice/src/main/java/org/openhab/binding/mynice/internal/MyNiceBindingConstants.java index b785ac6bb..62ec6d36b 100644 --- a/bundles/org.openhab.binding.mynice/src/main/java/org/openhab/binding/mynice/internal/MyNiceBindingConstants.java +++ b/bundles/org.openhab.binding.mynice/src/main/java/org/openhab/binding/mynice/internal/MyNiceBindingConstants.java @@ -25,11 +25,12 @@ public class MyNiceBindingConstants { private static final String BINDING_ID = "mynice"; // List of all Channel ids - public static final String DOOR_STATUS = "status"; - public static final String DOOR_OBSTRUCTED = "obstruct"; - public static final String DOOR_MOVING = "moving"; - public static final String DOOR_COMMAND = "command"; - public static final String DOOR_T4_COMMAND = "t4command"; + public static final String CHANNEL_STATUS = "status"; + public static final String CHANNEL_OBSTRUCTED = "obstruct"; + public static final String CHANNEL_MOVING = "moving"; + public static final String CHANNEL_COMMAND = "command"; + public static final String CHANNEL_T4_COMMAND = "t4command"; + public static final String CHANNEL_COURTESY = "courtesy"; // List of all Thing Type UIDs public static final ThingTypeUID BRIDGE_TYPE_IT4WIFI = new ThingTypeUID(BINDING_ID, "it4wifi"); diff --git a/bundles/org.openhab.binding.mynice/src/main/java/org/openhab/binding/mynice/internal/MyNiceHandlerFactory.java b/bundles/org.openhab.binding.mynice/src/main/java/org/openhab/binding/mynice/internal/MyNiceHandlerFactory.java index a5fc54a3f..78971286f 100644 --- a/bundles/org.openhab.binding.mynice/src/main/java/org/openhab/binding/mynice/internal/MyNiceHandlerFactory.java +++ b/bundles/org.openhab.binding.mynice/src/main/java/org/openhab/binding/mynice/internal/MyNiceHandlerFactory.java @@ -14,18 +14,26 @@ package org.openhab.binding.mynice.internal; import static org.openhab.binding.mynice.internal.MyNiceBindingConstants.*; +import java.security.KeyManagementException; +import java.security.NoSuchAlgorithmException; import java.util.Set; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLSocketFactory; +import javax.net.ssl.TrustManager; + import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import org.openhab.binding.mynice.internal.handler.GateHandler; import org.openhab.binding.mynice.internal.handler.It4WifiHandler; +import org.openhab.core.io.net.http.TrustAllTrustManager; import org.openhab.core.thing.Bridge; import org.openhab.core.thing.Thing; import org.openhab.core.thing.ThingTypeUID; import org.openhab.core.thing.binding.BaseThingHandlerFactory; import org.openhab.core.thing.binding.ThingHandler; import org.openhab.core.thing.binding.ThingHandlerFactory; +import org.osgi.service.component.annotations.Activate; import org.osgi.service.component.annotations.Component; /** @@ -38,6 +46,18 @@ import org.osgi.service.component.annotations.Component; public class MyNiceHandlerFactory extends BaseThingHandlerFactory { private static final Set SUPPORTED_THING_TYPES_UIDS = Set.of(BRIDGE_TYPE_IT4WIFI, THING_TYPE_SWING, THING_TYPE_SLIDING); + private final SSLSocketFactory socketFactory; + + @Activate + public MyNiceHandlerFactory() { + try { + SSLContext sslContext = SSLContext.getInstance("SSL"); + sslContext.init(null, new TrustManager[] { TrustAllTrustManager.getInstance() }, null); + socketFactory = sslContext.getSocketFactory(); + } catch (NoSuchAlgorithmException | KeyManagementException e) { + throw new IllegalArgumentException(e); + } + } @Override public boolean supportsThingType(ThingTypeUID thingTypeUID) { @@ -49,7 +69,7 @@ public class MyNiceHandlerFactory extends BaseThingHandlerFactory { ThingTypeUID thingTypeUID = thing.getThingTypeUID(); if (BRIDGE_TYPE_IT4WIFI.equals(thingTypeUID)) { - return new It4WifiHandler((Bridge) thing); + return new It4WifiHandler((Bridge) thing, socketFactory); } else if (THING_TYPE_SWING.equals(thingTypeUID)) { return new GateHandler(thing); } else if (THING_TYPE_SLIDING.equals(thingTypeUID)) { diff --git a/bundles/org.openhab.binding.mynice/src/main/java/org/openhab/binding/mynice/internal/config/CourtesyConfiguration.java b/bundles/org.openhab.binding.mynice/src/main/java/org/openhab/binding/mynice/internal/config/CourtesyConfiguration.java new file mode 100644 index 000000000..812b32ae2 --- /dev/null +++ b/bundles/org.openhab.binding.mynice/src/main/java/org/openhab/binding/mynice/internal/config/CourtesyConfiguration.java @@ -0,0 +1,25 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.mynice.internal.config; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link CourtesyConfiguration} class contains fields mapping courtesy channel configuration parameters. + * + * @author Gaël L'hopital - Initial contribution + */ +@NonNullByDefault +public class CourtesyConfiguration { + public int duration = 60; +} diff --git a/bundles/org.openhab.binding.mynice/src/main/java/org/openhab/binding/mynice/internal/discovery/MyNiceDiscoveryService.java b/bundles/org.openhab.binding.mynice/src/main/java/org/openhab/binding/mynice/internal/discovery/MyNiceDiscoveryService.java index 19dc18297..4859e3861 100644 --- a/bundles/org.openhab.binding.mynice/src/main/java/org/openhab/binding/mynice/internal/discovery/MyNiceDiscoveryService.java +++ b/bundles/org.openhab.binding.mynice/src/main/java/org/openhab/binding/mynice/internal/discovery/MyNiceDiscoveryService.java @@ -15,6 +15,7 @@ package org.openhab.binding.mynice.internal.discovery; import static org.openhab.binding.mynice.internal.MyNiceBindingConstants.*; import java.util.List; +import java.util.Optional; import java.util.Set; import org.eclipse.jdt.annotation.NonNullByDefault; @@ -45,49 +46,43 @@ public class MyNiceDiscoveryService extends AbstractDiscoveryService private static final int SEARCH_TIME = 5; private final Logger logger = LoggerFactory.getLogger(MyNiceDiscoveryService.class); - private @Nullable It4WifiHandler bridgeHandler; + private Optional bridgeHandler = Optional.empty(); /** * Creates a MyNiceDiscoveryService with background discovery disabled. */ public MyNiceDiscoveryService() { - super(Set.of(THING_TYPE_SWING), SEARCH_TIME, false); + super(Set.of(THING_TYPE_SWING, THING_TYPE_SLIDING), SEARCH_TIME, false); } @Override public void setThingHandler(ThingHandler handler) { if (handler instanceof It4WifiHandler it4Handler) { - bridgeHandler = it4Handler; + bridgeHandler = Optional.of(it4Handler); } } @Override public @Nullable ThingHandler getThingHandler() { - return bridgeHandler; + return bridgeHandler.orElse(null); } @Override public void activate() { super.activate(null); - It4WifiHandler handler = bridgeHandler; - if (handler != null) { - handler.registerDataListener(this); - } + bridgeHandler.ifPresent(h -> h.registerDataListener(this)); } @Override public void deactivate() { - It4WifiHandler handler = bridgeHandler; - if (handler != null) { - handler.unregisterDataListener(this); - } + bridgeHandler.ifPresent(h -> h.unregisterDataListener(this)); + bridgeHandler = Optional.empty(); super.deactivate(); } @Override public void onDataFetched(List devices) { - It4WifiHandler handler = bridgeHandler; - if (handler != null) { + bridgeHandler.ifPresent(handler -> { ThingUID bridgeUID = handler.getThing().getUID(); devices.stream().filter(device -> device.type != null).forEach(device -> { ThingUID thingUID = switch (device.type) { @@ -105,14 +100,11 @@ public class MyNiceDiscoveryService extends AbstractDiscoveryService logger.info("`{}` type of device is not yet supported", device.type); } }); - } + }); } @Override protected void startScan() { - It4WifiHandler handler = bridgeHandler; - if (handler != null) { - handler.sendCommand(CommandType.INFO); - } + bridgeHandler.ifPresent(h -> h.sendCommand(CommandType.INFO)); } } diff --git a/bundles/org.openhab.binding.mynice/src/main/java/org/openhab/binding/mynice/internal/handler/GateHandler.java b/bundles/org.openhab.binding.mynice/src/main/java/org/openhab/binding/mynice/internal/handler/GateHandler.java index 8f5e9fb3b..8a86a62e4 100644 --- a/bundles/org.openhab.binding.mynice/src/main/java/org/openhab/binding/mynice/internal/handler/GateHandler.java +++ b/bundles/org.openhab.binding.mynice/src/main/java/org/openhab/binding/mynice/internal/handler/GateHandler.java @@ -18,14 +18,20 @@ import static org.openhab.core.thing.Thing.*; import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.concurrent.TimeUnit; import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.mynice.internal.config.CourtesyConfiguration; import org.openhab.binding.mynice.internal.xml.dto.CommandType; import org.openhab.binding.mynice.internal.xml.dto.Device; +import org.openhab.binding.mynice.internal.xml.dto.Properties.DoorStatus; +import org.openhab.binding.mynice.internal.xml.dto.Property; import org.openhab.binding.mynice.internal.xml.dto.T4Command; import org.openhab.core.library.types.OnOffType; +import org.openhab.core.library.types.StopMoveType; import org.openhab.core.library.types.StringType; import org.openhab.core.thing.Bridge; +import org.openhab.core.thing.Channel; import org.openhab.core.thing.ChannelUID; import org.openhab.core.thing.Thing; import org.openhab.core.thing.ThingStatus; @@ -42,12 +48,12 @@ import org.slf4j.LoggerFactory; */ @NonNullByDefault public class GateHandler extends BaseThingHandler implements MyNiceDataListener { - private static final String OPENING = "opening"; - private static final String CLOSING = "closing"; private final Logger logger = LoggerFactory.getLogger(GateHandler.class); private String id = ""; + private Optional gateStatus = Optional.empty(); + private List t4Allowed = List.of(); public GateHandler(Thing thing) { super(thing); @@ -61,6 +67,9 @@ public class GateHandler extends BaseThingHandler implements MyNiceDataListener @Override public void dispose() { + id = ""; + gateStatus = Optional.empty(); + t4Allowed = List.of(); getBridgeHandler().ifPresent(h -> h.unregisterDataListener(this)); } @@ -77,30 +86,65 @@ public class GateHandler extends BaseThingHandler implements MyNiceDataListener @Override public void handleCommand(ChannelUID channelUID, Command command) { + String channelId = channelUID.getId(); + if (command instanceof RefreshType) { - return; + getBridgeHandler().ifPresent(handler -> handler.sendCommand(CommandType.INFO)); + } else if (CHANNEL_COURTESY.equals(channelId) && command instanceof OnOffType) { + handleT4Command(T4Command.MDEy); + } else if (CHANNEL_STATUS.equals(channelId)) { + gateStatus.ifPresentOrElse(status -> { + if (command instanceof StopMoveType stopMoveCommand) { + handleStopMove(status, stopMoveCommand); + } else { + try { + handleStopMove(status, StopMoveType.valueOf(command.toString())); + } catch (IllegalArgumentException e) { + logger.warn("Invalid StopMoveType command received : {}", command); + } + } + }, () -> logger.info("Current status of the gate unknown, can not send {} command", command)); + } else if (CHANNEL_COMMAND.equals(channelId)) { + getBridgeHandler().ifPresent(handler -> handler.sendCommand(id, command.toString())); + } else if (CHANNEL_T4_COMMAND.equals(channelId)) { + try { + T4Command t4 = T4Command.fromCode(command.toString()); + handleT4Command(t4); + } catch (IllegalArgumentException e) { + logger.warn("{} is not a valid T4 command", command); + } } else { - handleCommand(channelUID.getId(), command.toString()); + logger.warn("Unable to handle command {} on channel {}", command, channelId); } } - private void handleCommand(String channelId, String command) { - if (DOOR_COMMAND.equals(channelId)) { - getBridgeHandler().ifPresent(handler -> handler.sendCommand(id, command)); - } else if (DOOR_T4_COMMAND.equals(channelId)) { - String allowed = thing.getProperties().get(ALLOWED_T4); - if (allowed != null && allowed.contains(command)) { - getBridgeHandler().ifPresent(handler -> { - try { - T4Command t4 = T4Command.fromCode(command); - handler.sendCommand(id, t4); - } catch (IllegalArgumentException e) { - logger.warn("{} is not a valid T4 command", command); - } - }); + private void handleStopMove(DoorStatus status, StopMoveType stopMoveCommand) { + if (stopMoveCommand == StopMoveType.STOP) { + if (status == DoorStatus.STOPPED) { + logger.info("The gate is already stopped."); } else { - logger.warn("This thing does not accept the T4 command '{}'", command); + handleT4Command(T4Command.MDAy); } + return; + } + + // It's a move Command + if (status == DoorStatus.OPEN) { + handleT4Command(T4Command.MDA0); + } else if (status == DoorStatus.CLOSED) { + handleT4Command(T4Command.MDAz); + } else if (status.moving) { + logger.info("The gate is already currently moving."); + } else { // it is closed + handleT4Command(T4Command.MDAx); + } + } + + private void handleT4Command(T4Command t4Command) { + if (t4Allowed.contains(t4Command)) { + getBridgeHandler().ifPresent(handler -> handler.sendCommand(id, t4Command)); + } else { + logger.warn("This gate does not accept the T4 command '{}'", t4Command); } } @@ -108,20 +152,35 @@ public class GateHandler extends BaseThingHandler implements MyNiceDataListener public void onDataFetched(List devices) { devices.stream().filter(d -> id.equals(d.id)).findFirst().map(device -> { updateStatus(ThingStatus.ONLINE); - if (thing.getProperties().isEmpty()) { - int value = Integer.parseInt(device.properties.t4allowed.values, 16); - List t4Allowed = T4Command.fromBitmask(value).stream().map(Enum::name).toList(); - updateProperties(Map.of(PROPERTY_VENDOR, device.manuf, PROPERTY_MODEL_ID, device.prod, - PROPERTY_SERIAL_NUMBER, device.serialNr, PROPERTY_HARDWARE_VERSION, device.versionHW, - PROPERTY_FIRMWARE_VERSION, device.versionFW, ALLOWED_T4, String.join(",", t4Allowed))); + Property t4list = device.properties.t4allowed; + if (t4Allowed.isEmpty() && t4list != null) { + int value = Integer.parseInt(t4list.values, 16); + t4Allowed = T4Command.fromBitmask(value).stream().toList(); + if (thing.getProperties().isEmpty()) { + updateProperties(Map.of(PROPERTY_VENDOR, device.manuf, PROPERTY_MODEL_ID, device.prod, + PROPERTY_SERIAL_NUMBER, device.serialNr, PROPERTY_HARDWARE_VERSION, device.versionHW, + PROPERTY_FIRMWARE_VERSION, device.versionFW, ALLOWED_T4, + String.join(",", t4Allowed.stream().map(Enum::name).toList()))); + } } if (device.prod != null) { getBridgeHandler().ifPresent(h -> h.sendCommand(CommandType.STATUS)); } else { - String status = device.properties.doorStatus; - updateState(DOOR_STATUS, new StringType(status)); - updateState(DOOR_OBSTRUCTED, OnOffType.from("1".equals(device.properties.obstruct))); - updateState(DOOR_MOVING, OnOffType.from(status.equals(CLOSING) || status.equals(OPENING))); + DoorStatus status = device.properties.status(); + + updateState(CHANNEL_STATUS, new StringType(status.name())); + updateState(CHANNEL_OBSTRUCTED, OnOffType.from(device.properties.obstructed())); + updateState(CHANNEL_MOVING, OnOffType.from(status.moving)); + if (status.moving && isLinked(CHANNEL_COURTESY)) { + Channel courtesy = getThing().getChannel(CHANNEL_COURTESY); + if (courtesy != null) { + updateState(CHANNEL_COURTESY, OnOffType.ON); + CourtesyConfiguration config = courtesy.getConfiguration().as(CourtesyConfiguration.class); + scheduler.schedule(() -> updateState(CHANNEL_COURTESY, OnOffType.OFF), config.duration, + TimeUnit.SECONDS); + } + } + gateStatus = Optional.of(status); } return true; }); diff --git a/bundles/org.openhab.binding.mynice/src/main/java/org/openhab/binding/mynice/internal/handler/It4WifiConnector.java b/bundles/org.openhab.binding.mynice/src/main/java/org/openhab/binding/mynice/internal/handler/It4WifiConnector.java index 0a5a0c764..661a5fbdb 100644 --- a/bundles/org.openhab.binding.mynice/src/main/java/org/openhab/binding/mynice/internal/handler/It4WifiConnector.java +++ b/bundles/org.openhab.binding.mynice/src/main/java/org/openhab/binding/mynice/internal/handler/It4WifiConnector.java @@ -12,59 +12,47 @@ */ package org.openhab.binding.mynice.internal.handler; +import java.io.Closeable; import java.io.IOException; import java.io.InputStreamReader; import java.io.OutputStreamWriter; -import java.security.KeyManagementException; -import java.security.NoSuchAlgorithmException; -import javax.net.ssl.SSLContext; import javax.net.ssl.SSLSocket; -import javax.net.ssl.TrustManager; import org.eclipse.jdt.annotation.NonNullByDefault; -import org.openhab.core.io.net.http.TrustAllTrustManager; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** - * The {@link It4WifiConnector} is responsible for connecting reading, writing and disconnecting from the It4Wifi. + * The {@link It4WifiConnector} is responsible for reading and writing to the It4Wifi. * * @author Gaël L'hopital - Initial Contribution */ @NonNullByDefault public class It4WifiConnector extends Thread { - private static final int SERVER_PORT = 443; private static final char ETX = '\u0003'; private static final char STX = '\u0002'; private final Logger logger = LoggerFactory.getLogger(It4WifiConnector.class); private final It4WifiHandler handler; - private final SSLSocket sslsocket; + private final InputStreamReader in; + private final OutputStreamWriter out; - private @NonNullByDefault({}) InputStreamReader in; - private @NonNullByDefault({}) OutputStreamWriter out; - - public It4WifiConnector(String hostname, It4WifiHandler handler) { + public It4WifiConnector(It4WifiHandler handler, SSLSocket sslSocket) throws IOException { super(It4WifiConnector.class.getName()); this.handler = handler; - try { - SSLContext sslContext = SSLContext.getInstance("SSL"); - sslContext.init(null, new TrustManager[] { TrustAllTrustManager.getInstance() }, null); - sslsocket = (SSLSocket) sslContext.getSocketFactory().createSocket(hostname, SERVER_PORT); - setDaemon(true); - } catch (NoSuchAlgorithmException | KeyManagementException | IOException e) { - throw new IllegalArgumentException(e); - } + this.in = new InputStreamReader(sslSocket.getInputStream()); + this.out = new OutputStreamWriter(sslSocket.getOutputStream()); + setDaemon(true); } @Override public void run() { String buffer = ""; - try { - connect(); - while (!interrupted()) { - int data; + int data; + + while (!interrupted()) { + try { while ((data = in.read()) != -1) { if (data == STX) { buffer = ""; @@ -74,68 +62,37 @@ public class It4WifiConnector extends Thread { buffer += (char) data; } } + } catch (IOException e) { + handler.communicationError(e.toString()); + interrupt(); } - handler.connectorInterrupted("IT4WifiConnector interrupted"); - dispose(); - } catch (IOException e) { - handler.connectorInterrupted(e.getMessage()); } } + @Override + public void interrupt() { + logger.debug("Closing streams"); + tryClose(in); + tryClose(out); + + super.interrupt(); + } + public synchronized void sendCommand(String command) { logger.debug("Sending ItT4Wifi :{}", command); try { out.write(STX + command + ETX); out.flush(); } catch (IOException e) { - handler.connectorInterrupted(e.getMessage()); + handler.communicationError(e.toString()); } } - private void disconnect() { - logger.debug("Disconnecting"); - - if (in != null) { - try { - in.close(); - } catch (IOException ignore) { - } - } - if (out != null) { - try { - out.close(); - } catch (IOException ignore) { - } - } - - in = null; - out = null; - - logger.debug("Disconnected"); - } - - /** - * Stop the device thread - * - * @throws IOException - */ - public void dispose() { - interrupt(); - disconnect(); + private void tryClose(Closeable closeable) { try { - sslsocket.close(); + closeable.close(); } catch (IOException e) { - logger.warn("Error closing sslsocket : {}", e.getMessage()); + logger.debug("Exception closing stream : {}", e.getMessage()); } } - - private void connect() throws IOException { - disconnect(); - logger.debug("Initiating connection to IT4Wifi on port {}...", SERVER_PORT); - - sslsocket.startHandshake(); - in = new InputStreamReader(sslsocket.getInputStream()); - out = new OutputStreamWriter(sslsocket.getOutputStream()); - handler.handShaked(); - } } diff --git a/bundles/org.openhab.binding.mynice/src/main/java/org/openhab/binding/mynice/internal/handler/It4WifiHandler.java b/bundles/org.openhab.binding.mynice/src/main/java/org/openhab/binding/mynice/internal/handler/It4WifiHandler.java index b6535a5fb..84130e301 100644 --- a/bundles/org.openhab.binding.mynice/src/main/java/org/openhab/binding/mynice/internal/handler/It4WifiHandler.java +++ b/bundles/org.openhab.binding.mynice/src/main/java/org/openhab/binding/mynice/internal/handler/It4WifiHandler.java @@ -15,17 +15,22 @@ package org.openhab.binding.mynice.internal.handler; import static org.openhab.core.thing.Thing.*; import static org.openhab.core.types.RefreshType.REFRESH; +import java.io.IOException; +import java.net.UnknownHostException; import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.Set; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; +import javax.net.ssl.SSLSocket; +import javax.net.ssl.SSLSocketFactory; + import org.eclipse.jdt.annotation.NonNullByDefault; -import org.eclipse.jdt.annotation.Nullable; import org.openhab.binding.mynice.internal.config.It4WifiConfiguration; import org.openhab.binding.mynice.internal.discovery.MyNiceDiscoveryService; import org.openhab.binding.mynice.internal.xml.MyNiceXStream; @@ -54,21 +59,25 @@ import org.slf4j.LoggerFactory; */ @NonNullByDefault public class It4WifiHandler extends BaseBridgeHandler { + private static final int SERVER_PORT = 443; private static final int MAX_HANDSHAKE_ATTEMPTS = 3; private static final int KEEPALIVE_DELAY_S = 235; // Timeout seems to be at 6 min private final Logger logger = LoggerFactory.getLogger(It4WifiHandler.class); private final List dataListeners = new CopyOnWriteArrayList<>(); private final MyNiceXStream xstream = new MyNiceXStream(); + private final SSLSocketFactory socketFactory; private @NonNullByDefault({}) RequestBuilder reqBuilder; - private @Nullable It4WifiConnector connector; - private @Nullable ScheduledFuture keepAliveJob; private List devices = new ArrayList<>(); private int handshakeAttempts = 0; + private Optional> keepAliveJob = Optional.empty(); + private Optional connector = Optional.empty(); + private Optional sslSocket = Optional.empty(); - public It4WifiHandler(Bridge thing) { + public It4WifiHandler(Bridge thing, SSLSocketFactory socketFactory) { super(thing); + this.socketFactory = socketFactory; } @Override @@ -96,36 +105,57 @@ public class It4WifiHandler extends BaseBridgeHandler { public void initialize() { if (getConfigAs(It4WifiConfiguration.class).username.isBlank()) { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "@text/conf-error-no-username"); - } else { - updateStatus(ThingStatus.UNKNOWN); - scheduler.execute(() -> startConnector()); + return; } + updateStatus(ThingStatus.UNKNOWN); + scheduler.execute(() -> startConnector()); } @Override public void dispose() { - It4WifiConnector localConnector = connector; - if (localConnector != null) { - localConnector.dispose(); - } + dataListeners.clear(); + freeKeepAlive(); + + sslSocket.ifPresent(socket -> { + try { + socket.close(); + } catch (IOException e) { + logger.warn("Error closing sslsocket : {}", e.getMessage()); + } + }); + sslSocket = Optional.empty(); + + connector.ifPresent(c -> scheduler.execute(() -> c.interrupt())); + connector = Optional.empty(); } private void startConnector() { It4WifiConfiguration config = getConfigAs(It4WifiConfiguration.class); freeKeepAlive(); - reqBuilder = new RequestBuilder(config.macAddress, config.username); - It4WifiConnector localConnector = new It4WifiConnector(config.hostname, this); - localConnector.start(); - connector = localConnector; + try { + logger.debug("Initiating connection to IT4Wifi {} on port {}...", config.hostname, SERVER_PORT); + + SSLSocket localSocket = (SSLSocket) socketFactory.createSocket(config.hostname, SERVER_PORT); + sslSocket = Optional.of(localSocket); + localSocket.startHandshake(); + + It4WifiConnector localConnector = new It4WifiConnector(this, localSocket); + connector = Optional.of(localConnector); + localConnector.start(); + + reqBuilder = new RequestBuilder(config.macAddress, config.username); + handShaked(); + } catch (UnknownHostException e) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "@text/conf-error-hostname"); + } catch (IOException e) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "@text/error-handshake-init"); + } } private void freeKeepAlive() { - ScheduledFuture keepAlive = keepAliveJob; - if (keepAlive != null) { - keepAlive.cancel(true); - } - keepAliveJob = null; + keepAliveJob.ifPresent(job -> job.cancel(true)); + keepAliveJob = Optional.empty(); } public void received(String command) { @@ -134,8 +164,8 @@ public class It4WifiHandler extends BaseBridgeHandler { if (event.error != null) { logger.warn("Error code {} received : {}", event.error.code, event.error.info); } else { - if (event instanceof Response) { - handleResponse((Response) event); + if (event instanceof Response responseEvent) { + handleResponse(responseEvent); } else { notifyListeners(event.getDevices()); } @@ -152,40 +182,35 @@ public class It4WifiHandler extends BaseBridgeHandler { sendCommand(CommandType.VERIFY); return; case VERIFY: - if (keepAliveJob != null) { // means we are connected - return; - } - switch (response.authentication.perm) { - case admin: - case user: - sendCommand(CommandType.CONNECT); - return; - case wait: - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_PENDING, - "@text/conf-pending-validation"); - scheduler.schedule(() -> handShaked(), 15, TimeUnit.SECONDS); - return; - default: - return; + if (keepAliveJob.isEmpty()) { // means we are not connected + switch (response.authentication.perm) { + case admin, user: + sendCommand(CommandType.CONNECT); + return; + case wait: + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_PENDING, + "@text/conf-pending-validation"); + scheduler.schedule(() -> handShaked(), 15, TimeUnit.SECONDS); + return; + } } + return; case CONNECT: String sc = response.authentication.sc; - It4WifiConfiguration config = getConfigAs(It4WifiConfiguration.class); if (sc != null) { + It4WifiConfiguration config = getConfigAs(It4WifiConfiguration.class); reqBuilder.setChallenges(sc, response.authentication.id, config.password); - keepAliveJob = scheduler.scheduleWithFixedDelay(() -> sendCommand(CommandType.VERIFY), - KEEPALIVE_DELAY_S, KEEPALIVE_DELAY_S, TimeUnit.SECONDS); + keepAliveJob = Optional.of(scheduler.scheduleWithFixedDelay(() -> sendCommand(CommandType.VERIFY), + KEEPALIVE_DELAY_S, KEEPALIVE_DELAY_S, TimeUnit.SECONDS)); sendCommand(CommandType.INFO); } return; case INFO: updateStatus(ThingStatus.ONLINE); if (thing.getProperties().isEmpty()) { - Map properties = Map.of(PROPERTY_VENDOR, response.intf.manuf, PROPERTY_MODEL_ID, - response.intf.prod, PROPERTY_SERIAL_NUMBER, response.intf.serialNr, - PROPERTY_HARDWARE_VERSION, response.intf.versionHW, PROPERTY_FIRMWARE_VERSION, - response.intf.versionFW); - updateProperties(properties); + updateProperties(Map.of(PROPERTY_VENDOR, response.intf.manuf, PROPERTY_MODEL_ID, response.intf.prod, + PROPERTY_SERIAL_NUMBER, response.intf.serialNr, PROPERTY_HARDWARE_VERSION, + response.intf.versionHW, PROPERTY_FIRMWARE_VERSION, response.intf.versionFW)); } notifyListeners(response.getDevices()); return; @@ -212,12 +237,8 @@ public class It4WifiHandler extends BaseBridgeHandler { } private void sendCommand(String command) { - It4WifiConnector localConnector = connector; - if (localConnector != null) { - localConnector.sendCommand(command); - } else { - logger.warn("Tried to send a command when IT4WifiConnector is not initialized."); - } + connector.ifPresentOrElse(c -> c.sendCommand(command), + () -> logger.warn("Tried to send a command when IT4WifiConnector is not initialized.")); } public void sendCommand(CommandType command) { @@ -232,13 +253,16 @@ public class It4WifiHandler extends BaseBridgeHandler { sendCommand(reqBuilder.buildMessage(id, t4)); } - public void connectorInterrupted(@Nullable String message) { - if (handshakeAttempts++ <= MAX_HANDSHAKE_ATTEMPTS) { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, message); - startConnector(); - } else { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "@text/error-handshake-limit"); - connector = null; + public void communicationError(String message) { + // avoid a status update that would generates a WARN while we're already disconnecting + if (getThing().getStatus().equals(ThingStatus.ONLINE)) { + dispose(); + if (handshakeAttempts++ <= MAX_HANDSHAKE_ATTEMPTS) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, message); + startConnector(); + } else { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "@text/error-handshake-limit"); + } } } } diff --git a/bundles/org.openhab.binding.mynice/src/main/java/org/openhab/binding/mynice/internal/xml/dto/Properties.java b/bundles/org.openhab.binding.mynice/src/main/java/org/openhab/binding/mynice/internal/xml/dto/Properties.java index e90b6c043..92026c025 100644 --- a/bundles/org.openhab.binding.mynice/src/main/java/org/openhab/binding/mynice/internal/xml/dto/Properties.java +++ b/bundles/org.openhab.binding.mynice/src/main/java/org/openhab/binding/mynice/internal/xml/dto/Properties.java @@ -20,10 +20,32 @@ import com.thoughtworks.xstream.annotations.XStreamAlias; */ @XStreamAlias("Properties") public class Properties { + public static enum DoorStatus { + OPEN(false), + CLOSED(false), + OPENING(true), + CLOSING(true), + STOPPED(false); + + public final boolean moving; + + DoorStatus(boolean moving) { + this.moving = moving; + } + } + @XStreamAlias("DoorStatus") - public String doorStatus; + private String doorStatus; @XStreamAlias("Obstruct") - public String obstruct; + private String obstruct; @XStreamAlias("T4_allowed") public Property t4allowed; + + public boolean obstructed() { + return "1".equals(obstruct); + } + + public DoorStatus status() { + return DoorStatus.valueOf(doorStatus.toUpperCase()); + } } diff --git a/bundles/org.openhab.binding.mynice/src/main/java/org/openhab/binding/mynice/internal/xml/dto/T4Command.java b/bundles/org.openhab.binding.mynice/src/main/java/org/openhab/binding/mynice/internal/xml/dto/T4Command.java index 3b4812d5b..2e432ba82 100644 --- a/bundles/org.openhab.binding.mynice/src/main/java/org/openhab/binding/mynice/internal/xml/dto/T4Command.java +++ b/bundles/org.openhab.binding.mynice/src/main/java/org/openhab/binding/mynice/internal/xml/dto/T4Command.java @@ -13,7 +13,6 @@ package org.openhab.binding.mynice.internal.xml.dto; import java.util.List; -import java.util.stream.Collectors; import java.util.stream.Stream; import org.eclipse.jdt.annotation.NonNullByDefault; @@ -61,7 +60,6 @@ public enum T4Command { } public static List fromBitmask(int bitmask) { - return Stream.of(T4Command.values()).filter(command -> ((1 << command.bitPosition) & bitmask) != 0) - .collect(Collectors.toList()); + return Stream.of(T4Command.values()).filter(command -> ((1 << command.bitPosition) & bitmask) != 0).toList(); } } diff --git a/bundles/org.openhab.binding.mynice/src/main/resources/OH-INF/i18n/mynice.properties b/bundles/org.openhab.binding.mynice/src/main/resources/OH-INF/i18n/mynice.properties index fe20f9f52..753bfc37b 100644 --- a/bundles/org.openhab.binding.mynice/src/main/resources/OH-INF/i18n/mynice.properties +++ b/bundles/org.openhab.binding.mynice/src/main/resources/OH-INF/i18n/mynice.properties @@ -31,48 +31,59 @@ thing-type.config.mynice.swing.id.description = ID of the gate on the TP4 bus co channel-type.mynice.command.label = Command channel-type.mynice.command.description = Send a given command to the gate -channel-type.mynice.command.state.option.stop = Stop -channel-type.mynice.command.state.option.open = Open -channel-type.mynice.command.state.option.close = Close +channel-type.mynice.command.command.option.stop = Stop +channel-type.mynice.command.command.option.open = Open +channel-type.mynice.command.command.option.close = Close +channel-type.mynice.courtesy.label = Courtesy Light +channel-type.mynice.courtesy.description = Courtesy Light illuminates the area around your gates. channel-type.mynice.doorstatus.label = Gate Status channel-type.mynice.doorstatus.description = Position of the gate or state if moving -channel-type.mynice.doorstatus.state.option.open = Open -channel-type.mynice.doorstatus.state.option.closed = Closed -channel-type.mynice.doorstatus.state.option.opening = Opening -channel-type.mynice.doorstatus.state.option.closing = Closing -channel-type.mynice.doorstatus.state.option.stopped = Stopped +channel-type.mynice.doorstatus.state.option.OPEN = Open +channel-type.mynice.doorstatus.state.option.CLOSED = Closed +channel-type.mynice.doorstatus.state.option.OPENING = Opening +channel-type.mynice.doorstatus.state.option.CLOSING = Closing +channel-type.mynice.doorstatus.state.option.STOPPED = Stopped +channel-type.mynice.doorstatus.command.option.STOP = Stop +channel-type.mynice.doorstatus.command.option.MOVE = Move channel-type.mynice.moving.label = Moving channel-type.mynice.moving.description = Indicates if the device is currently operating a command channel-type.mynice.obstruct.label = Obstruction channel-type.mynice.obstruct.description = Something prevented normal operation of the gate by crossing the infra-red barrier channel-type.mynice.t4command.label = T4 Command channel-type.mynice.t4command.description = Send a T4 Command to the gate -channel-type.mynice.t4command.state.option.MDAx = Step by Step -channel-type.mynice.t4command.state.option.MDAy = Stop (as remote control) -channel-type.mynice.t4command.state.option.MDAz = Open (as remote control) -channel-type.mynice.t4command.state.option.MDA0 = Close (as remote control) -channel-type.mynice.t4command.state.option.MDA1 = Partial opening 1 -channel-type.mynice.t4command.state.option.MDA2 = Partial opening 2 -channel-type.mynice.t4command.state.option.MDA3 = Partial opening 3 -channel-type.mynice.t4command.state.option.MDBi = Apartment Step by Step -channel-type.mynice.t4command.state.option.MDBj = Step by Step high priority -channel-type.mynice.t4command.state.option.MDBk = Open and block -channel-type.mynice.t4command.state.option.MDBl = Close and block -channel-type.mynice.t4command.state.option.MDBm = Block -channel-type.mynice.t4command.state.option.MDEw = Release -channel-type.mynice.t4command.state.option.MDEx = Courtesy light timer on -channel-type.mynice.t4command.state.option.MDEy = Courtesy light on-off -channel-type.mynice.t4command.state.option.MDEz = Step by Step master door -channel-type.mynice.t4command.state.option.MDE0 = Open master door -channel-type.mynice.t4command.state.option.MDE1 = Close master door -channel-type.mynice.t4command.state.option.MDE2 = Step by Step slave door -channel-type.mynice.t4command.state.option.MDE3 = Open slave door -channel-type.mynice.t4command.state.option.MDE4 = Close slave door -channel-type.mynice.t4command.state.option.MDE5 = Release and Open -channel-type.mynice.t4command.state.option.MDFh = Release and Close +channel-type.mynice.t4command.command.option.MDAx = Step by Step +channel-type.mynice.t4command.command.option.MDAy = Stop (as remote control) +channel-type.mynice.t4command.command.option.MDAz = Open (as remote control) +channel-type.mynice.t4command.command.option.MDA0 = Close (as remote control) +channel-type.mynice.t4command.command.option.MDA1 = Partial opening 1 +channel-type.mynice.t4command.command.option.MDA2 = Partial opening 2 +channel-type.mynice.t4command.command.option.MDA3 = Partial opening 3 +channel-type.mynice.t4command.command.option.MDBi = Apartment Step by Step +channel-type.mynice.t4command.command.option.MDBj = Step by Step high priority +channel-type.mynice.t4command.command.option.MDBk = Open and block +channel-type.mynice.t4command.command.option.MDBl = Close and block +channel-type.mynice.t4command.command.option.MDBm = Block +channel-type.mynice.t4command.command.option.MDEw = Release +channel-type.mynice.t4command.command.option.MDEx = Courtesy light timer on +channel-type.mynice.t4command.command.option.MDEy = Courtesy light on-off +channel-type.mynice.t4command.command.option.MDEz = Step by Step master door +channel-type.mynice.t4command.command.option.MDE0 = Open master door +channel-type.mynice.t4command.command.option.MDE1 = Close master door +channel-type.mynice.t4command.command.option.MDE2 = Step by Step slave door +channel-type.mynice.t4command.command.option.MDE3 = Open slave door +channel-type.mynice.t4command.command.option.MDE4 = Close slave door +channel-type.mynice.t4command.command.option.MDE5 = Release and Open +channel-type.mynice.t4command.command.option.MDFh = Release and Close + +# channel types config + +channel-type.config.mynice.courtesy.duration.label = Duration +channel-type.config.mynice.courtesy.duration.description = Duration the lamp stays on # error messages conf-error-no-username = Please define a username for this thing conf-pending-validation = Please validate the user on the MyNice application +conf-error-hostname = Unable to reach the configured hostname error-handshake-limit = Maximum handshake attempts reached +error-handshake-init = Error initializing communication with IT4Wifi diff --git a/bundles/org.openhab.binding.mynice/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.mynice/src/main/resources/OH-INF/thing/thing-types.xml index 4809d4f14..c8272a6ca 100644 --- a/bundles/org.openhab.binding.mynice/src/main/resources/OH-INF/thing/thing-types.xml +++ b/bundles/org.openhab.binding.mynice/src/main/resources/OH-INF/thing/thing-types.xml @@ -47,8 +47,13 @@ + + + 1 + + id @@ -73,8 +78,13 @@ + + + 1 + + id @@ -90,15 +100,23 @@ String Position of the gate or state if moving - + door + - - - - - + + + + + + + + + + + + veto @@ -115,17 +133,17 @@ - + String Send a given command to the gate - + - + veto @@ -133,7 +151,7 @@ String Send a T4 Command to the gate - + @@ -159,8 +177,22 @@ - + veto + + Switch + + Courtesy Light illuminates the area around your gates. + lightbulb + + + + Duration the lamp stays on + 60 + + + + diff --git a/bundles/org.openhab.binding.mynice/src/main/resources/OH-INF/update/instructions.xml b/bundles/org.openhab.binding.mynice/src/main/resources/OH-INF/update/instructions.xml new file mode 100644 index 000000000..f3a2b62ee --- /dev/null +++ b/bundles/org.openhab.binding.mynice/src/main/resources/OH-INF/update/instructions.xml @@ -0,0 +1,26 @@ + + + + + + + + mynice:courtesy + + + + + + + + + + mynice:courtesy + + + + + +