]> git.basschouten.com Git - openhab-addons.git/commitdiff
[MyNice] Addition of Courtesy Light Channel (#14797)
authorGaël L'hopital <gael@lhopital.org>
Wed, 12 Jul 2023 21:01:08 +0000 (23:01 +0200)
committerGitHub <noreply@github.com>
Wed, 12 Jul 2023 21:01:08 +0000 (23:01 +0200)
* Solving activation / deactivation of IT4Wifi thing glitches.
* Adding Courtesy light
Added command capability of Stop / Move
* Changed misplaced handling of RefreshType

---------

Signed-off-by: clinique <gael@lhopital.org>
13 files changed:
bundles/org.openhab.binding.mynice/README.md
bundles/org.openhab.binding.mynice/src/main/java/org/openhab/binding/mynice/internal/MyNiceBindingConstants.java
bundles/org.openhab.binding.mynice/src/main/java/org/openhab/binding/mynice/internal/MyNiceHandlerFactory.java
bundles/org.openhab.binding.mynice/src/main/java/org/openhab/binding/mynice/internal/config/CourtesyConfiguration.java [new file with mode: 0644]
bundles/org.openhab.binding.mynice/src/main/java/org/openhab/binding/mynice/internal/discovery/MyNiceDiscoveryService.java
bundles/org.openhab.binding.mynice/src/main/java/org/openhab/binding/mynice/internal/handler/GateHandler.java
bundles/org.openhab.binding.mynice/src/main/java/org/openhab/binding/mynice/internal/handler/It4WifiConnector.java
bundles/org.openhab.binding.mynice/src/main/java/org/openhab/binding/mynice/internal/handler/It4WifiHandler.java
bundles/org.openhab.binding.mynice/src/main/java/org/openhab/binding/mynice/internal/xml/dto/Properties.java
bundles/org.openhab.binding.mynice/src/main/java/org/openhab/binding/mynice/internal/xml/dto/T4Command.java
bundles/org.openhab.binding.mynice/src/main/resources/OH-INF/i18n/mynice.properties
bundles/org.openhab.binding.mynice/src/main/resources/OH-INF/thing/thing-types.xml
bundles/org.openhab.binding.mynice/src/main/resources/OH-INF/update/instructions.xml [new file with mode: 0644]

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