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