[mynice] Binding for IT4Wifi module (Nice gate doors) (#12940)

Signed-off-by: clinique <gael@lhopital.org>
This commit is contained in:
Gaël L'hopital 2023-03-10 10:33:39 +01:00 committed by GitHub
parent d130595f85
commit 3b5dfb11a0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
31 changed files with 1887 additions and 0 deletions

View File

@ -217,6 +217,7 @@
/bundles/org.openhab.binding.mqtt.homie/ @davidgraeff
/bundles/org.openhab.binding.mycroft/ @dalgwen
/bundles/org.openhab.binding.mybmw/ @weymann @ntruchsess
/bundles/org.openhab.binding.mynice/ @clinique
/bundles/org.openhab.binding.myq/ @digitaldan
/bundles/org.openhab.binding.mystrom/ @pail23
/bundles/org.openhab.binding.nanoleaf/ @raepple @stefan-hoehn

View File

@ -1081,6 +1081,11 @@
<artifactId>org.openhab.binding.mybmw</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.openhab.addons.bundles</groupId>
<artifactId>org.openhab.binding.mynice</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.openhab.addons.bundles</groupId>
<artifactId>org.openhab.binding.myq</artifactId>

View File

@ -0,0 +1,13 @@
This content is produced and maintained by the openHAB project.
* Project home: https://www.openhab.org
== Declared Project Licenses
This program and the accompanying materials are made available under the terms
of the Eclipse Public License 2.0 which is available at
https://www.eclipse.org/legal/epl-2.0/.
== Source Code
https://github.com/openhab/openhab-addons

View File

@ -0,0 +1,114 @@
# MyNice Binding
This binding implements the support of the IT4Wifi module through the NHK protocol and enables management of Nice gates actuators.
IT4Wifi is a bridge between the TP4 bus of your gate and your Ethernet network.
## Supported Things
- `it4wifi`: The Bridge between openHAB and your module.
- `swing`: A Thing representing a swinging (two rotating doors) gate.
- `sliding`: A Thing representing a sliding gate.
## Discovery
The binding will auto-discover (by MDNS) your module, creating the associated `it4wifi` bridge.
Once discovered, a user named “org.openhab.binding.mynice” will be created on it.
You will have to grant him permissions using the MyNice app (Android or IOS).
Once configuration of the bridge is completed, your gate(s) will also be auto-discovered and added to the Inbox.
## Thing Configuration
First configuration should be done via UI discovery, this will let you get automatically the password provided by the IT4Wifi module.
Once done, you can also create your things via *.things file.
### `it4wifi` Bridge Configuration
| Name | Type | Description | Default | Required | Advanced |
|------------|------|------------------------------------------------------------------------|---------|----------|----------|
| hostname | text | Hostname or IP address of the device | N/A | yes | no |
| password | text | Password to access the device | N/A | yes | no |
| macAddress | text | The MAC address of the IT4Wifi | N/A | yes | no |
| username | text | Pairing Key needed to access the device, provided by the bridge itself | N/A | yes | no |
### Gates Thing Configuration
| Name | Type | Description | Default | Required | Advanced |
|------------|------|------------------------------------------------------------------------|---------|----------|----------|
| id | text | ID of the gate on the TP4 bus connected to the bridge | N/A | yes | no |
## Channels
There is no channel associated with the bridge.
Channels available for the gates are :
| Channel | Type | Read/Write | Description |
|-----------|--------|------------|----------------------------------------------------------|
| status | String | R | Description of the current status of the door (1) |
| 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) |
| t4command | String | W | Send a T4 Command to the gate |
(1) : can be open, closed, opening, closing, stopped.
(2) : must be "stop","open","close"
### T4 Commands
Depending upon your gate model and motor capabilities, some T4 commands can be used.
The list of available commands for your model will be automatically discovered by the binding.
This information is stored in the `allowedT4` property held by the gate Thing itself.
Complete list of T4 Commands :
| Command | Action |
|---------|----------------------------|
| MDAx | Step by Step |
| MDAy | Stop (as remote control) |
| MDAz | Open (as remote control) |
| MDA0 | Close (as remote control) |
| MDA1 | Partial opening 1 |
| MDA2 | Partial opening 2 |
| MDA3 | Partial opening 3 |
| MDBi | Apartment Step by Step |
| MDBj | Step by Step high priority |
| MDBk | Open and block |
| MDBl | Close and block |
| MDBm | Block |
| MDEw | Release |
| MDEx | Courtesy ligh timer on |
| MDEy | Courtesy light on-off |
| MDEz | Step by Step master door |
| MDE0 | Open master door |
| MDE1 | Close master door |
| MDE2 | Step by Step slave door |
| MDE3 | Open slave door |
| MDE4 | Close slave door |
| MDE5 | Release and Open |
| MDFh | Release and Close |
## Full Example
### things/mynice.things
```java
Bridge mynice:it4wifi:83eef09166 "Nice IT4WIFI" @ "portail" [
hostname="192.168.0.198",
macAddress="00:xx:zz:dd:ff:gg",
password="v***************************zU=",
username="neo_prod"] {
swing 1 "Nice POA3 Moteur Portail" @ "portail" [id="1"]
}
```
### items/mynice.items
```java
String NiceIT4WIFI_GateStatus "Gate Status" <gate> (gMyniceSwing) ["Status","Opening"] {channel="mynice:swing:83eef09166:1:status"}
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"}
```

