[MyNice] Addition of Courtesy Light Channel (#14797)

* Solving activation / deactivation of IT4Wifi thing glitches.
* Adding Courtesy light
Added command capability of Stop / Move
* Changed misplaced handling of RefreshType

---------

Signed-off-by: clinique <gael@lhopital.org>
This commit is contained in:
Gaël L'hopital 2023-07-12 23:01:08 +02:00 committed by GitHub
parent 80eeba48ce
commit 41e4cc4545
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 406 additions and 234 deletions

View File

@ -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" <gate> (gMyniceSwing) ["Statu
String NiceIT4WIFI_Obstruction "Obstruction" <none> (gMyniceSwing) {channel="mynice:swing:83eef09166:1:obstruct"}
Switch NiceIT4WIFI_Moving "Moving" <motion> (gMyniceSwing) ["Status","Vibration"] {channel="mynice:swing:83eef09166:1:moving"}
String NiceIT4WIFI_Command "Command" <none> (gMyniceSwing) {channel="mynice:swing:83eef09166:1:command"}
Switch NiceIT4WIFI_Command "Courtesy Light" <light> (gMyniceSwing) {channel="mynice:swing:83eef09166:1:courtesy"}
```

View File

@ -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");

View File

@ -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<ThingTypeUID> 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)) {

View File

@ -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;
}

View File

@ -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<It4WifiHandler> 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<Device> 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));
}
}

View File

@ -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<DoorStatus> gateStatus = Optional.empty();
private List<T4Command> 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<Device> 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<String> 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;
});

View File

@ -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();
}
}

View File

@ -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<MyNiceDataListener> 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<Device> devices = new ArrayList<>();
private int handshakeAttempts = 0;
private Optional<ScheduledFuture<?>> keepAliveJob = Optional.empty();
private Optional<It4WifiConnector> connector = Optional.empty();
private Optional<SSLSocket> 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<String, String> 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");
}
}
}
}

View File

@ -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());
}
}

View File

@ -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<T4Command> 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();
}
}

View File

@ -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

View File

@ -47,8 +47,13 @@
<channel id="moving" typeId="moving"/>
<channel id="command" typeId="command"/>
<channel id="t4command" typeId="t4command"/>
<channel id="courtesy" typeId="courtesy"/>
</channels>
<properties>
<property name="thingTypeVersion">1</property>
</properties>
<representation-property>id</representation-property>
<config-description>
@ -73,8 +78,13 @@
<channel id="moving" typeId="moving"/>
<channel id="command" typeId="command"/>
<channel id="t4command" typeId="t4command"/>
<channel id="courtesy" typeId="courtesy"/>
</channels>
<properties>
<property name="thingTypeVersion">1</property>
</properties>
<representation-property>id</representation-property>
<config-description>
@ -90,15 +100,23 @@
<item-type>String</item-type>
<label>Gate Status</label>
<description>Position of the gate or state if moving</description>
<state readOnly="true">
<category>door</category>
<state>
<options>
<option value="open">Open</option>
<option value="closed">Closed</option>
<option value="opening">Opening</option>
<option value="closing">Closing</option>
<option value="stopped">Stopped</option>
<option value="OPEN">Open</option>
<option value="CLOSED">Closed</option>
<option value="OPENING">Opening</option>
<option value="CLOSING">Closing</option>
<option value="STOPPED">Stopped</option>
</options>
</state>
<command>
<options>
<option value="STOP">Stop</option>
<option value="MOVE">Move</option>
</options>
</command>
<autoUpdatePolicy>veto</autoUpdatePolicy>
</channel-type>
<channel-type id="moving">
@ -115,17 +133,17 @@
<state readOnly="true"/>
</channel-type>
<channel-type id="command">
<channel-type id="command" advanced="true">
<item-type>String</item-type>
<label>Command</label>
<description>Send a given command to the gate</description>
<state readOnly="false">
<command>
<options>
<option value="stop">Stop</option>
<option value="open">Open</option>
<option value="close">Close</option>
</options>
</state>
</command>
<autoUpdatePolicy>veto</autoUpdatePolicy>
</channel-type>
@ -133,7 +151,7 @@
<item-type>String</item-type>
<label>T4 Command</label>
<description>Send a T4 Command to the gate</description>
<state readOnly="false">
<command>
<options>
<option value="MDAx">Step by Step</option>
<option value="MDAy">Stop (as remote control)</option>
@ -159,8 +177,22 @@
<option value="MDE5">Release and Open</option>
<option value="MDFh">Release and Close</option>
</options>
</state>
</command>
<autoUpdatePolicy>veto</autoUpdatePolicy>
</channel-type>
<channel-type id="courtesy">
<item-type>Switch</item-type>
<label>Courtesy Light</label>
<description>Courtesy Light illuminates the area around your gates.</description>
<category>lightbulb</category>
<config-description>
<parameter name="duration" type="integer" min="0" unit="s" step="1">
<label>Duration</label>
<description>Duration the lamp stays on</description>
<default>60</default>
</parameter>
</config-description>
</channel-type>
</thing:thing-descriptions>

View File

@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>
<update:update-descriptions xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:update="https://openhab.org/schemas/update-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/update-description/v1.0.0 https://openhab.org/schemas/update-description-1.0.0.xsd">
<thing-type uid="mynice:swing">
<instruction-set targetVersion="1">
<add-channel id="courtesy">
<type>mynice:courtesy</type>
</add-channel>
</instruction-set>
</thing-type>
<thing-type uid="mynice:sliding">
<instruction-set targetVersion="1">
<add-channel id="courtesy">
<type>mynice:courtesy</type>
</add-channel>
</instruction-set>
</thing-type>
</update:update-descriptions>