diff --git a/CODEOWNERS b/CODEOWNERS
index e249124e5..171df6890 100644
--- a/CODEOWNERS
+++ b/CODEOWNERS
@@ -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
diff --git a/bom/openhab-addons/pom.xml b/bom/openhab-addons/pom.xml
index 23feb8be9..25de4404b 100644
--- a/bom/openhab-addons/pom.xml
+++ b/bom/openhab-addons/pom.xml
@@ -436,6 +436,11 @@
org.openhab.binding.hpprinter
${project.version}
+
+ org.openhab.addons.bundles
+ org.openhab.binding.http
+ ${project.version}
+
org.openhab.addons.bundles
org.openhab.binding.hue
diff --git a/bundles/org.openhab.binding.http/NOTICE b/bundles/org.openhab.binding.http/NOTICE
new file mode 100644
index 000000000..38d625e34
--- /dev/null
+++ b/bundles/org.openhab.binding.http/NOTICE
@@ -0,0 +1,13 @@
+This content is produced and maintained by the openHAB project.
+
+* Project home: https://www.openhab.org
+
+== Declared Project Licenses
+
+This program and the accompanying materials are made available under the terms
+of the Eclipse Public License 2.0 which is available at
+https://www.eclipse.org/legal/epl-2.0/.
+
+== Source Code
+
+https://github.com/openhab/openhab-addons
diff --git a/bundles/org.openhab.binding.http/README.md b/bundles/org.openhab.binding.http/README.md
new file mode 100644
index 000000000..704b1287e
--- /dev/null
+++ b/bundles/org.openhab.binding.http/README.md
@@ -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` |
+| `23.2` | 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
index 000000000..92a410b8b
--- /dev/null
+++ b/bundles/org.openhab.binding.http/pom.xml
@@ -0,0 +1,17 @@
+
+
+
+ 4.0.0
+
+
+ org.openhab.addons.bundles
+ org.openhab.addons.reactor.bundles
+ 3.0.0-SNAPSHOT
+
+
+ org.openhab.binding.http
+
+ openHAB Add-ons :: Bundles :: HTTP Binding
+
+
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
index 000000000..662c97dd2
--- /dev/null
+++ b/bundles/org.openhab.binding.http/src/main/feature/feature.xml
@@ -0,0 +1,9 @@
+
+
+ mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features
+
+
+ openhab-runtime-base
+ mvn:org.openhab.addons.bundles/org.openhab.binding.http/${project.version}
+
+
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
index 000000000..eb2b0592d
--- /dev/null
+++ b/bundles/org.openhab.binding.http/src/main/java/org/openhab/binding/http/internal/HttpBindingConstants.java
@@ -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
index 000000000..1751bf8c1
--- /dev/null
+++ b/bundles/org.openhab.binding.http/src/main/java/org/openhab/binding/http/internal/HttpClientProvider.java
@@ -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
index 000000000..d1ace1921
--- /dev/null
+++ b/bundles/org.openhab.binding.http/src/main/java/org/openhab/binding/http/internal/HttpDynamicStateDescriptionProvider.java
@@ -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 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
index 000000000..7b1444156
--- /dev/null
+++ b/bundles/org.openhab.binding.http/src/main/java/org/openhab/binding/http/internal/HttpHandlerFactory.java
@@ -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 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
index 000000000..173baf41e
--- /dev/null
+++ b/bundles/org.openhab.binding.http/src/main/java/org/openhab/binding/http/internal/HttpThingHandler.java
@@ -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 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 urlHandlers = new HashMap<>();
+ private final Map channels = new HashMap<>();
+ private final Map 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 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
index 000000000..b29b07ddc
--- /dev/null
+++ b/bundles/org.openhab.binding.http/src/main/java/org/openhab/binding/http/internal/Util.java
@@ -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
index 000000000..c78c21ff5
--- /dev/null
+++ b/bundles/org.openhab.binding.http/src/main/java/org/openhab/binding/http/internal/config/HttpAuthMode.java
@@ -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
index 000000000..24a0aeb6a
--- /dev/null
+++ b/bundles/org.openhab.binding.http/src/main/java/org/openhab/binding/http/internal/config/HttpChannelConfig.java
@@ -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 stringStateMap = new HashMap<>();
+ private final Map 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
index 000000000..d5416ecfb
--- /dev/null
+++ b/bundles/org.openhab.binding.http/src/main/java/org/openhab/binding/http/internal/config/HttpChannelMode.java
@@ -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
index 000000000..a6cb7941c
--- /dev/null
+++ b/bundles/org.openhab.binding.http/src/main/java/org/openhab/binding/http/internal/config/HttpThingConfig.java
@@ -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 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
index 000000000..7a22081b2
--- /dev/null
+++ b/bundles/org.openhab.binding.http/src/main/java/org/openhab/binding/http/internal/converter/AbstractTransformingItemConverter.java
@@ -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 updateState;
+ private final Consumer postCommand;
+ private final @Nullable Consumer sendHttpValue;
+ private final ValueTransformation stateTransformations;
+ private final ValueTransformation commandTransformations;
+
+ protected HttpChannelConfig channelConfig;
+
+ public AbstractTransformingItemConverter(Consumer updateState, Consumer postCommand,
+ @Nullable Consumer 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 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 updateState, Consumer postCommand,
+ @Nullable Consumer 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
index 000000000..8a3b82bba
--- /dev/null
+++ b/bundles/org.openhab.binding.http/src/main/java/org/openhab/binding/http/internal/converter/ColorItemConverter.java
@@ -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 updateState, Consumer postCommand,
+ @Nullable Consumer 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
index 000000000..7464202bc
--- /dev/null
+++ b/bundles/org.openhab.binding.http/src/main/java/org/openhab/binding/http/internal/converter/DimmerItemConverter.java
@@ -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 updateState, Consumer postCommand,
+ @Nullable Consumer 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
index 000000000..e363e8843
--- /dev/null
+++ b/bundles/org.openhab.binding.http/src/main/java/org/openhab/binding/http/internal/converter/FixedValueMappingItemConverter.java
@@ -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 updateState, Consumer postCommand,
+ @Nullable Consumer 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
index 000000000..6828be7b6
--- /dev/null
+++ b/bundles/org.openhab.binding.http/src/main/java/org/openhab/binding/http/internal/converter/GenericItemConverter.java
@@ -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 toState;
+
+ public GenericItemConverter(Function toState, Consumer updateState,
+ Consumer postCommand, @Nullable Consumer 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
index 000000000..455ccecbc
--- /dev/null
+++ b/bundles/org.openhab.binding.http/src/main/java/org/openhab/binding/http/internal/converter/ImageItemConverter.java
@@ -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 updateState;
+
+ public ImageItemConverter(Consumer 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
index 000000000..0ac75c8dd
--- /dev/null
+++ b/bundles/org.openhab.binding.http/src/main/java/org/openhab/binding/http/internal/converter/ItemValueConverter.java
@@ -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
index 000000000..9a7764bb1
--- /dev/null
+++ b/bundles/org.openhab.binding.http/src/main/java/org/openhab/binding/http/internal/converter/PlayerItemConverter.java
@@ -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 updateState, Consumer postCommand,
+ @Nullable Consumer 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
index 000000000..b77568751
--- /dev/null
+++ b/bundles/org.openhab.binding.http/src/main/java/org/openhab/binding/http/internal/converter/RollershutterItemConverter.java
@@ -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 updateState, Consumer postCommand,
+ @Nullable Consumer 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
index 000000000..72d19b99f
--- /dev/null
+++ b/bundles/org.openhab.binding.http/src/main/java/org/openhab/binding/http/internal/http/Content.java
@@ -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
index 000000000..8fc19cfa6
--- /dev/null
+++ b/bundles/org.openhab.binding.http/src/main/java/org/openhab/binding/http/internal/http/HttpAuthException.java
@@ -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
index 000000000..09348ed14
--- /dev/null
+++ b/bundles/org.openhab.binding.http/src/main/java/org/openhab/binding/http/internal/http/HttpResponseListener.java
@@ -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
index 000000000..8a8d4ef51
--- /dev/null
+++ b/bundles/org.openhab.binding.http/src/main/java/org/openhab/binding/http/internal/http/RefreshingUrlCache.java
@@ -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> consumers = ConcurrentHashMap.newKeySet();
+ private final List 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 consumer) {
+ consumers.add(consumer);
+ }
+
+ public Optional 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 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
index 000000000..a163c9fdc
--- /dev/null
+++ b/bundles/org.openhab.binding.http/src/main/java/org/openhab/binding/http/internal/transform/CascadedValueTransformationImpl.java
@@ -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 transformations;
+
+ public CascadedValueTransformationImpl(String transformationString,
+ Function transformationServiceSupplier) {
+ transformations = Arrays.stream(transformationString.split("∩")).filter(s -> !s.isEmpty())
+ .map(transformation -> new SingleValueTransformation(transformation, transformationServiceSupplier))
+ .collect(Collectors.toList());
+ }
+
+ @Override
+ public Optional apply(String value) {
+ Optional 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
index 000000000..ff1bf5afb
--- /dev/null
+++ b/bundles/org.openhab.binding.http/src/main/java/org/openhab/binding/http/internal/transform/NoOpValueTransformation.java
@@ -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 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
index 000000000..fa98e681f
--- /dev/null
+++ b/bundles/org.openhab.binding.http/src/main/java/org/openhab/binding/http/internal/transform/SingleValueTransformation.java
@@ -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 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 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 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
index 000000000..95f99db40
--- /dev/null
+++ b/bundles/org.openhab.binding.http/src/main/java/org/openhab/binding/http/internal/transform/ValueTransformation.java
@@ -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 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
index 000000000..12dd8726e
--- /dev/null
+++ b/bundles/org.openhab.binding.http/src/main/java/org/openhab/binding/http/internal/transform/ValueTransformationProvider.java
@@ -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
index 000000000..714ff784d
--- /dev/null
+++ b/bundles/org.openhab.binding.http/src/main/resources/OH-INF/binding/binding.xml
@@ -0,0 +1,10 @@
+
+
+
+ HTTP Binding
+ This is the binding for retrieving and processing HTTP resources.
+ Jan N. Klug
+
+
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
index 000000000..51a51162d
--- /dev/null
+++ b/bundles/org.openhab.binding.http/src/main/resources/OH-INF/config/config.xml
@@ -0,0 +1,350 @@
+
+
+
+
+
+
+ This value is added to the base URL configured in the thing for retrieving values.
+ true
+
+
+
+ This value is added to the base URL configured in the thing for sending values.
+ true
+
+
+
+ Transformation pattern used when receiving values.
+
+
+
+ Transformation pattern used when sending values.
+
+
+
+
+
+
+
+
+ true
+ true
+ READWRITE
+
+
+
+
+
+
+ This value is added to the base URL configured in the thing for retrieving values.
+ true
+
+
+
+ This value is added to the base URL configured in the thing for sending values.
+ true
+
+
+
+ Transformation pattern used when receiving values.
+
+
+
+ Transformation pattern used when sending values.
+
+
+
+ The value that represents ON
+
+
+
+ The value that represents OFF
+
+
+
+ The value that represents INCREASE
+
+
+
+ The value that represents DECREASE
+
+
+
+ The value by which the current brightness is increased/decreased if the corresponding command is
+ received
+ 1
+
+
+
+ Color mode for parsing incoming and sending outgoing values
+
+
+
+
+ true
+ RGB
+
+
+
+
+
+
+
+
+ true
+ true
+ READWRITE
+
+
+
+
+
+
+ This value is added to the base URL configured in the thing for retrieving values.
+ true
+
+
+
+ This value is added to the base URL configured in the thing for sending values.
+ true
+
+
+
+ Transformation pattern used when receiving values.
+
+
+
+ Transformation pattern used when sending values.
+
+
+
+ The value that represents OPEN
+
+
+
+ The value that represents CLOSED
+
+
+
+
+
+
+
+
+ true
+ true
+ READWRITE
+
+
+
+
+
+
+ This value is added to the base URL configured in the thing for retrieving values.
+ true
+
+
+
+ This value is added to the base URL configured in the thing for sending values.
+ true
+
+
+
+ Transformation pattern used when receiving values.
+
+
+
+ Transformation pattern used when sending values.
+
+
+
+ The value that represents ON
+
+
+
+ The value that represents OFF
+
+
+
+ The value that represents INCREASE
+
+
+
+ The value that represents DECREASE
+
+
+
+ The value by which the current brightness is increased/decreased if the corresponding command is
+ received
+ 1
+
+
+
+
+
+
+
+
+ true
+ true
+ READWRITE
+
+
+
+
+
+
+ This value is added to the base URL configured in the thing for retrieving values.
+ true
+
+
+
+
+
+
+ This value is added to the base URL configured in the thing for retrieving values.
+ true
+
+
+
+ This value is added to the base URL configured in the thing for sending values.
+ true
+
+
+
+ Transformation pattern used when receiving values.
+
+
+
+ Transformation pattern used when sending values.
+
+
+
+
+ The value that represents PLAY
+
+
+
+ The value that represents PAUSE
+
+
+
+ The value that represents NEXT
+
+
+
+ The value that represents PREVIOUS
+
+
+
+ The value that represents REWIND
+
+
+
+ The value that represents FASTFORWARD
+
+
+
+
+
+
+
+
+ true
+ true
+ READWRITE
+
+
+
+
+
+
+ This value is added to the base URL configured in the thing for retrieving values.
+ true
+
+
+
+ This value is added to the base URL configured in the thing for sending values.
+ true
+
+
+
+ Transformation pattern used when receiving values.
+
+
+
+ Transformation pattern used when sending values.
+
+
+
+ The value that represents UP
+
+
+
+ The value that represents DOWN
+
+
+
+ The value that represents STOP
+
+
+
+ The value that represents MOVE
+
+
+
+
+
+
+
+
+ true
+ true
+ READWRITE
+
+
+
+
+
+
+ This value is added to the base URL configured in the thing for retrieving values.
+ true
+
+
+
+ This value is added to the base URL configured in the thing for sending values.
+ true
+
+
+
+ Transformation pattern used when receiving values.
+
+
+
+ Transformation pattern used when sending values.
+
+
+
+ The value that represents ON
+
+
+
+ The value that represents OFF
+
+
+
+
+
+
+
+
+ true
+ true
+ READWRITE
+
+
+
+
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
index 000000000..060a77929
--- /dev/null
+++ b/bundles/org.openhab.binding.http/src/main/resources/OH-INF/thing/thing-types.xml
@@ -0,0 +1,158 @@
+
+
+
+
+
+ Represents a base URL and all associated requests.
+
+
+
+
+ The URL set here can be extended in the channel configuration.
+ url
+
+
+
+ Time between two refreshes of all channels
+ 30
+
+
+
+ The timeout in ms for each request
+ 3000
+
+
+
+ Basic Authentication username
+ true
+
+
+
+ Basic Authentication password
+ password
+ true
+
+
+
+
+
+
+
+ BASIC
+ true
+ true
+
+
+
+ HTTP method (GET,POST, PUT) for sending commands.
+
+
+
+
+
+ true
+ GET
+ true
+
+
+
+ The MIME content type. Only used for `POST` and `PUT`.
+
+
+
+
+
+
+
+ true
+
+
+
+ Fallback Encoding text received by this thing's channels.
+ true
+
+
+
+ Additional headers send along with the request
+ true
+
+
+
+ If set to true ignores invalid SSL certificate errors. This is potentially dangerous.
+ false
+ true
+
+
+
+
+
+ Color
+
+
+
+
+
+ Contact
+
+
+
+
+
+ DateTime
+
+
+
+
+
+ Dimmer
+
+
+
+
+
+ Image
+
+
+
+
+
+ Location
+
+
+
+
+
+ Number
+
+
+
+
+
+ Player
+
+
+
+
+
+ Rollershutter
+
+
+
+
+
+ String
+
+
+
+
+
+ Switch
+
+
+
+
+
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
index 000000000..3ee43d4aa
--- /dev/null
+++ b/bundles/org.openhab.binding.http/src/test/java/org/openhab/binding/http/internal/converter/ConverterTest.java
@@ -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 fcn) {
+ return new GenericItemConverter(fcn, this::updateState, this::postCommand, this::sendHttpValue,
+ NoOpValueTransformation.getInstance(), NoOpValueTransformation.getInstance(), new HttpChannelConfig());
+ }
+}
diff --git a/bundles/pom.xml b/bundles/pom.xml
index 8c6c0436d..678876f39 100644
--- a/bundles/pom.xml
+++ b/bundles/pom.xml
@@ -121,6 +121,7 @@
org.openhab.binding.heos
org.openhab.binding.homematic
org.openhab.binding.hpprinter
+ org.openhab.binding.http
org.openhab.binding.hue
org.openhab.binding.hydrawise
org.openhab.binding.hyperion