View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.openhab.addons.bundles</groupId>
<artifactId>org.openhab.addons.reactor.bundles</artifactId>
<version>4.0.0-SNAPSHOT</version>
</parent>
<artifactId>org.openhab.binding.mynice</artifactId>
<name>openHAB Add-ons :: Bundles :: MyNice Binding</name>
</project>

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<features name="org.openhab.binding.mynice-${project.version}" xmlns="http://karaf.apache.org/xmlns/features/v1.4.0">
<repository>mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features</repository>
<feature name="openhab-binding-mynice" description="MyNice Binding" version="${project.version}">
<feature>openhab-runtime-base</feature>
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.mynice/${project.version}</bundle>
</feature>
</features>

View File

@ -0,0 +1,43 @@
/**
* 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;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.thing.ThingTypeUID;
/**
* The {@link MyNiceBindingConstants} class defines common constants, which are used across the whole binding.
*
* @author Gaël L'hopital - Initial contribution
*/
@NonNullByDefault
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";
// List of all Thing Type UIDs
public static final ThingTypeUID BRIDGE_TYPE_IT4WIFI = new ThingTypeUID(BINDING_ID, "it4wifi");
public static final ThingTypeUID THING_TYPE_SWING = new ThingTypeUID(BINDING_ID, "swing");
public static final ThingTypeUID THING_TYPE_SLIDING = new ThingTypeUID(BINDING_ID, "sliding");
// Configuration element of a portal
public static final String DEVICE_ID = "id";
public static final String ALLOWED_T4 = "allowedT4";
}

View File

@ -0,0 +1,61 @@
/**
* 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;
import static org.openhab.binding.mynice.internal.MyNiceBindingConstants.*;
import java.util.Set;
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.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.Component;
/**
* The {@link MyNiceHandlerFactory} is responsible for creating thing handlers.
*
* @author Gaël L'hopital - Initial contribution
*/
@NonNullByDefault
@Component(configurationPid = "binding.mynice", service = ThingHandlerFactory.class)
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);
@Override
public boolean supportsThingType(ThingTypeUID thingTypeUID) {
return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID);
}
@Override
protected @Nullable ThingHandler createHandler(Thing thing) {
ThingTypeUID thingTypeUID = thing.getThingTypeUID();
if (BRIDGE_TYPE_IT4WIFI.equals(thingTypeUID)) {
return new It4WifiHandler((Bridge) thing);
} else if (THING_TYPE_SWING.equals(thingTypeUID)) {
return new GateHandler(thing);
} else if (THING_TYPE_SLIDING.equals(thingTypeUID)) {
return new GateHandler(thing);
}
return null;
}
}

View File

@ -0,0 +1,31 @@
/**
* 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 It4WifiConfiguration} class contains fields mapping thing configuration parameters.
*
* @author Gaël L'hopital - Initial contribution
*/
@NonNullByDefault
public class It4WifiConfiguration {
public static final String PASSWORD = "password";
public static final String HOSTNAME = "hostname";
public String username = "";
public String hostname = "";
public String macAddress = "";
public String password = "";
}

View File

@ -0,0 +1,85 @@
/**
* 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.discovery;
import static org.openhab.binding.mynice.internal.MyNiceBindingConstants.BRIDGE_TYPE_IT4WIFI;
import static org.openhab.binding.mynice.internal.config.It4WifiConfiguration.HOSTNAME;
import static org.openhab.core.thing.Thing.PROPERTY_MAC_ADDRESS;
import java.util.Map;
import java.util.Set;
import java.util.regex.Pattern;
import javax.jmdns.ServiceInfo;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.config.discovery.DiscoveryResult;
import org.openhab.core.config.discovery.DiscoveryResultBuilder;
import org.openhab.core.config.discovery.mdns.MDNSDiscoveryParticipant;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.ThingUID;
import org.osgi.service.component.annotations.Component;
/**
* The {@link MyNiceDiscoveryParticipant} is responsible for discovering the IT4Wifi bridge using mDNS discovery service
*
* @author Gaël L'hopital - Initial contribution
*/
@Component
@NonNullByDefault
public class MyNiceDiscoveryParticipant implements MDNSDiscoveryParticipant {
private static final String PROPERTY_MODEL = "model";
private static final String PROPERTY_DEVICE_ID = "deviceid";
private static final String MAC_REGEX = "^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})|([0-9a-fA-F]{4}\\.[0-9a-fA-F]{4}\\.[0-9a-fA-F]{4})$";
private static final Pattern MAC_PATTERN = Pattern.compile(MAC_REGEX);
@Override
public Set<ThingTypeUID> getSupportedThingTypeUIDs() {
return Set.of(BRIDGE_TYPE_IT4WIFI);
}
@Override
public String getServiceType() {
return "_nap._tcp.local.";
}
@Override
public @Nullable DiscoveryResult createResult(ServiceInfo service) {
ThingUID thingUID = getThingUID(service);
String[] hostNames = service.getHostAddresses();
if (thingUID != null && hostNames.length > 0) {
String label = service.getPropertyString(PROPERTY_MODEL);
String macAddress = service.getPropertyString(PROPERTY_DEVICE_ID);
return DiscoveryResultBuilder.create(thingUID).withLabel(label)
.withRepresentationProperty(PROPERTY_MAC_ADDRESS).withThingType(BRIDGE_TYPE_IT4WIFI)
.withProperties(Map.of(HOSTNAME, hostNames[0], PROPERTY_MAC_ADDRESS, macAddress)).build();
}
return null;
}
@Override
public @Nullable ThingUID getThingUID(ServiceInfo service) {
String macAddress = service.getPropertyString(PROPERTY_DEVICE_ID);
if (macAddress != null && validate(macAddress)) {
macAddress = macAddress.replaceAll("[^a-fA-F0-9]", "").toLowerCase();
return new ThingUID(BRIDGE_TYPE_IT4WIFI, macAddress);
}
return null;
}
private boolean validate(String mac) {
return MAC_PATTERN.matcher(mac).find();
}
}

