]> git.basschouten.com Git - openhab-addons.git/commitdiff
[http] Initial contribution (#8521)
authorJ-N-K <J-N-K@users.noreply.github.com>
Mon, 9 Nov 2020 16:53:44 +0000 (17:53 +0100)
committerGitHub <noreply@github.com>
Mon, 9 Nov 2020 16:53:44 +0000 (17:53 +0100)
Signed-off-by: Jan N. Klug <jan.n.klug@rub.de>
39 files changed:
CODEOWNERS
bom/openhab-addons/pom.xml
bundles/org.openhab.binding.http/NOTICE [new file with mode: 0644]
bundles/org.openhab.binding.http/README.md [new file with mode: 0644]
bundles/org.openhab.binding.http/pom.xml [new file with mode: 0644]
bundles/org.openhab.binding.http/src/main/feature/feature.xml [new file with mode: 0644]
bundles/org.openhab.binding.http/src/main/java/org/openhab/binding/http/internal/HttpBindingConstants.java [new file with mode: 0644]
bundles/org.openhab.binding.http/src/main/java/org/openhab/binding/http/internal/HttpClientProvider.java [new file with mode: 0644]
bundles/org.openhab.binding.http/src/main/java/org/openhab/binding/http/internal/HttpDynamicStateDescriptionProvider.java [new file with mode: 0644]
bundles/org.openhab.binding.http/src/main/java/org/openhab/binding/http/internal/HttpHandlerFactory.java [new file with mode: 0644]
bundles/org.openhab.binding.http/src/main/java/org/openhab/binding/http/internal/HttpThingHandler.java [new file with mode: 0644]
bundles/org.openhab.binding.http/src/main/java/org/openhab/binding/http/internal/Util.java [new file with mode: 0644]
bundles/org.openhab.binding.http/src/main/java/org/openhab/binding/http/internal/config/HttpAuthMode.java [new file with mode: 0644]
bundles/org.openhab.binding.http/src/main/java/org/openhab/binding/http/internal/config/HttpChannelConfig.java [new file with mode: 0644]
bundles/org.openhab.binding.http/src/main/java/org/openhab/binding/http/internal/config/HttpChannelMode.java [new file with mode: 0644]
bundles/org.openhab.binding.http/src/main/java/org/openhab/binding/http/internal/config/HttpThingConfig.java [new file with mode: 0644]
bundles/org.openhab.binding.http/src/main/java/org/openhab/binding/http/internal/converter/AbstractTransformingItemConverter.java [new file with mode: 0644]
bundles/org.openhab.binding.http/src/main/java/org/openhab/binding/http/internal/converter/ColorItemConverter.java [new file with mode: 0644]
bundles/org.openhab.binding.http/src/main/java/org/openhab/binding/http/internal/converter/DimmerItemConverter.java [new file with mode: 0644]
bundles/org.openhab.binding.http/src/main/java/org/openhab/binding/http/internal/converter/FixedValueMappingItemConverter.java [new file with mode: 0644]
bundles/org.openhab.binding.http/src/main/java/org/openhab/binding/http/internal/converter/GenericItemConverter.java [new file with mode: 0644]
bundles/org.openhab.binding.http/src/main/java/org/openhab/binding/http/internal/converter/ImageItemConverter.java [new file with mode: 0644]
bundles/org.openhab.binding.http/src/main/java/org/openhab/binding/http/internal/converter/ItemValueConverter.java [new file with mode: 0644]
bundles/org.openhab.binding.http/src/main/java/org/openhab/binding/http/internal/converter/PlayerItemConverter.java [new file with mode: 0644]
bundles/org.openhab.binding.http/src/main/java/org/openhab/binding/http/internal/converter/RollershutterItemConverter.java [new file with mode: 0644]
bundles/org.openhab.binding.http/src/main/java/org/openhab/binding/http/internal/http/Content.java [new file with mode: 0644]
bundles/org.openhab.binding.http/src/main/java/org/openhab/binding/http/internal/http/HttpAuthException.java [new file with mode: 0644]
bundles/org.openhab.binding.http/src/main/java/org/openhab/binding/http/internal/http/HttpResponseListener.java [new file with mode: 0644]
bundles/org.openhab.binding.http/src/main/java/org/openhab/binding/http/internal/http/RefreshingUrlCache.java [new file with mode: 0644]
bundles/org.openhab.binding.http/src/main/java/org/openhab/binding/http/internal/transform/CascadedValueTransformationImpl.java [new file with mode: 0644]
bundles/org.openhab.binding.http/src/main/java/org/openhab/binding/http/internal/transform/NoOpValueTransformation.java [new file with mode: 0644]
bundles/org.openhab.binding.http/src/main/java/org/openhab/binding/http/internal/transform/SingleValueTransformation.java [new file with mode: 0644]
bundles/org.openhab.binding.http/src/main/java/org/openhab/binding/http/internal/transform/ValueTransformation.java [new file with mode: 0644]
bundles/org.openhab.binding.http/src/main/java/org/openhab/binding/http/internal/transform/ValueTransformationProvider.java [new file with mode: 0644]
bundles/org.openhab.binding.http/src/main/resources/OH-INF/binding/binding.xml [new file with mode: 0644]
bundles/org.openhab.binding.http/src/main/resources/OH-INF/config/config.xml [new file with mode: 0644]
bundles/org.openhab.binding.http/src/main/resources/OH-INF/thing/thing-types.xml [new file with mode: 0644]
bundles/org.openhab.binding.http/src/test/java/org/openhab/binding/http/internal/converter/ConverterTest.java [new file with mode: 0644]
bundles/pom.xml

index e249124e5b7a61fe654ec767f7888b7cd6132b2e..171df689062735fe035930963195160566422d48 100644 (file)
@@ -89,6 +89,7 @@
 /bundles/org.openhab.binding.heos/ @Wire82
 /bundles/org.openhab.binding.homematic/ @FStolte @gerrieg @mdicke2s
 /bundles/org.openhab.binding.hpprinter/ @cossey
+/bundles/org.openhab.binding.http/ @J-N-K
 /bundles/org.openhab.binding.hue/ @cweitkamp
 /bundles/org.openhab.binding.hydrawise/ @digitaldan
 /bundles/org.openhab.binding.hyperion/ @tavalin
index 23feb8be9f57ff11134b5e1271c2d9894065210d..25de4404bc0d0701b49b366b067cff19dd3eee43 100644 (file)
       <artifactId>org.openhab.binding.hpprinter</artifactId>
       <version>${project.version}</version>
     </dependency>
+    <dependency>
+      <groupId>org.openhab.addons.bundles</groupId>
+      <artifactId>org.openhab.binding.http</artifactId>
+      <version>${project.version}</version>
+    </dependency>
     <dependency>
       <groupId>org.openhab.addons.bundles</groupId>
       <artifactId>org.openhab.binding.hue</artifactId>
diff --git a/bundles/org.openhab.binding.http/NOTICE b/bundles/org.openhab.binding.http/NOTICE
new file mode 100644 (file)
index 0000000..38d625e
--- /dev/null
@@ -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.http/README.md b/bundles/org.openhab.binding.http/README.md
new file mode 100644 (file)
index 0000000..704b128
--- /dev/null
@@ -0,0 +1,146 @@
+# HTTP Binding
+
+This binding allows using HTTP to bring external data into openHAB or execute HTTP requests on commands.  
+
+## Supported Things
+
+Only one thing named `url` is available.
+It can be extended with different channels.
+
+## Thing Configuration
+
+| parameter         | optional | default | description |
+|-------------------|----------|---------|-------------|
+| `baseURL`         | no       |    -    | The base URL for this thing. Can be extended in channel-configuration. |
+| `refresh`         | no       |   30    | Time in seconds between two refresh calls for the channels of this thing. |
+| `timeout`         | no       |  3000   | Timeout for HTTP requests in ms. |
+| `username`        | yes      |    -    | Username for authentication (advanced parameter). |
+| `password`        | yes      |    -    | Password for authentication (advanced parameter). |
+| `authMode`        | no       |  BASIC  | Authentication mode, `BASIC` or `DIGEST` (advanced parameter). |
+| `commandMethod`   | no       |   GET   | Method used for sending commands `GET`, `PUT`, `POST`. |
+| `contentType`     | yes      |    -    | MIME content-type of the command requests. Only used for  `PUT` and `POST`. |
+| `encoding`        | yes      |    -    | Encoding to be used if no encoding is found in responses (advanced parameter). |  
+| `headers`         | yes      |    -    | Additional headers that are sent along with the request. Format is "header=value".| 
+| `ignoreSSLErrors` | no       |  false  | If set to true ignores invalid SSL certificate errors. This is potentially dangerous.|
+
+*Note:* optional "no" means that you have to configure a value unless a default is provided and you are ok with that setting.
+
+## Channels
+
+Each item type has its own channel-type.
+Depending on the channel-type, channels have different configuration options.
+All channel-types (except `image`) have `stateExtension`, `commandExtension`, `stateTransformation`, `commandTransformation` and `mode` parameters.
+The `image` channel-type supports `stateExtension` only.
+
+| parameter               | optional | default     | description |
+|-------------------------|----------|-------------|-------------|
+| `stateExtension`        | yes      |      -      | Appended to the `baseURL` for requesting states. |
+| `commandExtension`      | yes      |      -      | Appended to the `baseURL` for sending commands. If empty, same as `stateExtension`. |
+| `stateTransformation  ` | yes      |      -      | One or more transformation applied to received values before updating channel. |
+| `commandTransformation` | yes      |      -      | One or more transformation applied to channel value before sending to a remote. |
+| `mode`                  | no       | `READWRITE` | Mode this channel is allowed to operate. `READ` means receive state, `WRITE` means send commands. |
+
+Transformations need to be specified in the same format as 
+Some channels have additional parameters.
+When concatenating the `baseURL` and `stateExtions` or `commandExtension` the binding checks if a proper URL part separator (`/`, `&` or `?`) is present and adds a `/` if missing.
+
+### Value Transformations (`stateTransformation`, `commandTransformation`)
+
+Transformations can be used if the supplied value (or the required value) is different from what openHAB internal types require.
+Here are a few examples to unwrap an incoming value via `stateTransformation` from a complex response:
+
+| Received value                                                      | Tr. Service | Transformation                            |
+|---------------------------------------------------------------------|-------------|-------------------------------------------|
+| `{device: {status: { temperature: 23.2 }}}`                         | JSONPATH    | `JSONPATH:$.device.status.temperature`    |
+| `<device><status><temperature>23.2</temperature></status></device>` | XPath       | `XPath:/device/status/temperature/text()` |
+| `THEVALUE:23.2°C`                                                   | REGEX       | `REGEX::(.*?)°`                           |
+
+Transformations can be chained by separating them with the mathematical intersection character "∩".
+Please note that the values will be discarded if one transformation fails (e.g. REGEX did not match).
+
+The same mechanism works for commands (`commandTransformation`) for outgoing values. 
+
+### `color`
+
+| parameter               | optional | default     | description |
+|-------------------------|----------|-------------|-------------|
+| `onValue`               | yes      |      -      | A special value that represents `ON` |
+| `offValue`              | yes      |      -      | A special value that represents `OFF` |
+| `increaseValue`         | yes      |      -      | A special value that represents `INCREASE` |
+| `decreaseValue`         | yes      |      -      | A special value that represents `DECREASE` |
+| `step`                  | no       |      1      | The amount the brightness is increased/decreased on `INCREASE`/`DECREASE` |
+| `colorMode`             | no       |    RGB      | Mode for color values: `RGB` or `HSB` |
+
+All values that are not `onValue`, `offValue`, `increaseValue`, `decreaseValue` are interpreted as color value (according to the color mode) in the format `r,g,b` or `h,s,v`.
+
+### `contact`
+
+| parameter               | optional | default     | description |
+|-------------------------|----------|-------------|-------------|
+| `openValue`             | no       |      -      | A special value that represents `OPEN` |
+| `closedValue`           | no       |      -      | A special value that represents `CLOSED` |
+
+### `dimmer`
+
+| parameter               | optional | default     | description |
+|-------------------------|----------|-------------|-------------|
+| `onValue`               | yes      |      -      | A special value that represents `ON` |
+| `offValue`              | yes      |      -      | A special value that represents `OFF` |
+| `increaseValue`         | yes      |      -      | A special value that represents `INCREASE` |
+| `decreaseValue`         | yes      |      -      | A special value that represents `DECREASE` |
+| `step`                  | no       |      1      | The amount the brightness is increased/decreased on `INCREASE`/`DECREASE` |
+
+All values that are not `onValue`, `offValue`, `increaseValue`, `decreaseValue` are interpreted as brightness 0-100% and need to be numeric only.
+
+### `player`
+
+| parameter               | optional | default     | description |
+|-------------------------|----------|-------------|-------------|
+| `play`                  | yes      |      -      | A special value that represents `PLAY` |
+| `pause`                 | yes      |      -      | A special value that represents `PAUSE` |
+| `next`                  | yes      |      -      | A special value that represents `NEXT` |
+| `previous`              | yes      |      -      | A special value that represents `PREVIOUS` |
+| `fastforward`           | yes      |      -      | A special value that represents `FASTFORWARD` |
+| `rewind`                | yes      |      -      | A special value that represents `REWIND` |
+
+### `rollershutter`
+
+| parameter               | optional | default     | description |
+|-------------------------|----------|-------------|-------------|
+| `upValue`               | yes      |      -      | A special value that represents `UP` |
+| `downValue`             | yes      |      -      | A special value that represents `DOWN` |
+| `stopValue`             | yes      |      -      | A special value that represents `STOP` |
+| `moveValue`             | yes      |      -      | A special value that represents `MOVE` |
+
+All values that are not `upValue`, `downValue`, `stopValue`, `moveValue` are interpreted as position 0-100% and need to be numeric only.
+                    
+### `switch`
+
+| parameter               | optional | default     | description |
+|-------------------------|----------|-------------|-------------|
+| `onValue`               | no       |      -      | A special value that represents `ON` |
+| `offValue`              | no       |      -      | A special value that represents `OFF` |
+
+**Note:** Special values need to be exact matches, i.e. no leading or trailing characters and comparison is case-sensitive.
+
+## URL Formatting
+
+After concatenation of the `baseURL` and the `commandExtension` or the `stateExtension` (if provided) the URL is formatted using the [java.util.Formatter](http://docs.oracle.com/javase/6/docs/api/java/util/Formatter.html).
+The URL is used as format string and two parameters are added:
+
+- the current date (referenced as `%1$`)
+- the transformed command (referenced as `%2$`)
+
+After the parameter reference the format needs to be appended.
+See the link above for more information about the available format parameters (e.g. to use the string representation, you need to append `s` to the reference).
+When sending an OFF command on 2020-07-06, the URL
+
+```
+http://www.domain.org/home/lights/23871/?status=%2$s&date=%1$tY-%1$tm-%1$td
+``` 
+
+is transformed to 
+
+```
+http://www.domain.org/home/lights/23871/?status=OFF&date=2020-07-06
+```
\ No newline at end of file
diff --git a/bundles/org.openhab.binding.http/pom.xml b/bundles/org.openhab.binding.http/pom.xml
new file mode 100644 (file)
index 0000000..92a410b
--- /dev/null
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
+  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+
+  <modelVersion>4.0.0</modelVersion>
+
+  <parent>
+    <groupId>org.openhab.addons.bundles</groupId>
+    <artifactId>org.openhab.addons.reactor.bundles</artifactId>
+    <version>3.0.0-SNAPSHOT</version>
+  </parent>
+
+  <artifactId>org.openhab.binding.http</artifactId>
+
+  <name>openHAB Add-ons :: Bundles :: HTTP Binding</name>
+
+</project>
diff --git a/bundles/org.openhab.binding.http/src/main/feature/feature.xml b/bundles/org.openhab.binding.http/src/main/feature/feature.xml
new file mode 100644 (file)
index 0000000..662c97d
--- /dev/null
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<features name="org.openhab.binding.http-${project.version}" xmlns="http://karaf.apache.org/xmlns/features/v1.4.0">
+       <repository>mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features</repository>
+
+       <feature name="openhab-binding-http" description="HTTP Binding" version="${project.version}">
+               <feature>openhab-runtime-base</feature>
+               <bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.http/${project.version}</bundle>
+       </feature>
+</features>
diff --git a/bundles/org.openhab.binding.http/src/main/java/org/openhab/binding/http/internal/HttpBindingConstants.java b/bundles/org.openhab.binding.http/src/main/java/org/openhab/binding/http/internal/HttpBindingConstants.java
new file mode 100644 (file)
index 0000000..eb2b059
--- /dev/null
@@ -0,0 +1,30 @@
+/**
+ * Copyright (c) 2010-2020 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.http.internal;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.core.thing.ThingTypeUID;
+
+/**
+ * The {@link HttpBindingConstants} class defines common constants, which are
+ * used across the whole binding.
+ *
+ * @author Jan N. Klug - Initial contribution
+ */
+@NonNullByDefault
+public class HttpBindingConstants {
+
+    private static final String BINDING_ID = "http";
+
+    public static final ThingTypeUID THING_TYPE_URL = new ThingTypeUID(BINDING_ID, "url");
+}
diff --git a/bundles/org.openhab.binding.http/src/main/java/org/openhab/binding/http/internal/HttpClientProvider.java b/bundles/org.openhab.binding.http/src/main/java/org/openhab/binding/http/internal/HttpClientProvider.java
new file mode 100644 (file)
index 0000000..1751bf8
--- /dev/null
@@ -0,0 +1,39 @@
+/**
+ * Copyright (c) 2010-2020 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.http.internal;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jetty.client.HttpClient;
+
+/**
+ * The {@link HttpClientProvider} defines the interface for providing {@link HttpClient} instances to thing handlers
+ *
+ * @author Jan N. Klug - Initial contribution
+ */
+@NonNullByDefault
+public interface HttpClientProvider {
+
+    /**
+     * get the secure http client
+     *
+     * @return a HttpClient
+     */
+    HttpClient getSecureClient();
+
+    /**
+     * get the insecure http client (ignores SSL errors)
+     *
+     * @return q HttpClient
+     */
+    HttpClient getInsecureClient();
+}
diff --git a/bundles/org.openhab.binding.http/src/main/java/org/openhab/binding/http/internal/HttpDynamicStateDescriptionProvider.java b/bundles/org.openhab.binding.http/src/main/java/org/openhab/binding/http/internal/HttpDynamicStateDescriptionProvider.java
new file mode 100644 (file)
index 0000000..d1ace19
--- /dev/null
@@ -0,0 +1,78 @@
+/**
+ * Copyright (c) 2010-2020 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.http.internal;
+
+import java.util.Locale;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.core.thing.Channel;
+import org.openhab.core.thing.ChannelUID;
+import org.openhab.core.thing.ThingUID;
+import org.openhab.core.thing.type.DynamicStateDescriptionProvider;
+import org.openhab.core.types.StateDescription;
+import org.osgi.service.component.annotations.Component;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Dynamic channel state description provider.
+ * Overrides the state description for the controls, which receive its configuration in the runtime.
+ *
+ * @author Jan N. Klug - Initial contribution
+ */
+@NonNullByDefault
+@Component(service = { DynamicStateDescriptionProvider.class,
+        HttpDynamicStateDescriptionProvider.class }, immediate = true)
+public class HttpDynamicStateDescriptionProvider implements DynamicStateDescriptionProvider {
+
+    private final Map<ChannelUID, StateDescription> descriptions = new ConcurrentHashMap<>();
+    private final Logger logger = LoggerFactory.getLogger(HttpDynamicStateDescriptionProvider.class);
+
+    /**
+     * Set a state description for a channel. This description will be used when preparing the channel state by
+     * the framework for presentation. A previous description, if existed, will be replaced.
+     *
+     * @param channelUID
+     *            channel UID
+     * @param description
+     *            state description for the channel
+     */
+    public void setDescription(ChannelUID channelUID, StateDescription description) {
+        logger.trace("adding state description for channel {}", channelUID);
+        descriptions.put(channelUID, description);
+    }
+
+    /**
+     * remove all descriptions for a given thing
+     *
+     * @param thingUID the thing's UID
+     */
+    public void removeDescriptionsForThing(ThingUID thingUID) {
+        logger.trace("removing state description for thing {}", thingUID);
+        descriptions.entrySet().removeIf(entry -> entry.getKey().getThingUID().equals(thingUID));
+    }
+
+    @Override
+    public @Nullable StateDescription getStateDescription(Channel channel,
+            @Nullable StateDescription originalStateDescription, @Nullable Locale locale) {
+        if (descriptions.containsKey(channel.getUID())) {
+            logger.trace("returning new stateDescription for {}", channel.getUID());
+            return descriptions.get(channel.getUID());
+        } else {
+            return null;
+        }
+    }
+}
diff --git a/bundles/org.openhab.binding.http/src/main/java/org/openhab/binding/http/internal/HttpHandlerFactory.java b/bundles/org.openhab.binding.http/src/main/java/org/openhab/binding/http/internal/HttpHandlerFactory.java
new file mode 100644 (file)
index 0000000..7b14441
--- /dev/null
@@ -0,0 +1,120 @@
+/**
+ * Copyright (c) 2010-2020 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.http.internal;
+
+import static org.openhab.binding.http.internal.HttpBindingConstants.*;
+
+import java.util.Set;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.eclipse.jetty.client.HttpClient;
+import org.eclipse.jetty.util.ssl.SslContextFactory;
+import org.openhab.binding.http.internal.transform.CascadedValueTransformationImpl;
+import org.openhab.binding.http.internal.transform.NoOpValueTransformation;
+import org.openhab.binding.http.internal.transform.ValueTransformation;
+import org.openhab.binding.http.internal.transform.ValueTransformationProvider;
+import org.openhab.core.io.net.http.HttpClientFactory;
+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.openhab.core.transform.TransformationHelper;
+import org.osgi.service.component.annotations.Activate;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Deactivate;
+import org.osgi.service.component.annotations.Reference;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The {@link HttpHandlerFactory} is responsible for creating things and thing
+ * handlers.
+ *
+ * @author Jan N. Klug - Initial contribution
+ */
+@NonNullByDefault
+@Component(configurationPid = "binding.http", service = ThingHandlerFactory.class)
+public class HttpHandlerFactory extends BaseThingHandlerFactory
+        implements ValueTransformationProvider, HttpClientProvider {
+    private static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Set.of(THING_TYPE_URL);
+    private final Logger logger = LoggerFactory.getLogger(HttpHandlerFactory.class);
+
+    private final HttpClient secureClient;
+    private final HttpClient insecureClient;
+
+    private final HttpDynamicStateDescriptionProvider httpDynamicStateDescriptionProvider;
+
+    @Activate
+    public HttpHandlerFactory(@Reference HttpClientFactory httpClientFactory,
+            @Reference HttpDynamicStateDescriptionProvider httpDynamicStateDescriptionProvider) {
+        this.secureClient = new HttpClient(new SslContextFactory());
+        this.insecureClient = new HttpClient(new SslContextFactory(true));
+        try {
+            this.secureClient.start();
+            this.insecureClient.start();
+        } catch (Exception e) {
+            // catching exception is necessary due to the signature of HttpClient.start()
+            logger.warn("Failed to start insecure http client: {}", e.getMessage());
+            throw new IllegalStateException("Could not create insecure HttpClient");
+        }
+        this.httpDynamicStateDescriptionProvider = httpDynamicStateDescriptionProvider;
+    }
+
+    @Deactivate
+    public void deactivate() {
+        try {
+            secureClient.stop();
+            insecureClient.stop();
+        } catch (Exception e) {
+            // catching exception is necessary due to the signature of HttpClient.stop()
+            logger.warn("Failed to stop insecure http client: {}", e.getMessage());
+        }
+    }
+
+    @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 (THING_TYPE_URL.equals(thingTypeUID)) {
+            return new HttpThingHandler(thing, this, this, httpDynamicStateDescriptionProvider);
+        }
+
+        return null;
+    }
+
+    @Override
+    public ValueTransformation getValueTransformation(@Nullable String pattern) {
+        if (pattern == null) {
+            return NoOpValueTransformation.getInstance();
+        }
+        return new CascadedValueTransformationImpl(pattern,
+                name -> TransformationHelper.getTransformationService(bundleContext, name));
+    }
+
+    @Override
+    public HttpClient getSecureClient() {
+        return secureClient;
+    }
+
+    @Override
+    public HttpClient getInsecureClient() {
+        return insecureClient;
+    }
+}
diff --git a/bundles/org.openhab.binding.http/src/main/java/org/openhab/binding/http/internal/HttpThingHandler.java b/bundles/org.openhab.binding.http/src/main/java/org/openhab/binding/http/internal/HttpThingHandler.java
new file mode 100644 (file)
index 0000000..173baf4
--- /dev/null
@@ -0,0 +1,367 @@
+/**
+ * Copyright (c) 2010-2020 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.http.internal;
+
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.TimeUnit;
+import java.util.function.Function;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.eclipse.jetty.client.HttpClient;
+import org.eclipse.jetty.client.api.Authentication;
+import org.eclipse.jetty.client.api.AuthenticationStore;
+import org.eclipse.jetty.client.api.Request;
+import org.eclipse.jetty.client.util.BasicAuthentication;
+import org.eclipse.jetty.client.util.DigestAuthentication;
+import org.eclipse.jetty.client.util.StringContentProvider;
+import org.eclipse.jetty.http.HttpMethod;
+import org.openhab.binding.http.internal.config.HttpChannelConfig;
+import org.openhab.binding.http.internal.config.HttpChannelMode;
+import org.openhab.binding.http.internal.config.HttpThingConfig;
+import org.openhab.binding.http.internal.converter.AbstractTransformingItemConverter;
+import org.openhab.binding.http.internal.converter.ColorItemConverter;
+import org.openhab.binding.http.internal.converter.DimmerItemConverter;
+import org.openhab.binding.http.internal.converter.FixedValueMappingItemConverter;
+import org.openhab.binding.http.internal.converter.GenericItemConverter;
+import org.openhab.binding.http.internal.converter.ImageItemConverter;
+import org.openhab.binding.http.internal.converter.ItemValueConverter;
+import org.openhab.binding.http.internal.converter.PlayerItemConverter;
+import org.openhab.binding.http.internal.converter.RollershutterItemConverter;
+import org.openhab.binding.http.internal.http.Content;
+import org.openhab.binding.http.internal.http.HttpAuthException;
+import org.openhab.binding.http.internal.http.HttpResponseListener;
+import org.openhab.binding.http.internal.http.RefreshingUrlCache;
+import org.openhab.binding.http.internal.transform.ValueTransformationProvider;
+import org.openhab.core.library.types.DateTimeType;
+import org.openhab.core.library.types.DecimalType;
+import org.openhab.core.library.types.PointType;
+import org.openhab.core.library.types.StringType;
+import org.openhab.core.thing.Channel;
+import org.openhab.core.thing.ChannelUID;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.thing.ThingStatus;
+import org.openhab.core.thing.ThingStatusDetail;
+import org.openhab.core.thing.binding.BaseThingHandler;
+import org.openhab.core.types.Command;
+import org.openhab.core.types.RefreshType;
+import org.openhab.core.types.State;
+import org.openhab.core.types.StateDescription;
+import org.openhab.core.types.StateDescriptionFragmentBuilder;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The {@link HttpThingHandler} is responsible for handling commands, which are
+ * sent to one of the channels.
+ *
+ * @author Jan N. Klug - Initial contribution
+ */
+@NonNullByDefault
+public class HttpThingHandler extends BaseThingHandler {
+    private static final Set<Character> URL_PART_DELIMITER = Set.of('/', '?', '&');
+
+    private final Logger logger = LoggerFactory.getLogger(HttpThingHandler.class);
+    private final ValueTransformationProvider valueTransformationProvider;
+    private final HttpClientProvider httpClientProvider;
+    private HttpClient httpClient;
+    private final HttpDynamicStateDescriptionProvider httpDynamicStateDescriptionProvider;
+
+    private HttpThingConfig config = new HttpThingConfig();
+    private final Map<String, RefreshingUrlCache> urlHandlers = new HashMap<>();
+    private final Map<ChannelUID, ItemValueConverter> channels = new HashMap<>();
+    private final Map<ChannelUID, String> channelUrls = new HashMap<>();
+    private @Nullable Authentication authentication;
+
+    public HttpThingHandler(Thing thing, HttpClientProvider httpClientProvider,
+            ValueTransformationProvider valueTransformationProvider,
+            HttpDynamicStateDescriptionProvider httpDynamicStateDescriptionProvider) {
+        super(thing);
+        this.httpClientProvider = httpClientProvider;
+        this.httpClient = httpClientProvider.getSecureClient();
+        this.valueTransformationProvider = valueTransformationProvider;
+        this.httpDynamicStateDescriptionProvider = httpDynamicStateDescriptionProvider;
+    }
+
+    @Override
+    public void handleCommand(ChannelUID channelUID, Command command) {
+        ItemValueConverter itemValueConverter = channels.get(channelUID);
+        if (itemValueConverter == null) {
+            logger.warn("Cannot find channel implementation for channel {}.", channelUID);
+            return;
+        }
+
+        if (command instanceof RefreshType) {
+            String stateUrl = channelUrls.get(channelUID);
+            if (stateUrl != null) {
+                RefreshingUrlCache refreshingUrlCache = urlHandlers.get(stateUrl);
+                if (refreshingUrlCache != null) {
+                    try {
+                        refreshingUrlCache.get().ifPresent(itemValueConverter::process);
+                    } catch (IllegalArgumentException | IllegalStateException e) {
+                        logger.warn("Failed processing REFRESH command for channel {}: {}", channelUID, e.getMessage());
+                    }
+                }
+            }
+        } else {
+            try {
+                itemValueConverter.send(command);
+            } catch (IllegalArgumentException e) {
+                logger.warn("Failed to convert command '{}' to channel '{}' for sending", command, channelUID);
+            } catch (IllegalStateException e) {
+                logger.debug("Writing to read-only channel {} not permitted", channelUID);
+            }
+        }
+    }
+
+    @Override
+    public void initialize() {
+        config = getConfigAs(HttpThingConfig.class);
+
+        if (config.baseURL.isEmpty()) {
+            updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
+                    "Parameter baseURL must not be empty!");
+            return;
+        }
+        authentication = null;
+        if (!config.username.isEmpty()) {
+            try {
+                URI uri = new URI(config.baseURL);
+                switch (config.authMode) {
+                    case BASIC:
+                        authentication = new BasicAuthentication(uri, Authentication.ANY_REALM, config.username,
+                                config.password);
+                        logger.debug("Basic Authentication configured for thing '{}'", thing.getUID());
+                        break;
+                    case DIGEST:
+                        authentication = new DigestAuthentication(uri, Authentication.ANY_REALM, config.username,
+                                config.password);
+                        logger.debug("Digest Authentication configured for thing '{}'", thing.getUID());
+                        break;
+                    default:
+                        logger.warn("Unknown authentication method '{}' for thing '{}'", config.authMode,
+                                thing.getUID());
+                }
+                if (authentication != null) {
+                    AuthenticationStore authStore = httpClient.getAuthenticationStore();
+                    authStore.addAuthentication(authentication);
+                }
+            } catch (URISyntaxException e) {
+                updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
+                        "failed to create authentication: baseUrl is invalid");
+            }
+        } else {
+            logger.debug("No authentication configured for thing '{}'", thing.getUID());
+        }
+
+        if (config.ignoreSSLErrors) {
+            logger.info("Using the insecure client for thing '{}'.", thing.getUID());
+            httpClient = httpClientProvider.getInsecureClient();
+        } else {
+            logger.info("Using the secure client for thing '{}'.", thing.getUID());
+            httpClient = httpClientProvider.getSecureClient();
+        }
+
+        thing.getChannels().forEach(this::createChannel);
+
+        updateStatus(ThingStatus.ONLINE);
+    }
+
+    @Override
+    public void dispose() {
+        // stop update tasks
+        urlHandlers.values().forEach(RefreshingUrlCache::stop);
+
+        // clear lists
+        urlHandlers.clear();
+        channels.clear();
+        channelUrls.clear();
+
+        // remove state descriptions
+        httpDynamicStateDescriptionProvider.removeDescriptionsForThing(thing.getUID());
+
+        super.dispose();
+    }
+
+    /**
+     * create all necessary information to handle every channel
+     *
+     * @param channel a thing channel
+     */
+    private void createChannel(Channel channel) {
+        ChannelUID channelUID = channel.getUID();
+        HttpChannelConfig channelConfig = channel.getConfiguration().as(HttpChannelConfig.class);
+
+        String stateUrl = concatenateUrlParts(config.baseURL, channelConfig.stateExtension);
+        String commandUrl = channelConfig.commandExtension == null ? stateUrl
+                : concatenateUrlParts(config.baseURL, channelConfig.commandExtension);
+
+        String acceptedItemType = channel.getAcceptedItemType();
+        if (acceptedItemType == null) {
+            logger.warn("Cannot determine item-type for channel '{}'", channelUID);
+            return;
+        }
+
+        ItemValueConverter itemValueConverter;
+        switch (acceptedItemType) {
+            case "Color":
+                itemValueConverter = createItemConverter(ColorItemConverter::new, commandUrl, channelUID,
+                        channelConfig);
+                break;
+            case "DateTime":
+                itemValueConverter = createGenericItemConverter(commandUrl, channelUID, channelConfig,
+                        DateTimeType::new);
+                break;
+            case "Dimmer":
+                itemValueConverter = createItemConverter(DimmerItemConverter::new, commandUrl, channelUID,
+                        channelConfig);
+                break;
+            case "Contact":
+            case "Switch":
+                itemValueConverter = createItemConverter(FixedValueMappingItemConverter::new, commandUrl, channelUID,
+                        channelConfig);
+                break;
+            case "Image":
+                itemValueConverter = new ImageItemConverter(state -> updateState(channelUID, state));
+                break;
+            case "Location":
+                itemValueConverter = createGenericItemConverter(commandUrl, channelUID, channelConfig, PointType::new);
+                break;
+            case "Number":
+                itemValueConverter = createGenericItemConverter(commandUrl, channelUID, channelConfig,
+                        DecimalType::new);
+                break;
+            case "Player":
+                itemValueConverter = createItemConverter(PlayerItemConverter::new, commandUrl, channelUID,
+                        channelConfig);
+                break;
+            case "Rollershutter":
+                itemValueConverter = createItemConverter(RollershutterItemConverter::new, commandUrl, channelUID,
+                        channelConfig);
+                break;
+            case "String":
+                itemValueConverter = createGenericItemConverter(commandUrl, channelUID, channelConfig, StringType::new);
+                break;
+            default:
+                logger.warn("Unsupported item-type '{}'", channel.getAcceptedItemType());
+                return;
+        }
+
+        channels.put(channelUID, itemValueConverter);
+        if (channelConfig.mode != HttpChannelMode.WRITEONLY) {
+            channelUrls.put(channelUID, stateUrl);
+            urlHandlers.computeIfAbsent(stateUrl, url -> new RefreshingUrlCache(scheduler, httpClient, url, config))
+                    .addConsumer(itemValueConverter::process);
+        }
+
+        StateDescription stateDescription = StateDescriptionFragmentBuilder.create()
+                .withReadOnly(channelConfig.mode == HttpChannelMode.READONLY).build().toStateDescription();
+        if (stateDescription != null) {
+            // if the state description is not available, we don'tneed to add it
+            httpDynamicStateDescriptionProvider.setDescription(channelUID, stateDescription);
+        }
+    }
+
+    private void sendHttpValue(String commandUrl, String command) {
+        sendHttpValue(commandUrl, command, false);
+    }
+
+    private void sendHttpValue(String commandUrl, String command, boolean isRetry) {
+        try {
+            // format URL
+            URI finalUrl = new URI(String.format(commandUrl, new Date(), command));
+
+            // build request
+            Request request = httpClient.newRequest(finalUrl).timeout(config.timeout, TimeUnit.MILLISECONDS)
+                    .method(config.commandMethod);
+            if (config.commandMethod != HttpMethod.GET) {
+                final String contentType = config.contentType;
+                if (contentType != null) {
+                    request.content(new StringContentProvider(command), contentType);
+                } else {
+                    request.content(new StringContentProvider(command));
+                }
+            }
+
+            config.headers.forEach(header -> {
+                String[] keyValuePair = header.split("=", 2);
+                if (keyValuePair.length == 2) {
+                    request.header(keyValuePair[0], keyValuePair[1]);
+                } else {
+                    logger.warn("Splitting header '{}' failed. No '=' was found. Ignoring", header);
+                }
+            });
+
+            if (logger.isTraceEnabled()) {
+                logger.trace("Sending to '{}': {}", finalUrl, Util.requestToLogString(request));
+            }
+
+            CompletableFuture<@Nullable Content> f = new CompletableFuture<>();
+            f.exceptionally(e -> {
+                if (e instanceof HttpAuthException) {
+                    if (isRetry) {
+                        logger.warn("Retry after authentication failure failed again for '{}', failing here", finalUrl);
+                    } else {
+                        AuthenticationStore authStore = httpClient.getAuthenticationStore();
+                        Authentication.Result authResult = authStore.findAuthenticationResult(finalUrl);
+                        if (authResult != null) {
+                            authStore.removeAuthenticationResult(authResult);
+                            logger.debug("Cleared authentication result for '{}', retrying immediately", finalUrl);
+                            sendHttpValue(commandUrl, command, true);
+                        } else {
+                            logger.warn("Could not find authentication result for '{}', failing here", finalUrl);
+                        }
+                    }
+                }
+                return null;
+            });
+            request.send(new HttpResponseListener(f));
+        } catch (IllegalArgumentException | URISyntaxException e) {
+            logger.warn("Creating request for '{}' failed: {}", commandUrl, e.getMessage());
+        }
+    }
+
+    private String concatenateUrlParts(String baseUrl, @Nullable String extension) {
+        if (extension != null && !extension.isEmpty()) {
+            if (!URL_PART_DELIMITER.contains(baseUrl.charAt(baseUrl.length() - 1))
+                    && !URL_PART_DELIMITER.contains(extension.charAt(0))) {
+                return baseUrl + "/" + extension;
+            } else {
+                return baseUrl + extension;
+            }
+        } else {
+            return baseUrl;
+        }
+    }
+
+    private ItemValueConverter createItemConverter(AbstractTransformingItemConverter.Factory factory, String commandUrl,
+            ChannelUID channelUID, HttpChannelConfig channelConfig) {
+        return factory.create(state -> updateState(channelUID, state), command -> postCommand(channelUID, command),
+                command -> sendHttpValue(commandUrl, command),
+                valueTransformationProvider.getValueTransformation(channelConfig.stateTransformation),
+                valueTransformationProvider.getValueTransformation(channelConfig.commandTransformation), channelConfig);
+    }
+
+    private ItemValueConverter createGenericItemConverter(String commandUrl, ChannelUID channelUID,
+            HttpChannelConfig channelConfig, Function<String, State> toState) {
+        AbstractTransformingItemConverter.Factory factory = (state, command, value, stateTrans, commandTrans,
+                config) -> new GenericItemConverter(toState, state, command, value, stateTrans, commandTrans, config);
+        return createItemConverter(factory, commandUrl, channelUID, channelConfig);
+    }
+}
diff --git a/bundles/org.openhab.binding.http/src/main/java/org/openhab/binding/http/internal/Util.java b/bundles/org.openhab.binding.http/src/main/java/org/openhab/binding/http/internal/Util.java
new file mode 100644 (file)
index 0000000..b29b07d
--- /dev/null
@@ -0,0 +1,44 @@
+/**
+ * Copyright (c) 2010-2020 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.http.internal;
+
+import java.nio.charset.StandardCharsets;
+import java.util.stream.Collectors;
+import java.util.stream.StreamSupport;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jetty.client.api.ContentProvider;
+import org.eclipse.jetty.client.api.Request;
+import org.eclipse.jetty.http.HttpField;
+
+/**
+ * The {@link Util} is a utility class
+ * channels
+ *
+ * @author Jan N. Klug - Initial contribution
+ */
+@NonNullByDefault
+public class Util {
+
+    public static String requestToLogString(Request request) {
+        ContentProvider contentProvider = request.getContent();
+        String contentString = contentProvider == null ? "null"
+                : StreamSupport.stream(contentProvider.spliterator(), false)
+                        .map(b -> StandardCharsets.UTF_8.decode(b).toString()).collect(Collectors.joining(", "));
+        String logString = "Method = {" + request.getMethod() + "}, Headers = {"
+                + request.getHeaders().stream().map(HttpField::toString).collect(Collectors.joining(", "))
+                + "}, Content = {" + contentString + "}";
+
+        return logString;
+    }
+}
diff --git a/bundles/org.openhab.binding.http/src/main/java/org/openhab/binding/http/internal/config/HttpAuthMode.java b/bundles/org.openhab.binding.http/src/main/java/org/openhab/binding/http/internal/config/HttpAuthMode.java
new file mode 100644 (file)
index 0000000..c78c21f
--- /dev/null
@@ -0,0 +1,26 @@
+/**
+ * Copyright (c) 2010-2020 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.http.internal.config;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * The {@link HttpAuthMode} enum defines the method used for authentication.
+ *
+ * @author Jan N. Klug - Initial contribution
+ */
+@NonNullByDefault
+public enum HttpAuthMode {
+    BASIC,
+    DIGEST
+}
diff --git a/bundles/org.openhab.binding.http/src/main/java/org/openhab/binding/http/internal/config/HttpChannelConfig.java b/bundles/org.openhab.binding.http/src/main/java/org/openhab/binding/http/internal/config/HttpChannelConfig.java
new file mode 100644 (file)
index 0000000..24a0aeb
--- /dev/null
@@ -0,0 +1,137 @@
+/**
+ * Copyright (c) 2010-2020 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.http.internal.config;
+
+import java.math.BigDecimal;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.http.internal.converter.ColorItemConverter;
+import org.openhab.core.library.types.IncreaseDecreaseType;
+import org.openhab.core.library.types.NextPreviousType;
+import org.openhab.core.library.types.OnOffType;
+import org.openhab.core.library.types.OpenClosedType;
+import org.openhab.core.library.types.PlayPauseType;
+import org.openhab.core.library.types.RewindFastforwardType;
+import org.openhab.core.library.types.StopMoveType;
+import org.openhab.core.library.types.UpDownType;
+import org.openhab.core.types.Command;
+import org.openhab.core.types.State;
+
+/**
+ * The {@link HttpChannelConfig} class contains fields mapping channel configuration parameters.
+ *
+ * @author Jan N. Klug - Initial contribution
+ */
+@NonNullByDefault
+public class HttpChannelConfig {
+    private final Map<String, State> stringStateMap = new HashMap<>();
+    private final Map<Command, @Nullable String> commandStringMap = new HashMap<>();
+    private boolean initialized = false;
+
+    public @Nullable String stateExtension;
+    public @Nullable String commandExtension;
+    public @Nullable String stateTransformation;
+    public @Nullable String commandTransformation;
+
+    public HttpChannelMode mode = HttpChannelMode.READWRITE;
+
+    // switch, dimmer, color
+    public @Nullable String onValue;
+    public @Nullable String offValue;
+
+    // dimmer, color
+    public BigDecimal step = BigDecimal.ONE;
+    public @Nullable String increaseValue;
+    public @Nullable String decreaseValue;
+
+    // color
+    public ColorItemConverter.ColorMode colorMode = ColorItemConverter.ColorMode.RGB;
+
+    // contact
+    public @Nullable String openValue;
+    public @Nullable String closedValue;
+
+    // rollershutter
+    public @Nullable String upValue;
+    public @Nullable String downValue;
+    public @Nullable String stopValue;
+    public @Nullable String moveValue;
+
+    // player
+    public @Nullable String playValue;
+    public @Nullable String pauseValue;
+    public @Nullable String nextValue;
+    public @Nullable String previousValue;
+    public @Nullable String rewindValue;
+    public @Nullable String fastforwardValue;
+
+    /**
+     * maps a command to a user-defined string
+     *
+     * @param command the command to map
+     * @return a string or null if no mapping found
+     */
+    public @Nullable String commandToFixedValue(Command command) {
+        if (!initialized) {
+            createMaps();
+        }
+
+        return commandStringMap.get(command);
+    }
+
+    /**
+     * maps a user-defined string to a state
+     *
+     * @param string the string to map
+     * @return the state or null if no mapping found
+     */
+    public @Nullable State fixedValueToState(String string) {
+        if (!initialized) {
+            createMaps();
+        }
+
+        return stringStateMap.get(string);
+    }
+
+    private void createMaps() {
+        addToMaps(this.onValue, OnOffType.ON);
+        addToMaps(this.offValue, OnOffType.OFF);
+        addToMaps(this.openValue, OpenClosedType.OPEN);
+        addToMaps(this.closedValue, OpenClosedType.CLOSED);
+        addToMaps(this.upValue, UpDownType.UP);
+        addToMaps(this.downValue, UpDownType.DOWN);
+
+        commandStringMap.put(IncreaseDecreaseType.INCREASE, increaseValue);
+        commandStringMap.put(IncreaseDecreaseType.DECREASE, decreaseValue);
+        commandStringMap.put(StopMoveType.STOP, stopValue);
+        commandStringMap.put(StopMoveType.MOVE, moveValue);
+        commandStringMap.put(PlayPauseType.PLAY, playValue);
+        commandStringMap.put(PlayPauseType.PAUSE, pauseValue);
+        commandStringMap.put(NextPreviousType.NEXT, nextValue);
+        commandStringMap.put(NextPreviousType.PREVIOUS, previousValue);
+        commandStringMap.put(RewindFastforwardType.REWIND, rewindValue);
+        commandStringMap.put(RewindFastforwardType.FASTFORWARD, fastforwardValue);
+
+        initialized = true;
+    }
+
+    private void addToMaps(@Nullable String value, State state) {
+        if (value != null) {
+            commandStringMap.put((Command) state, value);
+            stringStateMap.put(value, state);
+        }
+    }
+}
diff --git a/bundles/org.openhab.binding.http/src/main/java/org/openhab/binding/http/internal/config/HttpChannelMode.java b/bundles/org.openhab.binding.http/src/main/java/org/openhab/binding/http/internal/config/HttpChannelMode.java
new file mode 100644 (file)
index 0000000..d5416ec
--- /dev/null
@@ -0,0 +1,27 @@
+/**
+ * Copyright (c) 2010-2020 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.http.internal.config;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * The {@link HttpChannelMode} enum defines control modes for channels
+ *
+ * @author Jan N. Klug - Initial contribution
+ */
+@NonNullByDefault
+public enum HttpChannelMode {
+    READONLY,
+    READWRITE,
+    WRITEONLY
+}
diff --git a/bundles/org.openhab.binding.http/src/main/java/org/openhab/binding/http/internal/config/HttpThingConfig.java b/bundles/org.openhab.binding.http/src/main/java/org/openhab/binding/http/internal/config/HttpThingConfig.java
new file mode 100644 (file)
index 0000000..a6cb794
--- /dev/null
@@ -0,0 +1,45 @@
+/**
+ * Copyright (c) 2010-2020 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.http.internal.config;
+
+import java.util.Collections;
+import java.util.List;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.eclipse.jetty.http.HttpMethod;
+
+/**
+ * The {@link HttpThingConfig} class contains fields mapping thing configuration parameters.
+ *
+ * @author Jan N. Klug - Initial contribution
+ */
+@NonNullByDefault
+public class HttpThingConfig {
+    public String baseURL = "";
+    public int refresh = 30;
+    public int timeout = 3000;
+
+    public String username = "";
+    public String password = "";
+    public HttpAuthMode authMode = HttpAuthMode.BASIC;
+
+    public HttpMethod commandMethod = HttpMethod.GET;
+
+    public @Nullable String encoding = null;
+    public @Nullable String contentType = null;
+
+    public boolean ignoreSSLErrors = false;
+
+    public List<String> headers = Collections.emptyList();
+}
diff --git a/bundles/org.openhab.binding.http/src/main/java/org/openhab/binding/http/internal/converter/AbstractTransformingItemConverter.java b/bundles/org.openhab.binding.http/src/main/java/org/openhab/binding/http/internal/converter/AbstractTransformingItemConverter.java
new file mode 100644 (file)
index 0000000..7a22081
--- /dev/null
@@ -0,0 +1,108 @@
+/**
+ * Copyright (c) 2010-2020 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.http.internal.converter;
+
+import java.util.function.Consumer;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.http.internal.config.HttpChannelConfig;
+import org.openhab.binding.http.internal.config.HttpChannelMode;
+import org.openhab.binding.http.internal.http.Content;
+import org.openhab.binding.http.internal.transform.ValueTransformation;
+import org.openhab.core.types.Command;
+import org.openhab.core.types.State;
+
+/**
+ * The {@link AbstractTransformingItemConverter} is a base class for an item converter with transformations
+ *
+ * @author Jan N. Klug - Initial contribution
+ */
+@NonNullByDefault
+public abstract class AbstractTransformingItemConverter implements ItemValueConverter {
+    private final Consumer<State> updateState;
+    private final Consumer<Command> postCommand;
+    private final @Nullable Consumer<String> sendHttpValue;
+    private final ValueTransformation stateTransformations;
+    private final ValueTransformation commandTransformations;
+
+    protected HttpChannelConfig channelConfig;
+
+    public AbstractTransformingItemConverter(Consumer<State> updateState, Consumer<Command> postCommand,
+            @Nullable Consumer<String> sendHttpValue, ValueTransformation stateTransformations,
+            ValueTransformation commandTransformations, HttpChannelConfig channelConfig) {
+        this.updateState = updateState;
+        this.postCommand = postCommand;
+        this.sendHttpValue = sendHttpValue;
+        this.stateTransformations = stateTransformations;
+        this.commandTransformations = commandTransformations;
+        this.channelConfig = channelConfig;
+    }
+
+    @Override
+    public void process(Content content) {
+        if (channelConfig.mode != HttpChannelMode.WRITEONLY) {
+            stateTransformations.apply(content.getAsString()).ifPresent(transformedValue -> {
+                Command command = toCommand(transformedValue);
+                if (command != null) {
+                    postCommand.accept(command);
+                } else {
+                    updateState.accept(toState(transformedValue));
+                }
+            });
+        } else {
+            throw new IllegalStateException("Write-only channel");
+        }
+    }
+
+    @Override
+    public void send(Command command) {
+        Consumer<String> sendHttpValue = this.sendHttpValue;
+        if (sendHttpValue != null && channelConfig.mode != HttpChannelMode.READONLY) {
+            commandTransformations.apply(toString(command)).ifPresent(sendHttpValue);
+        } else {
+            throw new IllegalStateException("Read-only channel");
+        }
+    }
+
+    /**
+     * check if this converter received a value that needs to be sent as command
+     *
+     * @param value the value
+     * @return the command or null
+     */
+    protected abstract @Nullable Command toCommand(String value);
+
+    /**
+     * convert the received value to a state
+     *
+     * @param value the value
+     * @return the state that represents the value of UNDEF if conversion failed
+     */
+    protected abstract State toState(String value);
+
+    /**
+     * convert a command to a string
+     *
+     * @param command the command
+     * @return the string representation of the command
+     */
+    protected abstract String toString(Command command);
+
+    @FunctionalInterface
+    public interface Factory {
+        ItemValueConverter create(Consumer<State> updateState, Consumer<Command> postCommand,
+                @Nullable Consumer<String> sendHttpValue, ValueTransformation stateTransformations,
+                ValueTransformation commandTransformations, HttpChannelConfig channelConfig);
+    }
+}
diff --git a/bundles/org.openhab.binding.http/src/main/java/org/openhab/binding/http/internal/converter/ColorItemConverter.java b/bundles/org.openhab.binding.http/src/main/java/org/openhab/binding/http/internal/converter/ColorItemConverter.java
new file mode 100644 (file)
index 0000000..8a3b82b
--- /dev/null
@@ -0,0 +1,145 @@
+/**
+ * Copyright (c) 2010-2020 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.http.internal.converter;
+
+import java.math.BigDecimal;
+import java.util.function.Consumer;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.http.internal.config.HttpChannelConfig;
+import org.openhab.binding.http.internal.transform.ValueTransformation;
+import org.openhab.core.library.types.HSBType;
+import org.openhab.core.library.types.PercentType;
+import org.openhab.core.types.Command;
+import org.openhab.core.types.State;
+import org.openhab.core.types.UnDefType;
+
+/**
+ * The {@link ColorItemConverter} implements {@link org.openhab.core.library.items.ColorItem} conversions
+ *
+ * @author Jan N. Klug - Initial contribution
+ */
+
+@NonNullByDefault
+public class ColorItemConverter extends AbstractTransformingItemConverter {
+    private static final BigDecimal BYTE_FACTOR = BigDecimal.valueOf(2.55);
+    private static final BigDecimal HUNDRED = BigDecimal.valueOf(100);
+    private static final Pattern TRIPLE_MATCHER = Pattern.compile("(\\d+),(\\d+),(\\d+)");
+
+    private State state = UnDefType.UNDEF;
+
+    public ColorItemConverter(Consumer<State> updateState, Consumer<Command> postCommand,
+            @Nullable Consumer<String> sendHttpValue, ValueTransformation stateTransformations,
+            ValueTransformation commandTransformations, HttpChannelConfig channelConfig) {
+        super(updateState, postCommand, sendHttpValue, stateTransformations, commandTransformations, channelConfig);
+        this.channelConfig = channelConfig;
+    }
+
+    @Override
+    protected @Nullable Command toCommand(String value) {
+        return null;
+    }
+
+    @Override
+    public String toString(Command command) {
+        String string = channelConfig.commandToFixedValue(command);
+        if (string != null) {
+            return string;
+        }
+
+        if (command instanceof HSBType) {
+            HSBType newState = (HSBType) command;
+            state = newState;
+            return hsbToString(newState);
+        } else if (command instanceof PercentType && state instanceof HSBType) {
+            HSBType newState = new HSBType(((HSBType) state).getBrightness(), ((HSBType) state).getSaturation(),
+                    (PercentType) command);
+            state = newState;
+            return hsbToString(newState);
+        }
+
+        throw new IllegalArgumentException("Command type '" + command.toString() + "' not supported");
+    }
+
+    @Override
+    public State toState(String string) {
+        State newState = UnDefType.UNDEF;
+        if (string.equals(channelConfig.onValue)) {
+            if (state instanceof HSBType) {
+                newState = new HSBType(((HSBType) state).getHue(), ((HSBType) state).getSaturation(),
+                        PercentType.HUNDRED);
+            } else {
+                newState = HSBType.WHITE;
+            }
+        } else if (string.equals(channelConfig.offValue)) {
+            if (state instanceof HSBType) {
+                newState = new HSBType(((HSBType) state).getHue(), ((HSBType) state).getSaturation(), PercentType.ZERO);
+            } else {
+                newState = HSBType.BLACK;
+            }
+        } else if (string.equals(channelConfig.increaseValue) && state instanceof HSBType) {
+            BigDecimal newBrightness = ((HSBType) state).getBrightness().toBigDecimal().add(channelConfig.step);
+            if (HUNDRED.compareTo(newBrightness) < 0) {
+                newBrightness = HUNDRED;
+            }
+            newState = new HSBType(((HSBType) state).getHue(), ((HSBType) state).getSaturation(),
+                    new PercentType(newBrightness));
+        } else if (string.equals(channelConfig.decreaseValue) && state instanceof HSBType) {
+            BigDecimal newBrightness = ((HSBType) state).getBrightness().toBigDecimal().subtract(channelConfig.step);
+            if (BigDecimal.ZERO.compareTo(newBrightness) > 0) {
+                newBrightness = BigDecimal.ZERO;
+            }
+            newState = new HSBType(((HSBType) state).getHue(), ((HSBType) state).getSaturation(),
+                    new PercentType(newBrightness));
+        } else {
+            Matcher matcher = TRIPLE_MATCHER.matcher(string);
+            if (matcher.matches()) {
+                switch (channelConfig.colorMode) {
+                    case RGB:
+                        int r = Integer.parseInt(matcher.group(0));
+                        int g = Integer.parseInt(matcher.group(1));
+                        int b = Integer.parseInt(matcher.group(2));
+                        newState = HSBType.fromRGB(r, g, b);
+                        break;
+                    case HSB:
+                        newState = new HSBType(string);
+                        break;
+                }
+            }
+        }
+
+        state = newState;
+        return newState;
+    }
+
+    private String hsbToString(HSBType state) {
+        switch (channelConfig.colorMode) {
+            case RGB:
+                PercentType[] rgb = state.toRGB();
+                return String.format("%1$d,%2$d,%3$d", rgb[0].toBigDecimal().multiply(BYTE_FACTOR).intValue(),
+                        rgb[1].toBigDecimal().multiply(BYTE_FACTOR).intValue(),
+                        rgb[2].toBigDecimal().multiply(BYTE_FACTOR).intValue());
+            case HSB:
+                return state.toString();
+        }
+        throw new IllegalStateException("Invalid colorMode setting");
+    }
+
+    public enum ColorMode {
+        RGB,
+        HSB
+    }
+}
diff --git a/bundles/org.openhab.binding.http/src/main/java/org/openhab/binding/http/internal/converter/DimmerItemConverter.java b/bundles/org.openhab.binding.http/src/main/java/org/openhab/binding/http/internal/converter/DimmerItemConverter.java
new file mode 100644 (file)
index 0000000..7464202
--- /dev/null
@@ -0,0 +1,103 @@
+/**
+ * Copyright (c) 2010-2020 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.http.internal.converter;
+
+import java.math.BigDecimal;
+import java.util.function.Consumer;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.http.internal.config.HttpChannelConfig;
+import org.openhab.binding.http.internal.transform.ValueTransformation;
+import org.openhab.core.library.types.PercentType;
+import org.openhab.core.types.Command;
+import org.openhab.core.types.State;
+import org.openhab.core.types.UnDefType;
+
+/**
+ * The {@link DimmerItemConverter} implements {@link org.openhab.core.library.items.DimmerItem} conversions
+ *
+ * @author Jan N. Klug - Initial contribution
+ */
+
+@NonNullByDefault
+public class DimmerItemConverter extends AbstractTransformingItemConverter {
+    private static final BigDecimal HUNDRED = BigDecimal.valueOf(100);
+
+    private State state = UnDefType.UNDEF;
+
+    public DimmerItemConverter(Consumer<State> updateState, Consumer<Command> postCommand,
+            @Nullable Consumer<String> sendHttpValue, ValueTransformation stateTransformations,
+            ValueTransformation commandTransformations, HttpChannelConfig channelConfig) {
+        super(updateState, postCommand, sendHttpValue, stateTransformations, commandTransformations, channelConfig);
+        this.channelConfig = channelConfig;
+    }
+
+    @Override
+    protected @Nullable Command toCommand(String value) {
+        return null;
+    }
+
+    @Override
+    public String toString(Command command) {
+        String string = channelConfig.commandToFixedValue(command);
+        if (string != null) {
+            return string;
+        }
+
+        if (command instanceof PercentType) {
+            return ((PercentType) command).toString();
+        }
+
+        throw new IllegalArgumentException("Command type '" + command.toString() + "' not supported");
+    }
+
+    @Override
+    public State toState(String string) {
+        State newState = UnDefType.UNDEF;
+
+        if (string.equals(channelConfig.onValue)) {
+            newState = PercentType.HUNDRED;
+        } else if (string.equals(channelConfig.offValue)) {
+            newState = PercentType.ZERO;
+        } else if (string.equals(channelConfig.increaseValue) && state instanceof PercentType) {
+            BigDecimal newBrightness = ((PercentType) state).toBigDecimal().add(channelConfig.step);
+            if (HUNDRED.compareTo(newBrightness) < 0) {
+                newBrightness = HUNDRED;
+            }
+            newState = new PercentType(newBrightness);
+        } else if (string.equals(channelConfig.decreaseValue) && state instanceof PercentType) {
+            BigDecimal newBrightness = ((PercentType) state).toBigDecimal().subtract(channelConfig.step);
+            if (BigDecimal.ZERO.compareTo(newBrightness) > 0) {
+                newBrightness = BigDecimal.ZERO;
+            }
+            newState = new PercentType(newBrightness);
+        } else {
+            try {
+                BigDecimal value = new BigDecimal(string);
+                if (value.compareTo(PercentType.HUNDRED.toBigDecimal()) > 0) {
+                    value = PercentType.HUNDRED.toBigDecimal();
+                }
+                if (value.compareTo(PercentType.ZERO.toBigDecimal()) < 0) {
+                    value = PercentType.ZERO.toBigDecimal();
+                }
+                newState = new PercentType(value);
+            } catch (NumberFormatException e) {
+                // ignore
+            }
+        }
+
+        state = newState;
+        return newState;
+    }
+}
diff --git a/bundles/org.openhab.binding.http/src/main/java/org/openhab/binding/http/internal/converter/FixedValueMappingItemConverter.java b/bundles/org.openhab.binding.http/src/main/java/org/openhab/binding/http/internal/converter/FixedValueMappingItemConverter.java
new file mode 100644 (file)
index 0000000..e363e88
--- /dev/null
@@ -0,0 +1,62 @@
+/**
+ * Copyright (c) 2010-2020 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.http.internal.converter;
+
+import java.util.function.Consumer;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.http.internal.config.HttpChannelConfig;
+import org.openhab.binding.http.internal.transform.ValueTransformation;
+import org.openhab.core.types.Command;
+import org.openhab.core.types.State;
+import org.openhab.core.types.UnDefType;
+
+/**
+ * The {@link FixedValueMappingItemConverter} implements mapping conversions for different item-types
+ *
+ * @author Jan N. Klug - Initial contribution
+ */
+
+@NonNullByDefault
+public class FixedValueMappingItemConverter extends AbstractTransformingItemConverter {
+
+    public FixedValueMappingItemConverter(Consumer<State> updateState, Consumer<Command> postCommand,
+            @Nullable Consumer<String> sendHttpValue, ValueTransformation stateTransformations,
+            ValueTransformation commandTransformations, HttpChannelConfig channelConfig) {
+        super(updateState, postCommand, sendHttpValue, stateTransformations, commandTransformations, channelConfig);
+    }
+
+    @Override
+    protected @Nullable Command toCommand(String value) {
+        return null;
+    }
+
+    @Override
+    public String toString(Command command) {
+        String value = channelConfig.commandToFixedValue(command);
+        if (value != null) {
+            return value;
+        }
+
+        throw new IllegalArgumentException(
+                "Command type '" + command.toString() + "' not supported or mapping not defined.");
+    }
+
+    @Override
+    public State toState(String string) {
+        State state = channelConfig.fixedValueToState(string);
+
+        return state != null ? state : UnDefType.UNDEF;
+    }
+}
diff --git a/bundles/org.openhab.binding.http/src/main/java/org/openhab/binding/http/internal/converter/GenericItemConverter.java b/bundles/org.openhab.binding.http/src/main/java/org/openhab/binding/http/internal/converter/GenericItemConverter.java
new file mode 100644 (file)
index 0000000..6828be7
--- /dev/null
@@ -0,0 +1,59 @@
+/**
+ * Copyright (c) 2010-2020 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.http.internal.converter;
+
+import java.util.function.Consumer;
+import java.util.function.Function;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.http.internal.config.HttpChannelConfig;
+import org.openhab.binding.http.internal.transform.ValueTransformation;
+import org.openhab.core.types.Command;
+import org.openhab.core.types.State;
+import org.openhab.core.types.UnDefType;
+
+/**
+ * The {@link GenericItemConverter} implements simple conversions for different item types
+ *
+ * @author Jan N. Klug - Initial contribution
+ */
+@NonNullByDefault
+public class GenericItemConverter extends AbstractTransformingItemConverter {
+    private final Function<String, State> toState;
+
+    public GenericItemConverter(Function<String, State> toState, Consumer<State> updateState,
+            Consumer<Command> postCommand, @Nullable Consumer<String> sendHttpValue,
+            ValueTransformation stateTransformations, ValueTransformation commandTransformations,
+            HttpChannelConfig channelConfig) {
+        super(updateState, postCommand, sendHttpValue, stateTransformations, commandTransformations, channelConfig);
+        this.toState = toState;
+    }
+
+    protected State toState(String value) {
+        try {
+            return toState.apply(value);
+        } catch (IllegalArgumentException e) {
+            return UnDefType.UNDEF;
+        }
+    }
+
+    @Override
+    protected @Nullable Command toCommand(String value) {
+        return null;
+    }
+
+    protected String toString(Command command) {
+        return command.toString();
+    }
+}
diff --git a/bundles/org.openhab.binding.http/src/main/java/org/openhab/binding/http/internal/converter/ImageItemConverter.java b/bundles/org.openhab.binding.http/src/main/java/org/openhab/binding/http/internal/converter/ImageItemConverter.java
new file mode 100644 (file)
index 0000000..455ccec
--- /dev/null
@@ -0,0 +1,48 @@
+/**
+ * Copyright (c) 2010-2020 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.http.internal.converter;
+
+import java.util.function.Consumer;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.http.internal.http.Content;
+import org.openhab.core.library.types.RawType;
+import org.openhab.core.types.Command;
+import org.openhab.core.types.State;
+
+/**
+ * The {@link ImageItemConverter} implements {@link org.openhab.core.library.items.ImageItem} conversions
+ *
+ * @author Jan N. Klug - Initial contribution
+ */
+
+@NonNullByDefault
+public class ImageItemConverter implements ItemValueConverter {
+    private final Consumer<State> updateState;
+
+    public ImageItemConverter(Consumer<State> updateState) {
+        this.updateState = updateState;
+    }
+
+    @Override
+    public void process(Content content) {
+        String mediaType = content.getMediaType();
+        updateState.accept(
+                new RawType(content.getRawContent(), mediaType != null ? mediaType : RawType.DEFAULT_MIME_TYPE));
+    }
+
+    @Override
+    public void send(Command command) {
+        throw new IllegalStateException("Read-only channel");
+    }
+}
diff --git a/bundles/org.openhab.binding.http/src/main/java/org/openhab/binding/http/internal/converter/ItemValueConverter.java b/bundles/org.openhab.binding.http/src/main/java/org/openhab/binding/http/internal/converter/ItemValueConverter.java
new file mode 100644 (file)
index 0000000..0ac75c8
--- /dev/null
@@ -0,0 +1,41 @@
+/**
+ * Copyright (c) 2010-2020 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.http.internal.converter;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.http.internal.http.Content;
+import org.openhab.core.types.Command;
+
+/**
+ * The {@link ItemValueConverter} defines the interface for converting received content to item state and converting
+ * comannds to sending value
+ *
+ * @author Jan N. Klug - Initial contribution
+ */
+@NonNullByDefault
+public interface ItemValueConverter {
+
+    /**
+     * called to process a given content for this channel
+     *
+     * @param content content of the HTTP request
+     */
+    void process(Content content);
+
+    /**
+     * called to send a command to this channel
+     *
+     * @param command
+     */
+    void send(Command command);
+}
diff --git a/bundles/org.openhab.binding.http/src/main/java/org/openhab/binding/http/internal/converter/PlayerItemConverter.java b/bundles/org.openhab.binding.http/src/main/java/org/openhab/binding/http/internal/converter/PlayerItemConverter.java
new file mode 100644 (file)
index 0000000..9a7764b
--- /dev/null
@@ -0,0 +1,79 @@
+/**
+ * Copyright (c) 2010-2020 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.http.internal.converter;
+
+import java.util.function.Consumer;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.http.internal.config.HttpChannelConfig;
+import org.openhab.binding.http.internal.transform.ValueTransformation;
+import org.openhab.core.library.types.NextPreviousType;
+import org.openhab.core.library.types.PlayPauseType;
+import org.openhab.core.library.types.RewindFastforwardType;
+import org.openhab.core.types.Command;
+import org.openhab.core.types.State;
+import org.openhab.core.types.UnDefType;
+
+/**
+ * The {@link PlayerItemConverter} implements {@link org.openhab.core.library.items.RollershutterItem}
+ * conversions
+ *
+ * @author Jan N. Klug - Initial contribution
+ */
+
+@NonNullByDefault
+public class PlayerItemConverter extends AbstractTransformingItemConverter {
+    private final HttpChannelConfig channelConfig;
+
+    public PlayerItemConverter(Consumer<State> updateState, Consumer<Command> postCommand,
+            @Nullable Consumer<String> sendHttpValue, ValueTransformation stateTransformations,
+            ValueTransformation commandTransformations, HttpChannelConfig channelConfig) {
+        super(updateState, postCommand, sendHttpValue, stateTransformations, commandTransformations, channelConfig);
+        this.channelConfig = channelConfig;
+    }
+
+    @Override
+    public String toString(Command command) {
+        String string = channelConfig.commandToFixedValue(command);
+        if (string != null) {
+            return string;
+        }
+
+        throw new IllegalArgumentException("Command type '" + command.toString() + "' not supported");
+    }
+
+    @Override
+    protected @Nullable Command toCommand(String string) {
+        if (string.equals(channelConfig.playValue)) {
+            return PlayPauseType.PLAY;
+        } else if (string.equals(channelConfig.pauseValue)) {
+            return PlayPauseType.PAUSE;
+        } else if (string.equals(channelConfig.nextValue)) {
+            return NextPreviousType.NEXT;
+        } else if (string.equals(channelConfig.previousValue)) {
+            return NextPreviousType.PREVIOUS;
+        } else if (string.equals(channelConfig.rewindValue)) {
+            return RewindFastforwardType.REWIND;
+        } else if (string.equals(channelConfig.fastforwardValue)) {
+            return RewindFastforwardType.FASTFORWARD;
+        }
+
+        return null;
+    }
+
+    @Override
+    public State toState(String string) {
+        return UnDefType.UNDEF;
+    }
+}
diff --git a/bundles/org.openhab.binding.http/src/main/java/org/openhab/binding/http/internal/converter/RollershutterItemConverter.java b/bundles/org.openhab.binding.http/src/main/java/org/openhab/binding/http/internal/converter/RollershutterItemConverter.java
new file mode 100644 (file)
index 0000000..b775687
--- /dev/null
@@ -0,0 +1,100 @@
+/**
+ * Copyright (c) 2010-2020 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.http.internal.converter;
+
+import java.math.BigDecimal;
+import java.util.function.Consumer;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.http.internal.config.HttpChannelConfig;
+import org.openhab.binding.http.internal.transform.ValueTransformation;
+import org.openhab.core.library.types.PercentType;
+import org.openhab.core.library.types.StopMoveType;
+import org.openhab.core.library.types.UpDownType;
+import org.openhab.core.types.Command;
+import org.openhab.core.types.State;
+import org.openhab.core.types.UnDefType;
+
+/**
+ * The {@link RollershutterItemConverter} implements {@link org.openhab.core.library.items.RollershutterItem}
+ * conversions
+ *
+ * @author Jan N. Klug - Initial contribution
+ */
+
+@NonNullByDefault
+public class RollershutterItemConverter extends AbstractTransformingItemConverter {
+    private final HttpChannelConfig channelConfig;
+
+    public RollershutterItemConverter(Consumer<State> updateState, Consumer<Command> postCommand,
+            @Nullable Consumer<String> sendHttpValue, ValueTransformation stateTransformations,
+            ValueTransformation commandTransformations, HttpChannelConfig channelConfig) {
+        super(updateState, postCommand, sendHttpValue, stateTransformations, commandTransformations, channelConfig);
+        this.channelConfig = channelConfig;
+    }
+
+    @Override
+    public String toString(Command command) {
+        String string = channelConfig.commandToFixedValue(command);
+        if (string != null) {
+            return string;
+        }
+
+        if (command instanceof PercentType) {
+            final String downValue = channelConfig.downValue;
+            final String upValue = channelConfig.upValue;
+            if (command.equals(PercentType.HUNDRED) && downValue != null) {
+                return downValue;
+            } else if (command.equals(PercentType.ZERO) && upValue != null) {
+                return upValue;
+            } else {
+                return ((PercentType) command).toString();
+            }
+        }
+
+        throw new IllegalArgumentException("Command type '" + command.toString() + "' not supported");
+    }
+
+    @Override
+    protected @Nullable Command toCommand(String string) {
+        if (string.equals(channelConfig.upValue)) {
+            return UpDownType.UP;
+        } else if (string.equals(channelConfig.downValue)) {
+            return UpDownType.DOWN;
+        } else if (string.equals(channelConfig.moveValue)) {
+            return StopMoveType.MOVE;
+        } else if (string.equals(channelConfig.stopValue)) {
+            return StopMoveType.STOP;
+        }
+
+        return null;
+    }
+
+    @Override
+    public State toState(String string) {
+        try {
+            BigDecimal value = new BigDecimal(string);
+            if (value.compareTo(PercentType.HUNDRED.toBigDecimal()) > 0) {
+                return PercentType.HUNDRED;
+            }
+            if (value.compareTo(PercentType.ZERO.toBigDecimal()) < 0) {
+                return PercentType.ZERO;
+            }
+        } catch (NumberFormatException e) {
+            // ignore
+        }
+
+        return UnDefType.UNDEF;
+    }
+}
diff --git a/bundles/org.openhab.binding.http/src/main/java/org/openhab/binding/http/internal/http/Content.java b/bundles/org.openhab.binding.http/src/main/java/org/openhab/binding/http/internal/http/Content.java
new file mode 100644 (file)
index 0000000..72d19b9
--- /dev/null
@@ -0,0 +1,55 @@
+/**
+ * Copyright (c) 2010-2020 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.http.internal.http;
+
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+
+/**
+ * The {@link Content} defines the pre-processed response
+ *
+ * @author Jan N. Klug - Initial contribution
+ */
+@NonNullByDefault
+public class Content {
+    private final byte[] rawContent;
+    private final Charset encoding;
+    private final @Nullable String mediaType;
+
+    public Content(byte[] rawContent, String encoding, @Nullable String mediaType) {
+        this.rawContent = rawContent;
+        this.mediaType = mediaType;
+
+        Charset finalEncoding = StandardCharsets.UTF_8;
+        try {
+            finalEncoding = Charset.forName(encoding);
+        } catch (IllegalArgumentException e) {
+        }
+        this.encoding = finalEncoding;
+    }
+
+    public byte[] getRawContent() {
+        return rawContent;
+    }
+
+    public String getAsString() {
+        return new String(rawContent, encoding);
+    }
+
+    public @Nullable String getMediaType() {
+        return mediaType;
+    }
+}
diff --git a/bundles/org.openhab.binding.http/src/main/java/org/openhab/binding/http/internal/http/HttpAuthException.java b/bundles/org.openhab.binding.http/src/main/java/org/openhab/binding/http/internal/http/HttpAuthException.java
new file mode 100644 (file)
index 0000000..8fc19cf
--- /dev/null
@@ -0,0 +1,33 @@
+/**
+ * Copyright (c) 2010-2020 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.http.internal.http;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * The {@link HttpAuthException} is an exception after authorization errors
+ *
+ * @author Jan N. Klug - Initial contribution
+ */
+@NonNullByDefault
+public class HttpAuthException extends Exception {
+    private static final long serialVersionUID = 1L;
+
+    public HttpAuthException() {
+        super();
+    }
+
+    public HttpAuthException(String message) {
+        super(message);
+    }
+}
diff --git a/bundles/org.openhab.binding.http/src/main/java/org/openhab/binding/http/internal/http/HttpResponseListener.java b/bundles/org.openhab.binding.http/src/main/java/org/openhab/binding/http/internal/http/HttpResponseListener.java
new file mode 100644 (file)
index 0000000..09348ed
--- /dev/null
@@ -0,0 +1,92 @@
+/**
+ * Copyright (c) 2010-2020 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.http.internal.http;
+
+import java.nio.charset.StandardCharsets;
+import java.util.concurrent.CompletableFuture;
+import java.util.stream.Collectors;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.eclipse.jetty.client.api.Request;
+import org.eclipse.jetty.client.api.Response;
+import org.eclipse.jetty.client.api.Result;
+import org.eclipse.jetty.client.util.BufferingResponseListener;
+import org.eclipse.jetty.http.HttpField;
+import org.eclipse.jetty.http.HttpStatus;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The {@link HttpResponseListener} is responsible for processing the result of a HTTP request
+ *
+ * @author Jan N. Klug - Initial contribution
+ */
+@NonNullByDefault
+public class HttpResponseListener extends BufferingResponseListener {
+    private final Logger logger = LoggerFactory.getLogger(HttpResponseListener.class);
+    private final CompletableFuture<@Nullable Content> future;
+    private final String fallbackEncoding;
+
+    public HttpResponseListener(CompletableFuture<@Nullable Content> future) {
+        this(future, null);
+    }
+
+    public HttpResponseListener(CompletableFuture<@Nullable Content> future, @Nullable String fallbackEncoding) {
+        this.future = future;
+        this.fallbackEncoding = fallbackEncoding != null ? fallbackEncoding : StandardCharsets.UTF_8.name();
+    }
+
+    @Override
+    public void onComplete(@NonNullByDefault({}) Result result) {
+        Response response = result.getResponse();
+        if (logger.isTraceEnabled()) {
+            logger.trace("Received from '{}': {}", result.getRequest().getURI(), responseToLogString(response));
+        }
+        Request request = result.getRequest();
+        if (result.isFailed()) {
+            logger.warn("Requesting '{}' (method='{}', content='{}') failed: {}", request.getURI(), request.getMethod(),
+                    request.getContent(), result.getFailure().getMessage());
+            future.complete(null);
+        } else {
+            switch (response.getStatus()) {
+                case HttpStatus.OK_200:
+                    byte[] content = getContent();
+                    String encoding = getEncoding();
+                    if (content != null) {
+                        future.complete(
+                                new Content(content, encoding == null ? fallbackEncoding : encoding, getMediaType()));
+                    } else {
+                        future.complete(null);
+                    }
+                    break;
+                case HttpStatus.UNAUTHORIZED_401:
+                    logger.debug("Requesting '{}' (method='{}', content='{}') failed: Authorization error",
+                            request.getURI(), request.getMethod(), request.getContent());
+                    future.completeExceptionally(new HttpAuthException());
+                    break;
+                default:
+                    logger.warn("Requesting '{}' (method='{}', content='{}') failed: {} {}", request.getURI(),
+                            request.getMethod(), request.getContent(), response.getStatus(), response.getReason());
+                    future.completeExceptionally(new IllegalStateException("Response - Code" + response.getStatus()));
+            }
+        }
+    }
+
+    private String responseToLogString(Response response) {
+        String logString = "Code = {" + response.getStatus() + "}, Headers = {"
+                + response.getHeaders().stream().map(HttpField::toString).collect(Collectors.joining(", "))
+                + "}, Content = {" + getContentAsString() + "}";
+        return logString;
+    }
+}
diff --git a/bundles/org.openhab.binding.http/src/main/java/org/openhab/binding/http/internal/http/RefreshingUrlCache.java b/bundles/org.openhab.binding.http/src/main/java/org/openhab/binding/http/internal/http/RefreshingUrlCache.java
new file mode 100644 (file)
index 0000000..8a8d4ef
--- /dev/null
@@ -0,0 +1,160 @@
+/**
+ * Copyright (c) 2010-2020 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.http.internal.http;
+
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.Date;
+import java.util.List;
+import java.util.Optional;
+import java.util.Set;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.TimeUnit;
+import java.util.function.Consumer;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.eclipse.jetty.client.HttpClient;
+import org.eclipse.jetty.client.api.Authentication;
+import org.eclipse.jetty.client.api.AuthenticationStore;
+import org.eclipse.jetty.client.api.Request;
+import org.openhab.binding.http.internal.Util;
+import org.openhab.binding.http.internal.config.HttpThingConfig;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The {@link RefreshingUrlCache} is responsible for requesting from a single URL and passing the content to the
+ * channels
+ *
+ * @author Jan N. Klug - Initial contribution
+ */
+@NonNullByDefault
+public class RefreshingUrlCache {
+    private final Logger logger = LoggerFactory.getLogger(RefreshingUrlCache.class);
+
+    private final String url;
+    private final HttpClient httpClient;
+    private final int timeout;
+    private final @Nullable String fallbackEncoding;
+    private final Set<Consumer<Content>> consumers = ConcurrentHashMap.newKeySet();
+    private final List<String> headers;
+
+    private final ScheduledFuture<?> future;
+    private @Nullable Content lastContent;
+
+    public RefreshingUrlCache(ScheduledExecutorService executor, HttpClient httpClient, String url,
+            HttpThingConfig thingConfig) {
+        this.httpClient = httpClient;
+        this.url = url;
+        this.timeout = thingConfig.timeout;
+        this.headers = thingConfig.headers;
+        fallbackEncoding = thingConfig.encoding;
+
+        future = executor.scheduleWithFixedDelay(this::refresh, 0, thingConfig.refresh, TimeUnit.SECONDS);
+        logger.trace("Started refresh task for URL '{}' with interval {}s", url, thingConfig.refresh);
+    }
+
+    private void refresh() {
+        refresh(false);
+    }
+
+    private void refresh(boolean isRetry) {
+        if (consumers.isEmpty()) {
+            // do not refresh if we don't have listeners
+            return;
+        }
+
+        // format URL
+        try {
+            URI finalUrl = new URI(String.format(this.url, new Date()));
+
+            logger.trace("Requesting refresh (retry={}) from '{}' with timeout {}ms", isRetry, finalUrl, timeout);
+            Request request = httpClient.newRequest(finalUrl).timeout(timeout, TimeUnit.MILLISECONDS);
+
+            headers.forEach(header -> {
+                String[] keyValuePair = header.split("=", 2);
+                if (keyValuePair.length == 2) {
+                    request.header(keyValuePair[0].trim(), keyValuePair[1].trim());
+                } else {
+                    logger.warn("Splitting header '{}' failed. No '=' was found. Ignoring", header);
+                }
+            });
+
+            CompletableFuture<@Nullable Content> response = new CompletableFuture<>();
+            response.exceptionally(e -> {
+                if (e instanceof HttpAuthException) {
+                    if (isRetry) {
+                        logger.warn("Retry after authentication  failure failed again for '{}', failing here",
+                                finalUrl);
+                    } else {
+                        AuthenticationStore authStore = httpClient.getAuthenticationStore();
+                        Authentication.Result authResult = authStore.findAuthenticationResult(finalUrl);
+                        if (authResult != null) {
+                            authStore.removeAuthenticationResult(authResult);
+                            logger.debug("Cleared authentication result for '{}', retrying immediately", finalUrl);
+                            refresh(true);
+                        } else {
+                            logger.warn("Could not find authentication result for '{}', failing here", finalUrl);
+                        }
+                    }
+                }
+                return null;
+            }).thenAccept(this::processResult);
+
+            if (logger.isTraceEnabled()) {
+                logger.trace("Sending to '{}': {}", finalUrl, Util.requestToLogString(request));
+            }
+
+            request.send(new HttpResponseListener(response, fallbackEncoding));
+        } catch (IllegalArgumentException | URISyntaxException e) {
+            logger.warn("Creating request for '{}' failed: {}", url, e.getMessage());
+        }
+    }
+
+    public void stop() {
+        // clearing all listeners to prevent further updates
+        consumers.clear();
+        future.cancel(false);
+        logger.trace("Stopped refresh task for URL '{}'", url);
+    }
+
+    public void addConsumer(Consumer<Content> consumer) {
+        consumers.add(consumer);
+    }
+
+    public Optional<Content> get() {
+        final Content content = lastContent;
+        if (content == null) {
+            return Optional.empty();
+        } else {
+            return Optional.of(content);
+        }
+    }
+
+    private void processResult(@Nullable Content content) {
+        if (content != null) {
+            for (Consumer<Content> consumer : consumers) {
+                try {
+                    consumer.accept(content);
+                } catch (IllegalArgumentException | IllegalStateException e) {
+                    logger.warn("Failed processing result for URL {}: {}", url, e.getMessage());
+                }
+            }
+        }
+        lastContent = content;
+    }
+}
diff --git a/bundles/org.openhab.binding.http/src/main/java/org/openhab/binding/http/internal/transform/CascadedValueTransformationImpl.java b/bundles/org.openhab.binding.http/src/main/java/org/openhab/binding/http/internal/transform/CascadedValueTransformationImpl.java
new file mode 100644 (file)
index 0000000..a163c9f
--- /dev/null
@@ -0,0 +1,53 @@
+/**
+ * Copyright (c) 2010-2020 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.http.internal.transform;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Optional;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.core.transform.TransformationService;
+
+/**
+ * The {@link CascadedValueTransformationImpl} implements {@link ValueTransformation for a cascaded set of
+ * transformations}
+ *
+ * @author Jan N. Klug - Initial contribution
+ */
+@NonNullByDefault
+public class CascadedValueTransformationImpl implements ValueTransformation {
+    private final List<ValueTransformation> transformations;
+
+    public CascadedValueTransformationImpl(String transformationString,
+            Function<String, @Nullable TransformationService> transformationServiceSupplier) {
+        transformations = Arrays.stream(transformationString.split("∩")).filter(s -> !s.isEmpty())
+                .map(transformation -> new SingleValueTransformation(transformation, transformationServiceSupplier))
+                .collect(Collectors.toList());
+    }
+
+    @Override
+    public Optional<String> apply(String value) {
+        Optional<String> valueOptional = Optional.of(value);
+
+        // process all transformations
+        for (ValueTransformation transformation : transformations) {
+            valueOptional = valueOptional.flatMap(transformation::apply);
+        }
+
+        return valueOptional;
+    }
+}
diff --git a/bundles/org.openhab.binding.http/src/main/java/org/openhab/binding/http/internal/transform/NoOpValueTransformation.java b/bundles/org.openhab.binding.http/src/main/java/org/openhab/binding/http/internal/transform/NoOpValueTransformation.java
new file mode 100644 (file)
index 0000000..ff1bf5a
--- /dev/null
@@ -0,0 +1,41 @@
+/**
+ * Copyright (c) 2010-2020 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.http.internal.transform;
+
+import java.util.Optional;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * The {@link NoOpValueTransformation} implements a no-op (identity) transformation
+ *
+ * @author Jan N. Klug - Initial contribution
+ */
+@NonNullByDefault
+public class NoOpValueTransformation implements ValueTransformation {
+    private static final NoOpValueTransformation NO_OP_VALUE_TRANSFORMATION = new NoOpValueTransformation();
+
+    @Override
+    public Optional<String> apply(String value) {
+        return Optional.of(value);
+    }
+
+    /**
+     * get the static value transformation for identity
+     *
+     * @return
+     */
+    public static ValueTransformation getInstance() {
+        return NO_OP_VALUE_TRANSFORMATION;
+    }
+}
diff --git a/bundles/org.openhab.binding.http/src/main/java/org/openhab/binding/http/internal/transform/SingleValueTransformation.java b/bundles/org.openhab.binding.http/src/main/java/org/openhab/binding/http/internal/transform/SingleValueTransformation.java
new file mode 100644 (file)
index 0000000..fa98e68
--- /dev/null
@@ -0,0 +1,89 @@
+/**
+ * Copyright (c) 2010-2020 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.http.internal.transform;
+
+import java.lang.ref.WeakReference;
+import java.util.Optional;
+import java.util.function.Function;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.core.transform.TransformationException;
+import org.openhab.core.transform.TransformationService;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * A transformation for a value used in {@HttpChannel}.
+ *
+ * @author David Graeff - Initial contribution
+ * @author Jan N. Klug - adapted from MQTT binding to HTTP binding
+ */
+@NonNullByDefault
+public class SingleValueTransformation implements ValueTransformation {
+    private final Logger logger = LoggerFactory.getLogger(SingleValueTransformation.class);
+    private final Function<String, @Nullable TransformationService> transformationServiceSupplier;
+    private WeakReference<@Nullable TransformationService> transformationService = new WeakReference<>(null);
+    private final String pattern;
+    private final String serviceName;
+
+    /**
+     * Creates a new channel state transformer.
+     *
+     * @param pattern A transformation pattern, starting with the transformation service
+     *            name, followed by a colon and the transformation itself.
+     * @param transformationServiceSupplier
+     */
+    public SingleValueTransformation(String pattern,
+            Function<String, @Nullable TransformationService> transformationServiceSupplier) {
+        this.transformationServiceSupplier = transformationServiceSupplier;
+        int index = pattern.indexOf(':');
+        if (index == -1) {
+            throw new IllegalArgumentException(
+                    "The transformation pattern must consist of the type and the pattern separated by a colon");
+        }
+        this.serviceName = pattern.substring(0, index).toUpperCase();
+        this.pattern = pattern.substring(index + 1);
+    }
+
+    @Override
+    public Optional<String> apply(String value) {
+        TransformationService transformationService = this.transformationService.get();
+        if (transformationService == null) {
+            transformationService = transformationServiceSupplier.apply(serviceName);
+            if (transformationService == null) {
+                logger.warn("Transformation service {} for pattern {} not found!", serviceName, pattern);
+                return Optional.empty();
+            }
+            this.transformationService = new WeakReference<>(transformationService);
+        }
+
+        try {
+            String result = transformationService.transform(pattern, value);
+            if (result == null) {
+                logger.debug("Transformation {} returned empty result when applied to {}.", this, value);
+                return Optional.empty();
+            }
+            return Optional.of(result);
+        } catch (TransformationException e) {
+            logger.warn("Executing transformation {} failed: {}", this, e.getMessage());
+        }
+
+        return Optional.empty();
+    }
+
+    @Override
+    public String toString() {
+        return "ChannelStateTransformation{pattern='" + pattern + "', serviceName='" + serviceName + "'}";
+    }
+}
diff --git a/bundles/org.openhab.binding.http/src/main/java/org/openhab/binding/http/internal/transform/ValueTransformation.java b/bundles/org.openhab.binding.http/src/main/java/org/openhab/binding/http/internal/transform/ValueTransformation.java
new file mode 100644 (file)
index 0000000..95f99db
--- /dev/null
@@ -0,0 +1,34 @@
+/**
+ * Copyright (c) 2010-2020 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.http.internal.transform;
+
+import java.util.Optional;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * The {@link ValueTransformation} applies a set of transformations to a value
+ *
+ * @author Jan N. Klug - Initial contribution
+ */
+@NonNullByDefault
+public interface ValueTransformation {
+
+    /**
+     * applies the value transformation to a value
+     *
+     * @param value The value
+     * @return Optional of string representing the transformed value (empty if transformation not present or failed)
+     */
+    Optional<String> apply(String value);
+}
diff --git a/bundles/org.openhab.binding.http/src/main/java/org/openhab/binding/http/internal/transform/ValueTransformationProvider.java b/bundles/org.openhab.binding.http/src/main/java/org/openhab/binding/http/internal/transform/ValueTransformationProvider.java
new file mode 100644 (file)
index 0000000..12dd872
--- /dev/null
@@ -0,0 +1,33 @@
+/**
+ * Copyright (c) 2010-2020 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.http.internal.transform;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+
+/**
+ * The {@link ValueTransformationProvider} allows to retrieve a transformation service by name
+ *
+ * @author Jan N. Klug - Initial contribution
+ */
+@NonNullByDefault
+public interface ValueTransformationProvider {
+
+    /**
+     *
+     * @param pattern A transformation pattern, starting with the transformation service
+     *            * name, followed by a colon and the transformation itself.
+     * @return
+     */
+    ValueTransformation getValueTransformation(@Nullable String pattern);
+}
diff --git a/bundles/org.openhab.binding.http/src/main/resources/OH-INF/binding/binding.xml b/bundles/org.openhab.binding.http/src/main/resources/OH-INF/binding/binding.xml
new file mode 100644 (file)
index 0000000..714ff78
--- /dev/null
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<binding:binding id="http" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xmlns:binding="https://openhab.org/schemas/binding/v1.0.0"
+       xsi:schemaLocation="https://openhab.org/schemas/binding/v1.0.0 https://openhab.org/schemas/binding-1.0.0.xsd">
+
+       <name>HTTP Binding</name>
+       <description>This is the binding for retrieving and processing HTTP resources.</description>
+       <author>Jan N. Klug</author>
+
+</binding:binding>
diff --git a/bundles/org.openhab.binding.http/src/main/resources/OH-INF/config/config.xml b/bundles/org.openhab.binding.http/src/main/resources/OH-INF/config/config.xml
new file mode 100644 (file)
index 0000000..51a5116
--- /dev/null
@@ -0,0 +1,350 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<config-description:config-descriptions
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xmlns:config-description="https://openhab.org/schemas/config-description/v1.0.0"
+       xsi:schemaLocation="https://openhab.org/schemas/config-description/v1.0.0 https://openhab.org/schemas/config-description-1.0.0.xsd">
+
+       <config-description uri="channel-type:http:channel-config">
+               <parameter name="stateExtension" type="text">
+                       <label>State URL Extension</label>
+                       <description>This value is added to the base URL configured in the thing for retrieving values.</description>
+                       <advanced>true</advanced>
+               </parameter>
+               <parameter name="commandExtension" type="text">
+                       <label>Command URL Extension</label>
+                       <description>This value is added to the base URL configured in the thing for sending values.</description>
+                       <advanced>true</advanced>
+               </parameter>
+               <parameter name="stateTransformation" type="text">
+                       <label>State Transformation</label>
+                       <description>Transformation pattern used when receiving values.</description>
+               </parameter>
+               <parameter name="commandTransformation" type="text">
+                       <label>Command Transformation</label>
+                       <description>Transformation pattern used when sending values.</description>
+               </parameter>
+               <parameter name="mode" type="text">
+                       <label>Read/Write Mode</label>
+                       <options>
+                               <option value="READWRITE">Read/Write</option>
+                               <option value="READONLY">Read Only</option>
+                               <option value="WRITEONLY">Write Only</option>
+                       </options>
+                       <limitToOptions>true</limitToOptions>
+                       <advanced>true</advanced>
+                       <default>READWRITE</default>
+               </parameter>
+       </config-description>
+
+       <config-description uri="channel-type:http:channel-config-color">
+               <parameter name="stateExtension" type="text">
+                       <label>State URL Extension</label>
+                       <description>This value is added to the base URL configured in the thing for retrieving values.</description>
+                       <advanced>true</advanced>
+               </parameter>
+               <parameter name="commandExtension" type="text">
+                       <label>Command URL Extension</label>
+                       <description>This value is added to the base URL configured in the thing for sending values.</description>
+                       <advanced>true</advanced>
+               </parameter>
+               <parameter name="stateTransformation" type="text">
+                       <label>State Transformation</label>
+                       <description>Transformation pattern used when receiving values.</description>
+               </parameter>
+               <parameter name="commandTransformation" type="text">
+                       <label>Command Transformation</label>
+                       <description>Transformation pattern used when sending values.</description>
+               </parameter>
+               <parameter name="onValue" type="text">
+                       <label>On Value</label>
+                       <description>The value that represents ON</description>
+               </parameter>
+               <parameter name="offValue" type="text">
+                       <label>Off Value</label>
+                       <description>The value that represents OFF</description>
+               </parameter>
+               <parameter name="increaseValue" type="text">
+                       <label>Increase Value</label>
+                       <description>The value that represents INCREASE</description>
+               </parameter>
+               <parameter name="decreaseValue" type="text">
+                       <label>Decrease Value</label>
+                       <description>The value that represents DECREASE</description>
+               </parameter>
+               <parameter name="step" type="text">
+                       <label>Increase/Decrease Step</label>
+                       <description>The value by which the current brightness is increased/decreased if the corresponding command is
+                               received</description>
+                       <default>1</default>
+               </parameter>
+               <parameter name="colorMode" type="text">
+                       <label>Color Mode</label>
+                       <description>Color mode for parsing incoming and sending outgoing values</description>
+                       <options>
+                               <option value="HSB">HSB</option>
+                               <option value="RGB">RGB</option>
+                       </options>
+                       <limitToOptions>true</limitToOptions>
+                       <default>RGB</default>
+               </parameter>
+               <parameter name="mode" type="text">
+                       <label>Read/Write Mode</label>
+                       <options>
+                               <option value="READWRITE">Read/Write</option>
+                               <option value="READONLY">Read Only</option>
+                               <option value="WRITEONLY">Write Only</option>
+                       </options>
+                       <limitToOptions>true</limitToOptions>
+                       <advanced>true</advanced>
+                       <default>READWRITE</default>
+               </parameter>
+       </config-description>
+
+       <config-description uri="channel-type:http:channel-config-contact">
+               <parameter name="stateExtension" type="text">
+                       <label>State URL Extension</label>
+                       <description>This value is added to the base URL configured in the thing for retrieving values.</description>
+                       <advanced>true</advanced>
+               </parameter>
+               <parameter name="commandExtension" type="text">
+                       <label>Command URL Extension</label>
+                       <description>This value is added to the base URL configured in the thing for sending values.</description>
+                       <advanced>true</advanced>
+               </parameter>
+               <parameter name="stateTransformation" type="text">
+                       <label>State Transformation</label>
+                       <description>Transformation pattern used when receiving values.</description>
+               </parameter>
+               <parameter name="commandTransformation" type="text">
+                       <label>Command Transformation</label>
+                       <description>Transformation pattern used when sending values.</description>
+               </parameter>
+               <parameter name="openValue" type="text" required="true">
+                       <label>Open Value</label>
+                       <description>The value that represents OPEN</description>
+               </parameter>
+               <parameter name="closedValue" type="text" required="true">
+                       <label>Closed Value</label>
+                       <description>The value that represents CLOSED</description>
+               </parameter>
+               <parameter name="mode" type="text">
+                       <label>Read/Write Mode</label>
+                       <options>
+                               <option value="READWRITE">Read/Write</option>
+                               <option value="READONLY">Read Only</option>
+                               <option value="WRITEONLY">Write Only</option>
+                       </options>
+                       <limitToOptions>true</limitToOptions>
+                       <advanced>true</advanced>
+                       <default>READWRITE</default>
+               </parameter>
+       </config-description>
+
+       <config-description uri="channel-type:http:channel-config-dimmer">
+               <parameter name="stateExtension" type="text">
+                       <label>State URL Extension</label>
+                       <description>This value is added to the base URL configured in the thing for retrieving values.</description>
+                       <advanced>true</advanced>
+               </parameter>
+               <parameter name="commandExtension" type="text">
+                       <label>Command URL Extension</label>
+                       <description>This value is added to the base URL configured in the thing for sending values.</description>
+                       <advanced>true</advanced>
+               </parameter>
+               <parameter name="stateTransformation" type="text">
+                       <label>State Transformation</label>
+                       <description>Transformation pattern used when receiving values.</description>
+               </parameter>
+               <parameter name="commandTransformation" type="text">
+                       <label>Command Transformation</label>
+                       <description>Transformation pattern used when sending values.</description>
+               </parameter>
+               <parameter name="onValue" type="text">
+                       <label>On Value</label>
+                       <description>The value that represents ON</description>
+               </parameter>
+               <parameter name="offValue" type="text">
+                       <label>Off Value</label>
+                       <description>The value that represents OFF</description>
+               </parameter>
+               <parameter name="increaseValue" type="text">
+                       <label>Increase Value</label>
+                       <description>The value that represents INCREASE</description>
+               </parameter>
+               <parameter name="decreaseValue" type="text">
+                       <label>Decrease Value</label>
+                       <description>The value that represents DECREASE</description>
+               </parameter>
+               <parameter name="step" type="text">
+                       <label>Increase/Decrease Step</label>
+                       <description>The value by which the current brightness is increased/decreased if the corresponding command is
+                               received</description>
+                       <default>1</default>
+               </parameter>
+               <parameter name="mode" type="text">
+                       <label>Read/Write Mode</label>
+                       <options>
+                               <option value="READWRITE">Read/Write</option>
+                               <option value="READONLY">Read Only</option>
+                               <option value="WRITEONLY">Write Only</option>
+                       </options>
+                       <limitToOptions>true</limitToOptions>
+                       <advanced>true</advanced>
+                       <default>READWRITE</default>
+               </parameter>
+       </config-description>
+
+       <config-description uri="channel-type:http:channel-config-image">
+               <parameter name="stateExtension" type="text">
+                       <label>State URL Extension</label>
+                       <description>This value is added to the base URL configured in the thing for retrieving values.</description>
+                       <advanced>true</advanced>
+               </parameter>
+       </config-description>
+
+       <config-description uri="channel-type:http:channel-config-player">
+               <parameter name="stateExtension" type="text">
+                       <label>State URL Extension</label>
+                       <description>This value is added to the base URL configured in the thing for retrieving values.</description>
+                       <advanced>true</advanced>
+               </parameter>
+               <parameter name="commandExtension" type="text">
+                       <label>Command URL Extension</label>
+                       <description>This value is added to the base URL configured in the thing for sending values.</description>
+                       <advanced>true</advanced>
+               </parameter>
+               <parameter name="stateTransformation" type="text">
+                       <label>State Transformation</label>
+                       <description>Transformation pattern used when receiving values.</description>
+               </parameter>
+               <parameter name="commandTransformation" type="text">
+                       <label>Command Transformation</label>
+                       <description>Transformation pattern used when sending values.</description>
+               </parameter>
+
+               <parameter name="playValue" type="text">
+                       <label>Play Value</label>
+                       <description>The value that represents PLAY</description>
+               </parameter>
+               <parameter name="pauseValue" type="text">
+                       <label>Pause Value</label>
+                       <description>The value that represents PAUSE</description>
+               </parameter>
+               <parameter name="nextValue" type="text">
+                       <label>Next Value</label>
+                       <description>The value that represents NEXT</description>
+               </parameter>
+               <parameter name="previousValue" type="text">
+                       <label>Previous Value</label>
+                       <description>The value that represents PREVIOUS</description>
+               </parameter>
+               <parameter name="rewindValue" type="text">
+                       <label>Rewind Value</label>
+                       <description>The value that represents REWIND</description>
+               </parameter>
+               <parameter name="fastforwardValue" type="text">
+                       <label>Fast Forward Value</label>
+                       <description>The value that represents FASTFORWARD</description>
+               </parameter>
+               <parameter name="mode" type="text">
+                       <label>Read/Write Mode</label>
+                       <options>
+                               <option value="READWRITE">Read/Write</option>
+                               <option value="READONLY">Read Only</option>
+                               <option value="WRITEONLY">Write Only</option>
+                       </options>
+                       <limitToOptions>true</limitToOptions>
+                       <advanced>true</advanced>
+                       <default>READWRITE</default>
+               </parameter>
+       </config-description>
+
+       <config-description uri="channel-type:http:channel-config-rollershutter">
+               <parameter name="stateExtension" type="text">
+                       <label>State URL Extension</label>
+                       <description>This value is added to the base URL configured in the thing for retrieving values.</description>
+                       <advanced>true</advanced>
+               </parameter>
+               <parameter name="commandExtension" type="text">
+                       <label>Command URL Extension</label>
+                       <description>This value is added to the base URL configured in the thing for sending values.</description>
+                       <advanced>true</advanced>
+               </parameter>
+               <parameter name="stateTransformation" type="text">
+                       <label>State Transformation</label>
+                       <description>Transformation pattern used when receiving values.</description>
+               </parameter>
+               <parameter name="commandTransformation" type="text">
+                       <label>Command Transformation</label>
+                       <description>Transformation pattern used when sending values.</description>
+               </parameter>
+               <parameter name="upValue" type="text">
+                       <label>Up Value</label>
+                       <description>The value that represents UP</description>
+               </parameter>
+               <parameter name="downValue" type="text">
+                       <label>Down Value</label>
+                       <description>The value that represents DOWN</description>
+               </parameter>
+               <parameter name="stopValue" type="text">
+                       <label>Stop Value</label>
+                       <description>The value that represents STOP</description>
+               </parameter>
+               <parameter name="moveValue" type="text">
+                       <label>Move Value</label>
+                       <description>The value that represents MOVE</description>
+               </parameter>
+               <parameter name="mode" type="text">
+                       <label>Read/Write Mode</label>
+                       <options>
+                               <option value="READWRITE">Read/Write</option>
+                               <option value="READONLY">Read Only</option>
+                               <option value="WRITEONLY">Write Only</option>
+                       </options>
+                       <limitToOptions>true</limitToOptions>
+                       <advanced>true</advanced>
+                       <default>READWRITE</default>
+               </parameter>
+       </config-description>
+
+       <config-description uri="channel-type:http:channel-config-switch">
+               <parameter name="stateExtension" type="text">
+                       <label>State URL Extension</label>
+                       <description>This value is added to the base URL configured in the thing for retrieving values.</description>
+                       <advanced>true</advanced>
+               </parameter>
+               <parameter name="commandExtension" type="text">
+                       <label>Command URL Extension</label>
+                       <description>This value is added to the base URL configured in the thing for sending values.</description>
+                       <advanced>true</advanced>
+               </parameter>
+               <parameter name="stateTransformation" type="text">
+                       <label>State Transformation</label>
+                       <description>Transformation pattern used when receiving values.</description>
+               </parameter>
+               <parameter name="commandTransformation" type="text">
+                       <label>Command Transformation</label>
+                       <description>Transformation pattern used when sending values.</description>
+               </parameter>
+               <parameter name="onValue" type="text" required="true">
+                       <label>On Value</label>
+                       <description>The value that represents ON</description>
+               </parameter>
+               <parameter name="offValue" type="text" required="true">
+                       <label>Off Value</label>
+                       <description>The value that represents OFF</description>
+               </parameter>
+               <parameter name="mode" type="text">
+                       <label>Read/Write Mode</label>
+                       <options>
+                               <option value="READWRITE">Read/Write</option>
+                               <option value="READONLY">Read Only</option>
+                               <option value="WRITEONLY">Write Only</option>
+                       </options>
+                       <limitToOptions>true</limitToOptions>
+                       <advanced>true</advanced>
+                       <default>READWRITE</default>
+               </parameter>
+       </config-description>
+
+</config-description:config-descriptions>
diff --git a/bundles/org.openhab.binding.http/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.http/src/main/resources/OH-INF/thing/thing-types.xml
new file mode 100644 (file)
index 0000000..060a779
--- /dev/null
@@ -0,0 +1,158 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="http"
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
+       xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
+
+       <thing-type id="url"
+               extensible="color,contact,datetime,dimmer,image,location,number,rollershutter,string,switch">
+               <label>HTTP URL Thing</label>
+               <description>Represents a base URL and all associated requests.</description>
+
+               <config-description>
+                       <parameter name="baseURL" type="text" required="true">
+                               <label>Base URL</label>
+                               <description>The URL set here can be extended in the channel configuration.</description>
+                               <context>url</context>
+                       </parameter>
+                       <parameter name="refresh" type="integer" unit="s" min="1">
+                               <label>Refresh Time</label>
+                               <description>Time between two refreshes of all channels</description>
+                               <default>30</default>
+                       </parameter>
+                       <parameter name="timeout" type="integer" unit="ms" min="0">
+                               <label>Timeout</label>
+                               <description>The timeout in ms for each request</description>
+                               <default>3000</default>
+                       </parameter>
+                       <parameter name="username" type="text">
+                               <label>Username</label>
+                               <description>Basic Authentication username</description>
+                               <advanced>true</advanced>
+                       </parameter>
+                       <parameter name="password" type="text">
+                               <label>Password</label>
+                               <description>Basic Authentication password</description>
+                               <context>password</context>
+                               <advanced>true</advanced>
+                       </parameter>
+                       <parameter name="authMode" type="text">
+                               <label>Authentication Mode</label>
+                               <options>
+                                       <option value="BASIC">Basic Authentication</option>
+                                       <option value="DIGEST">Digest Authentication</option>
+                               </options>
+                               <default>BASIC</default>
+                               <limitToOptions>true</limitToOptions>
+                               <advanced>true</advanced>
+                       </parameter>
+                       <parameter name="commandMethod" type="text">
+                               <label>Command Method</label>
+                               <description>HTTP method (GET,POST, PUT) for sending commands.</description>
+                               <options>
+                                       <option value="GET">GET</option>
+                                       <option value="POST">POST</option>
+                                       <option value="PUT">PUT</option>
+                               </options>
+                               <limitToOptions>true</limitToOptions>
+                               <default>GET</default>
+                               <advanced>true</advanced>
+                       </parameter>
+                       <parameter name="contentType" type="text">
+                               <label>Content Type</label>
+                               <description>The MIME content type. Only used for `POST` and `PUT`.</description>
+                               <options>
+                                       <option value="application/json">application/json</option>
+                                       <option value="application/xml">application/xml</option>
+                                       <option value="text/html">text/html</option>
+                                       <option value="text/plain">text/plain</option>
+                                       <option value="text/xml">text/xml</option>
+                               </options>
+                               <advanced>true</advanced>
+                       </parameter>
+                       <parameter name="encoding" type="text">
+                               <label>Fallback Encoding</label>
+                               <description>Fallback Encoding text received by this thing's channels.</description>
+                               <advanced>true</advanced>
+                       </parameter>
+                       <parameter name="headers" type="text" multiple="true">
+                               <label>Headers</label>
+                               <description>Additional headers send along with the request</description>
+                               <advanced>true</advanced>
+                       </parameter>
+                       <parameter name="ignoreSSLErrors" type="boolean">
+                               <label>Ignore SSL Errors</label>
+                               <description>If set to true ignores invalid SSL certificate errors. This is potentially dangerous.</description>
+                               <default>false</default>
+                               <advanced>true</advanced>
+                       </parameter>
+               </config-description>
+       </thing-type>
+
+       <channel-type id="color">
+               <item-type>Color</item-type>
+               <label>Color Channel</label>
+               <config-description-ref uri="channel-type:http:channel-config-color"/>
+       </channel-type>
+
+       <channel-type id="contact">
+               <item-type>Contact</item-type>
+               <label>Contact Channel</label>
+               <config-description-ref uri="channel-type:http:channel-config-contact"/>
+       </channel-type>
+
+       <channel-type id="datetime">
+               <item-type>DateTime</item-type>
+               <label>DateTime Channel</label>
+               <config-description-ref uri="channel-type:http:channel-config"/>
+       </channel-type>
+
+       <channel-type id="dimmer">
+               <item-type>Dimmer</item-type>
+               <label>Dimmer Channel</label>
+               <config-description-ref uri="channel-type:http:channel-config-dimmer"/>
+       </channel-type>
+
+       <channel-type id="image">
+               <item-type>Image</item-type>
+               <label>Image Channel</label>
+               <config-description-ref uri="channel-type:http:channel-config-image"/>
+       </channel-type>
+
+       <channel-type id="location">
+               <item-type>Location</item-type>
+               <label>Location Channel</label>
+               <config-description-ref uri="channel-type:http:channel-config"/>
+       </channel-type>
+
+       <channel-type id="number">
+               <item-type>Number</item-type>
+               <label>Number Channel</label>
+               <config-description-ref uri="channel-type:http:channel-config"/>
+       </channel-type>
+
+       <channel-type id="player">
+               <item-type>Player</item-type>
+               <label>Player Channel</label>
+               <config-description-ref uri="channel-type:http:channel-config-player"/>
+       </channel-type>
+
+       <channel-type id="rollershutter">
+               <item-type>Rollershutter</item-type>
+               <label>Rollershutter Channel</label>
+               <config-description-ref uri="channel-type:http:channel-config-rollershutter"/>
+       </channel-type>
+
+       <channel-type id="string">
+               <item-type>String</item-type>
+               <label>String Channel</label>
+               <config-description-ref uri="channel-type:http:channel-config"/>
+       </channel-type>
+
+       <channel-type id="switch">
+               <item-type>Switch</item-type>
+               <label>Switch Channel</label>
+               <config-description-ref uri="channel-type:http:channel-config-switch"/>
+       </channel-type>
+
+</thing:thing-descriptions>
diff --git a/bundles/org.openhab.binding.http/src/test/java/org/openhab/binding/http/internal/converter/ConverterTest.java b/bundles/org.openhab.binding.http/src/test/java/org/openhab/binding/http/internal/converter/ConverterTest.java
new file mode 100644 (file)
index 0000000..3ee43d4
--- /dev/null
@@ -0,0 +1,68 @@
+/**
+ * Copyright (c) 2010-2020 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.http.internal.converter;
+
+import java.util.function.Function;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.openhab.binding.http.internal.config.HttpChannelConfig;
+import org.openhab.binding.http.internal.transform.NoOpValueTransformation;
+import org.openhab.core.library.types.DecimalType;
+import org.openhab.core.library.types.PointType;
+import org.openhab.core.library.types.StringType;
+import org.openhab.core.types.Command;
+import org.openhab.core.types.State;
+
+/**
+ * The {@link ConverterTest} is a test class for state converters
+ *
+ * @author Jan N. Klug - Initial contribution
+ */
+@NonNullByDefault
+public class ConverterTest {
+
+    @Test
+    public void stringTypeConverter() {
+        GenericItemConverter converter = createConverter(StringType::new);
+        Assertions.assertEquals(new StringType("Test"), converter.toState("Test"));
+    }
+
+    @Test
+    public void decimalTypeConverter() {
+        GenericItemConverter converter = createConverter(DecimalType::new);
+        Assertions.assertEquals(new DecimalType(15.6), converter.toState("15.6"));
+    }
+
+    @Test
+    public void pointTypeConverter() {
+        GenericItemConverter converter = createConverter(PointType::new);
+        Assertions.assertEquals(new PointType(new DecimalType(51.1), new DecimalType(7.2), new DecimalType(100)),
+                converter.toState("51.1, 7.2, 100"));
+    }
+
+    private void sendHttpValue(String value) {
+    }
+
+    private void updateState(State state) {
+    }
+
+    public void postCommand(Command command) {
+    }
+
+    public GenericItemConverter createConverter(Function<String, State> fcn) {
+        return new GenericItemConverter(fcn, this::updateState, this::postCommand, this::sendHttpValue,
+                NoOpValueTransformation.getInstance(), NoOpValueTransformation.getInstance(), new HttpChannelConfig());
+    }
+}
index 8c6c0436d5cab20dba518253ffa4a03237ba6b03..678876f3984f6aed38c5c0080f200e47b0c9e93e 100644 (file)
     <module>org.openhab.binding.heos</module>
     <module>org.openhab.binding.homematic</module>
     <module>org.openhab.binding.hpprinter</module>
+    <module>org.openhab.binding.http</module>
     <module>org.openhab.binding.hue</module>
     <module>org.openhab.binding.hydrawise</module>
     <module>org.openhab.binding.hyperion</module>