diff --git a/CODEOWNERS b/CODEOWNERS index f1f204fa412cd..69fc52c11ce4e 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -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 diff --git a/bom/openhab-addons/pom.xml b/bom/openhab-addons/pom.xml index efe39a63389da..28893eca65d64 100644 --- a/bom/openhab-addons/pom.xml +++ b/bom/openhab-addons/pom.xml @@ -1081,6 +1081,11 @@ org.openhab.binding.mybmw ${project.version} + + org.openhab.addons.bundles + org.openhab.binding.mynice + ${project.version} + org.openhab.addons.bundles org.openhab.binding.myq diff --git a/bundles/org.openhab.binding.mynice/NOTICE b/bundles/org.openhab.binding.mynice/NOTICE new file mode 100644 index 0000000000000..38d625e349232 --- /dev/null +++ b/bundles/org.openhab.binding.mynice/NOTICE @@ -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 diff --git a/bundles/org.openhab.binding.mynice/README.md b/bundles/org.openhab.binding.mynice/README.md new file mode 100644 index 0000000000000..d4c7e65e70c35 --- /dev/null +++ b/bundles/org.openhab.binding.mynice/README.md @@ -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" (gMyniceSwing) ["Status","Opening"] {channel="mynice:swing:83eef09166:1:status"} +String NiceIT4WIFI_Obstruction "Obstruction" (gMyniceSwing) {channel="mynice:swing:83eef09166:1:obstruct"} +Switch NiceIT4WIFI_Moving "Moving" (gMyniceSwing) ["Status","Vibration"] {channel="mynice:swing:83eef09166:1:moving"} +String NiceIT4WIFI_Command "Command" (gMyniceSwing) {channel="mynice:swing:83eef09166:1:command"} + +``` diff --git a/bundles/org.openhab.binding.mynice/pom.xml b/bundles/org.openhab.binding.mynice/pom.xml new file mode 100644 index 0000000000000..47d08b3480372 --- /dev/null +++ b/bundles/org.openhab.binding.mynice/pom.xml @@ -0,0 +1,17 @@ + + + + 4.0.0 + + + org.openhab.addons.bundles + org.openhab.addons.reactor.bundles + 4.0.0-SNAPSHOT + + + org.openhab.binding.mynice + + openHAB Add-ons :: Bundles :: MyNice Binding + + diff --git a/bundles/org.openhab.binding.mynice/src/main/feature/feature.xml b/bundles/org.openhab.binding.mynice/src/main/feature/feature.xml new file mode 100644 index 0000000000000..001b78d9f57fb --- /dev/null +++ b/bundles/org.openhab.binding.mynice/src/main/feature/feature.xml @@ -0,0 +1,9 @@ + + + mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features + + + openhab-runtime-base + mvn:org.openhab.addons.bundles/org.openhab.binding.mynice/${project.version} + + diff --git a/bundles/org.openhab.binding.mynice/src/main/java/org/openhab/binding/mynice/internal/MyNiceBindingConstants.java b/bundles/org.openhab.binding.mynice/src/main/java/org/openhab/binding/mynice/internal/MyNiceBindingConstants.java new file mode 100644 index 0000000000000..b785ac6bb46d8 --- /dev/null +++ b/bundles/org.openhab.binding.mynice/src/main/java/org/openhab/binding/mynice/internal/MyNiceBindingConstants.java @@ -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"; +} diff --git a/bundles/org.openhab.binding.mynice/src/main/java/org/openhab/binding/mynice/internal/MyNiceHandlerFactory.java b/bundles/org.openhab.binding.mynice/src/main/java/org/openhab/binding/mynice/internal/MyNiceHandlerFactory.java new file mode 100644 index 0000000000000..a5fc54a3f1c35 --- /dev/null +++ b/bundles/org.openhab.binding.mynice/src/main/java/org/openhab/binding/mynice/internal/MyNiceHandlerFactory.java @@ -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 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; + } +} diff --git a/bundles/org.openhab.binding.mynice/src/main/java/org/openhab/binding/mynice/internal/config/It4WifiConfiguration.java b/bundles/org.openhab.binding.mynice/src/main/java/org/openhab/binding/mynice/internal/config/It4WifiConfiguration.java new file mode 100644 index 0000000000000..6b92fd34a46d8 --- /dev/null +++ b/bundles/org.openhab.binding.mynice/src/main/java/org/openhab/binding/mynice/internal/config/It4WifiConfiguration.java @@ -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 = ""; +} diff --git a/bundles/org.openhab.binding.mynice/src/main/java/org/openhab/binding/mynice/internal/discovery/MyNiceDiscoveryParticipant.java b/bundles/org.openhab.binding.mynice/src/main/java/org/openhab/binding/mynice/internal/discovery/MyNiceDiscoveryParticipant.java new file mode 100644 index 0000000000000..267d7c63500b9 --- /dev/null +++ b/bundles/org.openhab.binding.mynice/src/main/java/org/openhab/binding/mynice/internal/discovery/MyNiceDiscoveryParticipant.java @@ -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 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(); + } +} diff --git a/bundles/org.openhab.binding.mynice/src/main/java/org/openhab/binding/mynice/internal/discovery/MyNiceDiscoveryService.java b/bundles/org.openhab.binding.mynice/src/main/java/org/openhab/binding/mynice/internal/discovery/MyNiceDiscoveryService.java new file mode 100644 index 0000000000000..19dc182975e7b --- /dev/null +++ b/bundles/org.openhab.binding.mynice/src/main/java/org/openhab/binding/mynice/internal/discovery/MyNiceDiscoveryService.java @@ -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 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); + } + } +} diff --git a/bundles/org.openhab.binding.mynice/src/main/java/org/openhab/binding/mynice/internal/handler/GateHandler.java b/bundles/org.openhab.binding.mynice/src/main/java/org/openhab/binding/mynice/internal/handler/GateHandler.java new file mode 100644 index 0000000000000..8f5e9fb3b719c --- /dev/null +++ b/bundles/org.openhab.binding.mynice/src/main/java/org/openhab/binding/mynice/internal/handler/GateHandler.java @@ -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 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 devices) { + devices.stream().filter(d -> id.equals(d.id)).findFirst().map(device -> { + updateStatus(ThingStatus.ONLINE); + if (thing.getProperties().isEmpty()) { + int value = Integer.parseInt(device.properties.t4allowed.values, 16); + List t4Allowed = T4Command.fromBitmask(value).stream().map(Enum::name).toList(); + updateProperties(Map.of(PROPERTY_VENDOR, device.manuf, PROPERTY_MODEL_ID, device.prod, + PROPERTY_SERIAL_NUMBER, device.serialNr, PROPERTY_HARDWARE_VERSION, device.versionHW, + PROPERTY_FIRMWARE_VERSION, device.versionFW, ALLOWED_T4, String.join(",", t4Allowed))); + } + 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; + }); + } +} diff --git a/bundles/org.openhab.binding.mynice/src/main/java/org/openhab/binding/mynice/internal/handler/It4WifiConnector.java b/bundles/org.openhab.binding.mynice/src/main/java/org/openhab/binding/mynice/internal/handler/It4WifiConnector.java new file mode 100644 index 0000000000000..0a5a0c76499bf --- /dev/null +++ b/bundles/org.openhab.binding.mynice/src/main/java/org/openhab/binding/mynice/internal/handler/It4WifiConnector.java @@ -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(); + } +} diff --git a/bundles/org.openhab.binding.mynice/src/main/java/org/openhab/binding/mynice/internal/handler/It4WifiHandler.java b/bundles/org.openhab.binding.mynice/src/main/java/org/openhab/binding/mynice/internal/handler/It4WifiHandler.java new file mode 100644 index 0000000000000..b6535a5fb56e4 --- /dev/null +++ b/bundles/org.openhab.binding.mynice/src/main/java/org/openhab/binding/mynice/internal/handler/It4WifiHandler.java @@ -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 dataListeners = new CopyOnWriteArrayList<>(); + private final MyNiceXStream xstream = new MyNiceXStream(); + + private @NonNullByDefault({}) RequestBuilder reqBuilder; + private @Nullable It4WifiConnector connector; + private @Nullable ScheduledFuture keepAliveJob; + private List devices = new ArrayList<>(); + private int handshakeAttempts = 0; + + public It4WifiHandler(Bridge thing) { + super(thing); + } + + @Override + public Collection> 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 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 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; + } + } +} diff --git a/bundles/org.openhab.binding.mynice/src/main/java/org/openhab/binding/mynice/internal/handler/MyNiceDataListener.java b/bundles/org.openhab.binding.mynice/src/main/java/org/openhab/binding/mynice/internal/handler/MyNiceDataListener.java new file mode 100644 index 0000000000000..aa2112c0ce19a --- /dev/null +++ b/bundles/org.openhab.binding.mynice/src/main/java/org/openhab/binding/mynice/internal/handler/MyNiceDataListener.java @@ -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 devices); +} diff --git a/bundles/org.openhab.binding.mynice/src/main/java/org/openhab/binding/mynice/internal/xml/MyNiceXStream.java b/bundles/org.openhab.binding.mynice/src/main/java/org/openhab/binding/mynice/internal/xml/MyNiceXStream.java new file mode 100644 index 0000000000000..e572f8811ecc0 --- /dev/null +++ b/bundles/org.openhab.binding.mynice/src/main/java/org/openhab/binding/mynice/internal/xml/MyNiceXStream.java @@ -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 = ""; + + 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); + } +} diff --git a/bundles/org.openhab.binding.mynice/src/main/java/org/openhab/binding/mynice/internal/xml/RequestBuilder.java b/bundles/org.openhab.binding.mynice/src/main/java/org/openhab/binding/mynice/internal/xml/RequestBuilder.java new file mode 100644 index 0000000000000..da480fb6aebc0 --- /dev/null +++ b/bundles/org.openhab.binding.mynice/src/main/java/org/openhab/binding/mynice/internal/xml/RequestBuilder.java @@ -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 = "\r\n"; + private static final String END_REQUEST = "%s%s"; + private static final String DOOR_ACTION = "%s"; + private static final String T4_ACTION = "%s"; + private static final String SIGN = "%s"; + + 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; + } +} diff --git a/bundles/org.openhab.binding.mynice/src/main/java/org/openhab/binding/mynice/internal/xml/dto/Authentication.java b/bundles/org.openhab.binding.mynice/src/main/java/org/openhab/binding/mynice/internal/xml/dto/Authentication.java new file mode 100644 index 0000000000000..0c5be7dc5db00 --- /dev/null +++ b/bundles/org.openhab.binding.mynice/src/main/java/org/openhab/binding/mynice/internal/xml/dto/Authentication.java @@ -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; +} diff --git a/bundles/org.openhab.binding.mynice/src/main/java/org/openhab/binding/mynice/internal/xml/dto/CommandType.java b/bundles/org.openhab.binding.mynice/src/main/java/org/openhab/binding/mynice/internal/xml/dto/CommandType.java new file mode 100644 index 0000000000000..e99d75947ee33 --- /dev/null +++ b/bundles/org.openhab.binding.mynice/src/main/java/org/openhab/binding/mynice/internal/xml/dto/CommandType.java @@ -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, + ""), + VERIFY(false, ""), + CONNECT(false, ""), + INFO(true, ""), + STATUS(true, ""), + CHANGE(true, "%s"); + + public final boolean signNeeded; + public final String body; + + CommandType(boolean signNeeded, String body) { + this.signNeeded = signNeeded; + this.body = body; + } +} diff --git a/bundles/org.openhab.binding.mynice/src/main/java/org/openhab/binding/mynice/internal/xml/dto/Device.java b/bundles/org.openhab.binding.mynice/src/main/java/org/openhab/binding/mynice/internal/xml/dto/Device.java new file mode 100644 index 0000000000000..e4a0b3b042ac9 --- /dev/null +++ b/bundles/org.openhab.binding.mynice/src/main/java/org/openhab/binding/mynice/internal/xml/dto/Device.java @@ -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; +} diff --git a/bundles/org.openhab.binding.mynice/src/main/java/org/openhab/binding/mynice/internal/xml/dto/Error.java b/bundles/org.openhab.binding.mynice/src/main/java/org/openhab/binding/mynice/internal/xml/dto/Error.java new file mode 100644 index 0000000000000..4e753d1497227 --- /dev/null +++ b/bundles/org.openhab.binding.mynice/src/main/java/org/openhab/binding/mynice/internal/xml/dto/Error.java @@ -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; +} diff --git a/bundles/org.openhab.binding.mynice/src/main/java/org/openhab/binding/mynice/internal/xml/dto/Event.java b/bundles/org.openhab.binding.mynice/src/main/java/org/openhab/binding/mynice/internal/xml/dto/Event.java new file mode 100644 index 0000000000000..3a55872cabf6f --- /dev/null +++ b/bundles/org.openhab.binding.mynice/src/main/java/org/openhab/binding/mynice/internal/xml/dto/Event.java @@ -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 devices; + + public @NonNull List getDevices() { + List localDevices = devices; + return localDevices == null ? List.of() : localDevices; + } +} diff --git a/bundles/org.openhab.binding.mynice/src/main/java/org/openhab/binding/mynice/internal/xml/dto/Interface.java b/bundles/org.openhab.binding.mynice/src/main/java/org/openhab/binding/mynice/internal/xml/dto/Interface.java new file mode 100644 index 0000000000000..02e15d0f8b293 --- /dev/null +++ b/bundles/org.openhab.binding.mynice/src/main/java/org/openhab/binding/mynice/internal/xml/dto/Interface.java @@ -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; +} diff --git a/bundles/org.openhab.binding.mynice/src/main/java/org/openhab/binding/mynice/internal/xml/dto/Properties.java b/bundles/org.openhab.binding.mynice/src/main/java/org/openhab/binding/mynice/internal/xml/dto/Properties.java new file mode 100644 index 0000000000000..e90b6c0430874 --- /dev/null +++ b/bundles/org.openhab.binding.mynice/src/main/java/org/openhab/binding/mynice/internal/xml/dto/Properties.java @@ -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; +} diff --git a/bundles/org.openhab.binding.mynice/src/main/java/org/openhab/binding/mynice/internal/xml/dto/Property.java b/bundles/org.openhab.binding.mynice/src/main/java/org/openhab/binding/mynice/internal/xml/dto/Property.java new file mode 100644 index 0000000000000..80589288624da --- /dev/null +++ b/bundles/org.openhab.binding.mynice/src/main/java/org/openhab/binding/mynice/internal/xml/dto/Property.java @@ -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; +} diff --git a/bundles/org.openhab.binding.mynice/src/main/java/org/openhab/binding/mynice/internal/xml/dto/Response.java b/bundles/org.openhab.binding.mynice/src/main/java/org/openhab/binding/mynice/internal/xml/dto/Response.java new file mode 100644 index 0000000000000..f6ae5c4dc75fc --- /dev/null +++ b/bundles/org.openhab.binding.mynice/src/main/java/org/openhab/binding/mynice/internal/xml/dto/Response.java @@ -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; +} diff --git a/bundles/org.openhab.binding.mynice/src/main/java/org/openhab/binding/mynice/internal/xml/dto/T4Command.java b/bundles/org.openhab.binding.mynice/src/main/java/org/openhab/binding/mynice/internal/xml/dto/T4Command.java new file mode 100644 index 0000000000000..3b4812d5b0eec --- /dev/null +++ b/bundles/org.openhab.binding.mynice/src/main/java/org/openhab/binding/mynice/internal/xml/dto/T4Command.java @@ -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 fromBitmask(int bitmask) { + return Stream.of(T4Command.values()).filter(command -> ((1 << command.bitPosition) & bitmask) != 0) + .collect(Collectors.toList()); + } +} diff --git a/bundles/org.openhab.binding.mynice/src/main/resources/OH-INF/addon/addon.xml b/bundles/org.openhab.binding.mynice/src/main/resources/OH-INF/addon/addon.xml new file mode 100644 index 0000000000000..537e9cefc350b --- /dev/null +++ b/bundles/org.openhab.binding.mynice/src/main/resources/OH-INF/addon/addon.xml @@ -0,0 +1,10 @@ + + + + binding + MyNice Binding + This binding lets you connect to your IT4Wifi Nice device. + + diff --git a/bundles/org.openhab.binding.mynice/src/main/resources/OH-INF/i18n/mynice.properties b/bundles/org.openhab.binding.mynice/src/main/resources/OH-INF/i18n/mynice.properties new file mode 100644 index 0000000000000..fe20f9f5288f1 --- /dev/null +++ b/bundles/org.openhab.binding.mynice/src/main/resources/OH-INF/i18n/mynice.properties @@ -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 diff --git a/bundles/org.openhab.binding.mynice/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.mynice/src/main/resources/OH-INF/thing/thing-types.xml new file mode 100644 index 0000000000000..4809d4f149b44 --- /dev/null +++ b/bundles/org.openhab.binding.mynice/src/main/resources/OH-INF/thing/thing-types.xml @@ -0,0 +1,166 @@ + + + + + + This thing connects to your IT4Wifi module + + macAddress + + + + + User defined on the IT4Wifi for openHAB + + + network-address + + Hostname or IP address of the IT4Wifi + + + + The MAC address of the IT4Wifi + + + password + + Pairing Key needed to access the device, provided by the bridge itself + + + + + + + + + + + A dual swing gate + + + + + + + + + + id + + + + + ID of the gate on the TP4 bus connected to the bridge + + + + + + + + + + + A sliding gate + + + + + + + + + + id + + + + + ID of the gate on the TP4 bus connected to the bridge. + + + + + + + String + + Position of the gate or state if moving + + + + + + + + + + + + + Switch + + Indicates if the device is currently operating a command + + + + + Switch + + Something prevented normal operation of the gate by crossing the infra-red barrier + + + + + String + + Send a given command to the gate + + + + + + + + veto + + + + String + + Send a T4 Command to the gate + + + + + + + + + + + + + + + + + + + + + + + + + + + + veto + + + diff --git a/bundles/pom.xml b/bundles/pom.xml index e22d555988c61..23bf367cd0e9e 100644 --- a/bundles/pom.xml +++ b/bundles/pom.xml @@ -250,6 +250,7 @@ org.openhab.binding.mqtt.homie org.openhab.binding.mybmw org.openhab.binding.mycroft + org.openhab.binding.mynice org.openhab.binding.myq org.openhab.binding.mystrom org.openhab.binding.nanoleaf