View File

@ -0,0 +1,118 @@
/**
* 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.discovery;
import static org.openhab.binding.mynice.internal.MyNiceBindingConstants.*;
import java.util.List;
import java.util.Set;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.mynice.internal.handler.It4WifiHandler;
import org.openhab.binding.mynice.internal.handler.MyNiceDataListener;
import org.openhab.binding.mynice.internal.xml.dto.CommandType;
import org.openhab.binding.mynice.internal.xml.dto.Device;
import org.openhab.core.config.discovery.AbstractDiscoveryService;
import org.openhab.core.config.discovery.DiscoveryResult;
import org.openhab.core.config.discovery.DiscoveryResultBuilder;
import org.openhab.core.thing.ThingUID;
import org.openhab.core.thing.binding.ThingHandler;
import org.openhab.core.thing.binding.ThingHandlerService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link MyNiceDiscoveryService} is responsible for discovering all things
* except the It4Wifi bridge itself
*
* @author Gaël L'hopital - Initial contribution
*/
@NonNullByDefault
public class MyNiceDiscoveryService extends AbstractDiscoveryService
implements MyNiceDataListener, ThingHandlerService {
private static final int SEARCH_TIME = 5;
private final Logger logger = LoggerFactory.getLogger(MyNiceDiscoveryService.class);
private @Nullable It4WifiHandler bridgeHandler;
/**
* Creates a MyNiceDiscoveryService with background discovery disabled.
*/
public MyNiceDiscoveryService() {
super(Set.of(THING_TYPE_SWING), SEARCH_TIME, false);
}
@Override
public void setThingHandler(ThingHandler handler) {
if (handler instanceof It4WifiHandler it4Handler) {
bridgeHandler = it4Handler;
}
}
@Override
public @Nullable ThingHandler getThingHandler() {
return bridgeHandler;
}
@Override
public void activate() {
super.activate(null);
It4WifiHandler handler = bridgeHandler;
if (handler != null) {
handler.registerDataListener(this);
}
}
@Override
public void deactivate() {
It4WifiHandler handler = bridgeHandler;
if (handler != null) {
handler.unregisterDataListener(this);
}
super.deactivate();
}
@Override
public void onDataFetched(List<Device> devices) {
It4WifiHandler handler = bridgeHandler;
if (handler != null) {
ThingUID bridgeUID = handler.getThing().getUID();
devices.stream().filter(device -> device.type != null).forEach(device -> {
ThingUID thingUID = switch (device.type) {
case SWING -> new ThingUID(THING_TYPE_SWING, bridgeUID, device.id);
case SLIDING -> new ThingUID(THING_TYPE_SLIDING, bridgeUID, device.id);
default -> null;
};
if (thingUID != null) {
DiscoveryResult discoveryResult = DiscoveryResultBuilder.create(thingUID).withBridge(bridgeUID)
.withLabel(String.format("%s %s", device.manuf, device.prod))
.withRepresentationProperty(DEVICE_ID).withProperty(DEVICE_ID, device.id).build();
thingDiscovered(discoveryResult);
} else {
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);
}
}
}

View File

@ -0,0 +1,129 @@
/**
* 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.handler;
import static org.openhab.binding.mynice.internal.MyNiceBindingConstants.*;
import static org.openhab.core.thing.Thing.*;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import org.eclipse.jdt.annotation.NonNullByDefault;
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.T4Command;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.StringType;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.binding.BaseThingHandler;
import org.openhab.core.thing.binding.BridgeHandler;
import org.openhab.core.types.Command;
import org.openhab.core.types.RefreshType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
*
* @author Gaël L'hopital - Initial contribution
*/
@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 = "";
public GateHandler(Thing thing) {
super(thing);
}
@Override
public void initialize() {
id = (String) getConfig().get(DEVICE_ID);
getBridgeHandler().ifPresent(h -> h.registerDataListener(this));
}
@Override
public void dispose() {
getBridgeHandler().ifPresent(h -> h.unregisterDataListener(this));
}
private Optional<It4WifiHandler> getBridgeHandler() {
Bridge bridge = getBridge();
if (bridge != null) {
BridgeHandler handler = bridge.getHandler();
if (handler instanceof It4WifiHandler it4Handler) {
return Optional.of(it4Handler);
}
}
return Optional.empty();
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
if (command instanceof RefreshType) {
return;
} else {
handleCommand(channelUID.getId(), command.toString());
}
}
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);
}
});
} else {
logger.warn("This thing does not accept the T4 command '{}'", command);
}
}
}
@Override
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)));
}
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)));
}
return true;
});
}
}

View File

@ -0,0 +1,141 @@
/**
* 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.handler;
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.
*
* @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 @NonNullByDefault({}) InputStreamReader in;
private @NonNullByDefault({}) OutputStreamWriter out;
public It4WifiConnector(String hostname, It4WifiHandler handler) {
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);
}
}
@Override
public void run() {
String buffer = "";
try {
connect();
while (!interrupted()) {
int data;
while ((data = in.read()) != -1) {
if (data == STX) {
buffer = "";
} else if (data == ETX) {
handler.received(buffer);
} else {
buffer += (char) data;
}
}
}
handler.connectorInterrupted("IT4WifiConnector interrupted");
dispose();
} catch (IOException e) {
handler.connectorInterrupted(e.getMessage());
}
}
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());
}
}
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();
try {
sslsocket.close();
} catch (IOException e) {
logger.warn("Error closing sslsocket : {}", 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

@ -0,0 +1,244 @@
/**
* 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.handler;
import static org.openhab.core.thing.Thing.*;
import static org.openhab.core.types.RefreshType.REFRESH;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
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;
import org.openhab.binding.mynice.internal.xml.RequestBuilder;
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.Event;
import org.openhab.binding.mynice.internal.xml.dto.Response;
import org.openhab.binding.mynice.internal.xml.dto.T4Command;
import org.openhab.core.config.core.Configuration;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingStatusDetail;
import org.openhab.core.thing.binding.BaseBridgeHandler;
import org.openhab.core.thing.binding.ThingHandlerService;
import org.openhab.core.types.Command;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link It4WifiHandler} is responsible for handling commands, which are
* sent to one of the channels.
*
* @author Gaël L'hopital - Initial contribution
*/
@NonNullByDefault
public class It4WifiHandler extends BaseBridgeHandler {
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 @NonNullByDefault({}) RequestBuilder reqBuilder;
private @Nullable It4WifiConnector connector;
private @Nullable ScheduledFuture<?> keepAliveJob;
private List<Device> devices = new ArrayList<>();
private int handshakeAttempts = 0;
public It4WifiHandler(Bridge thing) {
super(thing);
}
@Override
public Collection<Class<? extends ThingHandlerService>> getServices() {
return Set.of(MyNiceDiscoveryService.class);
}
public void registerDataListener(MyNiceDataListener dataListener) {
dataListeners.add(dataListener);
notifyListeners(devices);
}
public void unregisterDataListener(MyNiceDataListener dataListener) {
dataListeners.remove(dataListener);
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
if (REFRESH.equals(command)) {
sendCommand(CommandType.INFO);
}
}
@Override
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());
}
}
@Override
public void dispose() {
It4WifiConnector localConnector = connector;
if (localConnector != null) {
localConnector.dispose();
}
freeKeepAlive();
}
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;
}
private void freeKeepAlive() {
ScheduledFuture<?> keepAlive = keepAliveJob;
if (keepAlive != null) {
keepAlive.cancel(true);
}
keepAliveJob = null;
}
public void received(String command) {
logger.debug("Received : {}", command);
Event event = xstream.deserialize(command);
if (event.error != null) {
logger.warn("Error code {} received : {}", event.error.code, event.error.info);
} else {
if (event instanceof Response) {
handleResponse((Response) event);
} else {
notifyListeners(event.getDevices());
}
}
}
private void handleResponse(Response response) {
switch (response.type) {
case PAIR:
Configuration thingConfig = editConfiguration();
thingConfig.put(It4WifiConfiguration.PASSWORD, response.authentication.pwd);
updateConfiguration(thingConfig);
logger.info("Pairing key updated in Configuration.");
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;
}
case CONNECT:
String sc = response.authentication.sc;
It4WifiConfiguration config = getConfigAs(It4WifiConfiguration.class);
if (sc != null) {
reqBuilder.setChallenges(sc, response.authentication.id, config.password);
keepAliveJob = 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);
}
notifyListeners(response.getDevices());
return;
case STATUS:
notifyListeners(response.getDevices());
return;
case CHANGE:
logger.debug("Change command accepted");
return;
default:
logger.warn("Unhandled response type : {}", response.type);
}
}
public void handShaked() {
handshakeAttempts = 0;
It4WifiConfiguration config = getConfigAs(It4WifiConfiguration.class);
sendCommand(config.password.isBlank() ? CommandType.PAIR : CommandType.VERIFY);
}
private void notifyListeners(List<Device> list) {
devices = list;
dataListeners.forEach(listener -> listener.onDataFetched(devices));
}
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.");
}
}
public void sendCommand(CommandType command) {
sendCommand(reqBuilder.buildMessage(command));
}
public void sendCommand(String id, String command) {
sendCommand(reqBuilder.buildMessage(id, command.toLowerCase()));
}
public void sendCommand(String id, T4Command t4) {
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;
}
}
}

View File

@ -0,0 +1,30 @@
/**
* 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.handler;
import java.util.List;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.mynice.internal.xml.dto.Device;
/**
* The {@link MyNiceDataListener} is notified by the bridge thing handler with updated data from
* the IP4Wifi.
*
* @author Gaël L'hopital - Initial contribution
*/
@NonNullByDefault
public interface MyNiceDataListener {
public void onDataFetched(List<Device> devices);
}

View File

@ -0,0 +1,63 @@
/**
* 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.xml;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.mynice.internal.xml.dto.Authentication;
import org.openhab.binding.mynice.internal.xml.dto.Authentication.UserPerm;
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.Device.DeviceType;
import org.openhab.binding.mynice.internal.xml.dto.Error;
import org.openhab.binding.mynice.internal.xml.dto.Event;
import org.openhab.binding.mynice.internal.xml.dto.Interface;
import org.openhab.binding.mynice.internal.xml.dto.Properties;
import org.openhab.binding.mynice.internal.xml.dto.Response;
import com.thoughtworks.xstream.XStream;
import com.thoughtworks.xstream.io.xml.StaxDriver;
/**
* The {@link MyNiceXStream} class is a utility class that wraps an XStream object and provide additional
* functionality specific to the MyNice binding. It automatically load the correct converter classes and
* processes the XStream annotations used by the object classes.
*
* @author Gaël L'hopital - Initial contribution
*/
@NonNullByDefault
public class MyNiceXStream extends XStream {
public static final String XML_HEADER = "<?xml version=\"1.0\" encoding=\"UTF-8\" ?>";
public MyNiceXStream() {
super(new StaxDriver());
allowTypesByWildcard(new String[] { Response.class.getPackageName() + ".**" });
setClassLoader(getClass().getClassLoader());
autodetectAnnotations(true);
ignoreUnknownElements();
alias("Response", Response.class);
alias("Event", Event.class);
alias("Authentication", Authentication.class);
alias("CommandType", CommandType.class);
alias("UserPerm", UserPerm.class);
alias("DeviceType", DeviceType.class);
alias("Error", Error.class);
alias("Interface", Interface.class);
alias("Device", Device.class);
alias("Properties", Properties.class);
}
public Event deserialize(String response) {
return (Event) fromXML(XML_HEADER + response);
}
}

View File

@ -0,0 +1,128 @@
/**
* 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.xml;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Base64;
import java.util.Base64.Encoder;
import java.util.UUID;
import javax.xml.bind.DatatypeConverter;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.mynice.internal.xml.dto.CommandType;
import org.openhab.binding.mynice.internal.xml.dto.T4Command;
/**
* The {@link RequestBuilder} is responsible for building a string request from the CommandType
*
* @author Gaël L'hopital - Initial contribution
*/
@NonNullByDefault
public class RequestBuilder {
private static final Encoder BASE64_ENCODER = Base64.getEncoder();
public static final String USERNAME = "%un%";
public static final String CLIENT_CHALLENGE = "%cc%";
private static final String START_REQUEST = "<Request id=\"%s\" source=\"openhab\" target=\"%s\" gw=\"gwID\" protocolType=\"NHK\" protocolVersion=\"1.0\" type=\"%s\">\r\n";
private static final String END_REQUEST = "%s%s</Request>";
private static final String DOOR_ACTION = "<DoorAction>%s</DoorAction>";
private static final String T4_ACTION = "<T4Action>%s</T4Action>";
private static final String SIGN = "<Sign>%s</Sign>";
private final String clientChallenge = UUID.randomUUID().toString().substring(0, 8);
private final byte[] clientChallengeArr = invertArray(DatatypeConverter.parseHexBinary(clientChallenge));
private final MessageDigest digest;
private final String it4WifiMac;
private final String username;
private int sessionId = 0;
private int commandSequence = 0;
private byte[] sessionPassword = {};
public RequestBuilder(String it4WifiMac, String username) {
try {
this.digest = MessageDigest.getInstance("SHA-256");
this.it4WifiMac = it4WifiMac;
this.username = username;
} catch (NoSuchAlgorithmException e) {
throw new IllegalArgumentException(e);
}
}
private String buildSign(CommandType command, String message) {
if (command.signNeeded) {
byte[] msgHash = sha256(message.getBytes());
byte[] sign = sha256(msgHash, sessionPassword);
return String.format(SIGN, BASE64_ENCODER.encodeToString(sign));
}
return "";
}
public String buildMessage(String id, String command) {
return buildMessage(CommandType.CHANGE, id, String.format(DOOR_ACTION, command.toLowerCase()));
}
public String buildMessage(String id, T4Command t4) {
return buildMessage(CommandType.CHANGE, id, String.format(T4_ACTION, t4.name()));
}
public String buildMessage(CommandType command, Object... bodyParms) {
String startRequest = String.format(START_REQUEST, getCommandId(), it4WifiMac, command);
String body = startRequest + getBody(command, bodyParms);
String sign = buildSign(command, body);
return String.format(END_REQUEST, body, sign);
}
public String getBody(CommandType command, Object... bodyParms) {
String result = command.body;
if (result.length() != 0) {
result = result.replace(USERNAME, username);
result = result.replace(CLIENT_CHALLENGE, clientChallenge);
result = String.format(result, bodyParms);
}
return result;
}
public int getCommandId() {
return (commandSequence++ << 8) | sessionId;
}
public void setChallenges(String serverChallenge, int sessionId, String password) {
byte[] serverChallengeArr = invertArray(DatatypeConverter.parseHexBinary(serverChallenge));
byte[] pairingPassword = Base64.getDecoder().decode(password);
this.sessionPassword = sha256(pairingPassword, serverChallengeArr, clientChallengeArr);
this.sessionId = sessionId & 255;
}
private byte[] sha256(byte[]... values) {
for (byte[] data : values) {
digest.update(data);
}
return digest.digest();
}
private static byte[] invertArray(byte[] data) {
byte[] result = new byte[data.length];
int i = data.length - 1;
int c = 0;
while (i >= 0) {
int c2 = c + 1;
result[c] = data[i];
i--;
c = c2;
}
return result;
}
}

View File

@ -0,0 +1,40 @@
/**
* 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.xml.dto;
import com.thoughtworks.xstream.annotations.XStreamAsAttribute;
/**
*
* @author Gaël L'hopital - Initial contribution
*/
public class Authentication {
public enum UserPerm {
wait,
user,
admin;
}
@XStreamAsAttribute
public int id;
@XStreamAsAttribute
public String pwd;
@XStreamAsAttribute
private String username;
@XStreamAsAttribute
public UserPerm perm;
@XStreamAsAttribute
public boolean notify;
@XStreamAsAttribute
public String sc;
}

View File

@ -0,0 +1,42 @@
/**
* 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.xml.dto;
import static org.openhab.binding.mynice.internal.xml.RequestBuilder.*;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* The CommandType enum lists all handled command with according syntax
*
* @author Gaël L'hopital - Initial contribution
*/
@NonNullByDefault
public enum CommandType {
PAIR(false,
"<Authentication username=\"" + USERNAME
+ "\" cc=\"null\" CType=\"phone\" OSType=\"Android\" OSVer=\"6.0.1\"/>"),
VERIFY(false, "<User username=\"" + USERNAME + "\"/>"),
CONNECT(false, "<Authentication username=\"" + USERNAME + "\" cc=\"" + CLIENT_CHALLENGE + "\"/>"),
INFO(true, ""),
STATUS(true, ""),
CHANGE(true, "<Devices><Device id=\"%s\"><Services>%s</Services></Device></Devices>");
public final boolean signNeeded;
public final String body;
CommandType(boolean signNeeded, String body) {
this.signNeeded = signNeeded;
this.body = body;
}
}

View File

@ -0,0 +1,57 @@
/**
* 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.xml.dto;
import com.thoughtworks.xstream.annotations.XStreamAlias;
import com.thoughtworks.xstream.annotations.XStreamAsAttribute;
/**
*
* @author Gaël L'hopital - Initial contribution
*/
public class Device {
public enum DeviceType {
SECTIONAL,
UP_AND_OVER,
SLIDING,
BARRIER,
SWING;
}
@XStreamAsAttribute
public String id;
@XStreamAlias("Type")
public DeviceType type;
@XStreamAlias("Manuf")
public String manuf;
@XStreamAlias("Prod")
public String prod;
@XStreamAlias("Desc")
public String desc;
@XStreamAlias("VersionHW")
public String versionHW;
@XStreamAlias("VersionFW")
public String versionFW;
@XStreamAlias("SerialNr")
public String serialNr;
@XStreamAlias("Properties")
public Properties properties;
}

View File

@ -0,0 +1,26 @@
/**
* 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.xml.dto;
import com.thoughtworks.xstream.annotations.XStreamAlias;
/**
*
* @author Gaël L'hopital - Initial contribution
*/
public class Error {
@XStreamAlias("Code")
public int code;
@XStreamAlias("Info")
public String info;
}

View File

@ -0,0 +1,49 @@
/**
* 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.xml.dto;
import java.util.List;
import org.eclipse.jdt.annotation.NonNull;
import com.thoughtworks.xstream.annotations.XStreamAlias;
import com.thoughtworks.xstream.annotations.XStreamAsAttribute;
/**
*
* @author Gaël L'hopital - Initial contribution
*/
public class Event {
@XStreamAsAttribute
private String id;
@XStreamAsAttribute
private String source;
@XStreamAsAttribute
private String target;
@XStreamAsAttribute
private String protocolType;
@XStreamAsAttribute
private String protocolVersion;
@XStreamAsAttribute
public CommandType type;
@XStreamAlias("Error")
public Error error;
@XStreamAlias("Devices")
private List<Device> devices;
public @NonNull List<Device> getDevices() {
List<Device> localDevices = devices;
return localDevices == null ? List.of() : localDevices;
}
}

View File

@ -0,0 +1,36 @@
/**
* 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.xml.dto;
import com.thoughtworks.xstream.annotations.XStreamAlias;
/**
*
* @author Gaël L'hopital - Initial contribution
*/
public class Interface {
@XStreamAlias("Zone")
public String zone;
@XStreamAlias("DST")
public String dst;
@XStreamAlias("VersionHW")
public String versionHW;
@XStreamAlias("VersionFW")
public String versionFW;
@XStreamAlias("Manuf")
public String manuf;
@XStreamAlias("Prod")
public String prod;
@XStreamAlias("SerialNr")
public String serialNr;
}

View File

@ -0,0 +1,29 @@
/**
* 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.xml.dto;
import com.thoughtworks.xstream.annotations.XStreamAlias;
/**
*
* @author Gaël L'hopital - Initial contribution
*/
@XStreamAlias("Properties")
public class Properties {
@XStreamAlias("DoorStatus")
public String doorStatus;
@XStreamAlias("Obstruct")
public String obstruct;
@XStreamAlias("T4_allowed")
public Property t4allowed;
}

View File

@ -0,0 +1,28 @@
/**
* 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.xml.dto;
import com.thoughtworks.xstream.annotations.XStreamAsAttribute;
/**
*
* @author Gaël L'hopital - Initial contribution
*/
public class Property {
@XStreamAsAttribute
public String type;
@XStreamAsAttribute
public String values;
@XStreamAsAttribute
public String perm;
}

View File

@ -0,0 +1,26 @@
/**
* 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.xml.dto;
import com.thoughtworks.xstream.annotations.XStreamAlias;
/**
*
* @author Gaël L'hopital - Initial contribution
*/
public class Response extends Event {
@XStreamAlias("Authentication")
public Authentication authentication;
@XStreamAlias("Interface")
public Interface intf;
}

View File

@ -0,0 +1,67 @@
/**
* 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.xml.dto;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* This enum lists all handled T4 commands
*
* @author Gaël L'hopital - Initial contribution
*/
@NonNullByDefault
public enum T4Command {
MDAx(1),
MDAy(2),
MDAz(3),
MDA0(4),
MDA1(5),
MDA2(6),
MDA3(7),
MDBi(11),
MDBj(12),
MDBk(13),
MDBl(14),
MDBm(15),
MDEw(16),
MDEx(17),
MDEy(18),
MDEz(19),
MDE0(20),
MDE1(21),
MDE2(22),
MDE3(23),
MDE4(24),
MDE5(25),
MDFh(26);
private int bitPosition;
private T4Command(int bitPosition) {
this.bitPosition = bitPosition;
}
public static T4Command fromCode(String commandCode) {
return Stream.of(T4Command.values()).filter(command -> command.name().equalsIgnoreCase(commandCode)).findFirst()
.orElseThrow(() -> new IllegalArgumentException("Unknown T4 command code (%s)".formatted(commandCode)));
}
public static List<T4Command> fromBitmask(int bitmask) {
return Stream.of(T4Command.values()).filter(command -> ((1 << command.bitPosition) & bitmask) != 0)
.collect(Collectors.toList());
}
}

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<addon:addon id="mynice" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:addon="https://openhab.org/schemas/addon/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/addon/v1.0.0 https://openhab.org/schemas/addon-1.0.0.xsd">
<type>binding</type>
<name>MyNice Binding</name>
<description>This binding lets you connect to your IT4Wifi Nice device.</description>
</addon:addon>

View File

@ -0,0 +1,78 @@
# add-on
addon.mynice.name = MyNice Binding
addon.mynice.description = This binding lets you connect to your IT4Wifi Nice device.
# thing types
thing-type.mynice.it4wifi.label = IT4Wifi
thing-type.mynice.it4wifi.description = This thing connects to your IT4Wifi module
thing-type.mynice.sliding.label = Sliding Gate
thing-type.mynice.sliding.description = A sliding gate
thing-type.mynice.swing.label = Swing Gate
thing-type.mynice.swing.description = A dual swing gate
# thing types config
thing-type.config.mynice.it4wifi.hostname.label = Hostname
thing-type.config.mynice.it4wifi.hostname.description = Hostname or IP address of the IT4Wifi
thing-type.config.mynice.it4wifi.macAddress.label = MAC Address
thing-type.config.mynice.it4wifi.macAddress.description = The MAC address of the IT4Wifi
thing-type.config.mynice.it4wifi.password.label = Pairing Key
thing-type.config.mynice.it4wifi.password.description = Pairing Key needed to access the device, provided by the bridge itself
thing-type.config.mynice.it4wifi.username.label = Username
thing-type.config.mynice.it4wifi.username.description = User defined on the IT4Wifi for openHAB
thing-type.config.mynice.sliding.id.label = ID
thing-type.config.mynice.sliding.id.description = ID of the gate on the TP4 bus connected to the bridge.
thing-type.config.mynice.swing.id.label = ID
thing-type.config.mynice.swing.id.description = ID of the gate on the TP4 bus connected to the bridge
# channel types
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.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.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
# error messages
conf-error-no-username = Please define a username for this thing
conf-pending-validation = Please validate the user on the MyNice application
error-handshake-limit = Maximum handshake attempts reached

View File

@ -0,0 +1,166 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="mynice"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
<bridge-type id="it4wifi">
<label>IT4Wifi</label>
<description>This thing connects to your IT4Wifi module</description>
<representation-property>macAddress</representation-property>
<config-description>
<parameter name="username" type="text">
<label>Username</label>
<description>User defined on the IT4Wifi for openHAB</description>
</parameter>
<parameter name="hostname" type="text" required="true">
<context>network-address</context>
<label>Hostname</label>
<description>Hostname or IP address of the IT4Wifi</description>
</parameter>
<parameter name="macAddress" type="text" pattern="([0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}(\s*#.*)*"
required="true">
<label>MAC Address</label>
<description>The MAC address of the IT4Wifi</description>
</parameter>
<parameter name="password" type="text">
<context>password</context>
<label>Pairing Key</label>
<description>Pairing Key needed to access the device, provided by the bridge itself</description>
</parameter>
</config-description>
</bridge-type>
<thing-type id="swing">
<supported-bridge-type-refs>
<bridge-type-ref id="it4wifi"/>
</supported-bridge-type-refs>
<label>Swing Gate</label>
<description>A dual swing gate</description>
<channels>
<channel id="status" typeId="doorstatus"/>
<channel id="obstruct" typeId="obstruct"/>
<channel id="moving" typeId="moving"/>
<channel id="command" typeId="command"/>
<channel id="t4command" typeId="t4command"/>
</channels>
<representation-property>id</representation-property>
<config-description>
<parameter name="id" type="text" required="true">
<label>ID</label>
<description>ID of the gate on the TP4 bus connected to the bridge</description>
</parameter>
</config-description>
</thing-type>
<thing-type id="sliding">
<supported-bridge-type-refs>
<bridge-type-ref id="it4wifi"/>
</supported-bridge-type-refs>
<label>Sliding Gate</label>
<description>A sliding gate</description>
<channels>
<channel id="status" typeId="doorstatus"/>
<channel id="obstruct" typeId="obstruct"/>
<channel id="moving" typeId="moving"/>
<channel id="command" typeId="command"/>
<channel id="t4command" typeId="t4command"/>
</channels>
<representation-property>id</representation-property>
<config-description>
<parameter name="id" type="text" required="true">
<label>ID</label>
<description>ID of the gate on the TP4 bus connected to the bridge.</description>
</parameter>
</config-description>
</thing-type>
<channel-type id="doorstatus">
<item-type>String</item-type>
<label>Gate Status</label>
<description>Position of the gate or state if moving</description>
<state readOnly="true">
<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>
</options>
</state>
</channel-type>
<channel-type id="moving">
<item-type>Switch</item-type>
<label>Moving</label>
<description>Indicates if the device is currently operating a command</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="obstruct">
<item-type>Switch</item-type>
<label>Obstruction</label>
<description>Something prevented normal operation of the gate by crossing the infra-red barrier</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="command">
<item-type>String</item-type>
<label>Command</label>
<description>Send a given command to the gate</description>
<state readOnly="false">
<options>
<option value="stop">Stop</option>
<option value="open">Open</option>
<option value="close">Close</option>
</options>
</state>
<autoUpdatePolicy>veto</autoUpdatePolicy>
</channel-type>
<channel-type id="t4command">
<item-type>String</item-type>
<label>T4 Command</label>
<description>Send a T4 Command to the gate</description>
<state readOnly="false">
<options>
<option value="MDAx">Step by Step</option>
<option value="MDAy">Stop (as remote control)</option>
<option value="MDAz">Open (as remote control)</option>
<option value="MDA0">Close (as remote control)</option>
<option value="MDA1">Partial opening 1</option>
<option value="MDA2">Partial opening 2</option>
<option value="MDA3">Partial opening 3</option>
<option value="MDBi">Apartment Step by Step</option>
<option value="MDBj">Step by Step high priority</option>
<option value="MDBk">Open and block</option>
<option value="MDBl">Close and block</option>
<option value="MDBm">Block</option>
<option value="MDEw">Release</option>
<option value="MDEx">Courtesy light timer on</option>
<option value="MDEy">Courtesy light on-off</option>
<option value="MDEz">Step by Step master door</option>
<option value="MDE0">Open master door</option>
<option value="MDE1">Close master door</option>
<option value="MDE2">Step by Step slave door</option>
<option value="MDE3">Open slave door</option>
<option value="MDE4">Close slave door</option>
<option value="MDE5">Release and Open</option>
<option value="MDFh">Release and Close</option>
</options>
</state>
<autoUpdatePolicy>veto</autoUpdatePolicy>
</channel-type>
</thing:thing-descriptions>

View File

@ -250,6 +250,7 @@
<module>org.openhab.binding.mqtt.homie</module>
<module>org.openhab.binding.mybmw</module>
<module>org.openhab.binding.mycroft</module>
<module>org.openhab.binding.mynice</module>
<module>org.openhab.binding.myq</module>
<module>org.openhab.binding.mystrom</module>
<module>org.openhab.binding.nanoleaf</module>