[http] Initial contribution (#8521)

Signed-off-by: Jan N. Klug <jan.n.klug@rub.de>
This commit is contained in:
J-N-K 2020-11-09 17:53:44 +01:00 committed by GitHub
parent 3a4eaae1e9
commit 8de5652ed1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
39 changed files with 3026 additions and 0 deletions

View File

@ -89,6 +89,7 @@
/bundles/org.openhab.binding.heos/ @Wire82
/bundles/org.openhab.binding.homematic/ @FStolte @gerrieg @mdicke2s
/bundles/org.openhab.binding.hpprinter/ @cossey
/bundles/org.openhab.binding.http/ @J-N-K
/bundles/org.openhab.binding.hue/ @cweitkamp
/bundles/org.openhab.binding.hydrawise/ @digitaldan
/bundles/org.openhab.binding.hyperion/ @tavalin

View File

@ -436,6 +436,11 @@
<artifactId>org.openhab.binding.hpprinter</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.openhab.addons.bundles</groupId>
<artifactId>org.openhab.binding.http</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.openhab.addons.bundles</groupId>
<artifactId>org.openhab.binding.hue</artifactId>

View File

@ -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

View File

@ -0,0 +1,146 @@
# HTTP Binding
This binding allows using HTTP to bring external data into openHAB or execute HTTP requests on commands.
## Supported Things
Only one thing named `url` is available.
It can be extended with different channels.
## Thing Configuration
| parameter | optional | default | description |
|-------------------|----------|---------|-------------|
| `baseURL` | no | - | The base URL for this thing. Can be extended in channel-configuration. |
| `refresh` | no | 30 | Time in seconds between two refresh calls for the channels of this thing. |
| `timeout` | no | 3000 | Timeout for HTTP requests in ms. |
| `username` | yes | - | Username for authentication (advanced parameter). |
| `password` | yes | - | Password for authentication (advanced parameter). |
| `authMode` | no | BASIC | Authentication mode, `BASIC` or `DIGEST` (advanced parameter). |
| `commandMethod` | no | GET | Method used for sending commands `GET`, `PUT`, `POST`. |
| `contentType` | yes | - | MIME content-type of the command requests. Only used for `PUT` and `POST`. |
| `encoding` | yes | - | Encoding to be used if no encoding is found in responses (advanced parameter). |
| `headers` | yes | - | Additional headers that are sent along with the request. Format is "header=value".|
| `ignoreSSLErrors` | no | false | If set to true ignores invalid SSL certificate errors. This is potentially dangerous.|
*Note:* optional "no" means that you have to configure a value unless a default is provided and you are ok with that setting.
## Channels
Each item type has its own channel-type.
Depending on the channel-type, channels have different configuration options.
All channel-types (except `image`) have `stateExtension`, `commandExtension`, `stateTransformation`, `commandTransformation` and `mode` parameters.
The `image` channel-type supports `stateExtension` only.
| parameter | optional | default | description |
|-------------------------|----------|-------------|-------------|
| `stateExtension` | yes | - | Appended to the `baseURL` for requesting states. |
| `commandExtension` | yes | - | Appended to the `baseURL` for sending commands. If empty, same as `stateExtension`. |
| `stateTransformation ` | yes | - | One or more transformation applied to received values before updating channel. |
| `commandTransformation` | yes | - | One or more transformation applied to channel value before sending to a remote. |
| `mode` | no | `READWRITE` | Mode this channel is allowed to operate. `READ` means receive state, `WRITE` means send commands. |
Transformations need to be specified in the same format as
Some channels have additional parameters.
When concatenating the `baseURL` and `stateExtions` or `commandExtension` the binding checks if a proper URL part separator (`/`, `&` or `?`) is present and adds a `/` if missing.
### Value Transformations (`stateTransformation`, `commandTransformation`)
Transformations can be used if the supplied value (or the required value) is different from what openHAB internal types require.
Here are a few examples to unwrap an incoming value via `stateTransformation` from a complex response:
| Received value | Tr. Service | Transformation |
|---------------------------------------------------------------------|-------------|-------------------------------------------|
| `{device: {status: { temperature: 23.2 }}}` | JSONPATH | `JSONPATH:$.device.status.temperature` |
| `<device><status><temperature>23.2</temperature></status></device>` | XPath | `XPath:/device/status/temperature/text()` |
| `THEVALUE:23.2°C` | REGEX | `REGEX::(.*?)°` |
Transformations can be chained by separating them with the mathematical intersection character "∩".
Please note that the values will be discarded if one transformation fails (e.g. REGEX did not match).
The same mechanism works for commands (`commandTransformation`) for outgoing values.
### `color`
| parameter | optional | default | description |
|-------------------------|----------|-------------|-------------|
| `onValue` | yes | - | A special value that represents `ON` |
| `offValue` | yes | - | A special value that represents `OFF` |
| `increaseValue` | yes | - | A special value that represents `INCREASE` |
| `decreaseValue` | yes | - | A special value that represents `DECREASE` |
| `step` | no | 1 | The amount the brightness is increased/decreased on `INCREASE`/`DECREASE` |
| `colorMode` | no | RGB | Mode for color values: `RGB` or `HSB` |
All values that are not `onValue`, `offValue`, `increaseValue`, `decreaseValue` are interpreted as color value (according to the color mode) in the format `r,g,b` or `h,s,v`.
### `contact`
| parameter | optional | default | description |
|-------------------------|----------|-------------|-------------|
| `openValue` | no | - | A special value that represents `OPEN` |
| `closedValue` | no | - | A special value that represents `CLOSED` |
### `dimmer`
| parameter | optional | default | description |
|-------------------------|----------|-------------|-------------|
| `onValue` | yes | - | A special value that represents `ON` |
| `offValue` | yes | - | A special value that represents `OFF` |
| `increaseValue` | yes | - | A special value that represents `INCREASE` |
| `decreaseValue` | yes | - | A special value that represents `DECREASE` |
| `step` | no | 1 | The amount the brightness is increased/decreased on `INCREASE`/`DECREASE` |
All values that are not `onValue`, `offValue`, `increaseValue`, `decreaseValue` are interpreted as brightness 0-100% and need to be numeric only.
### `player`
| parameter | optional | default | description |
|-------------------------|----------|-------------|-------------|
| `play` | yes | - | A special value that represents `PLAY` |
| `pause` | yes | - | A special value that represents `PAUSE` |
| `next` | yes | - | A special value that represents `NEXT` |
| `previous` | yes | - | A special value that represents `PREVIOUS` |
| `fastforward` | yes | - | A special value that represents `FASTFORWARD` |
| `rewind` | yes | - | A special value that represents `REWIND` |
### `rollershutter`
| parameter | optional | default | description |
|-------------------------|----------|-------------|-------------|
| `upValue` | yes | - | A special value that represents `UP` |
| `downValue` | yes | - | A special value that represents `DOWN` |
| `stopValue` | yes | - | A special value that represents `STOP` |
| `moveValue` | yes | - | A special value that represents `MOVE` |
All values that are not `upValue`, `downValue`, `stopValue`, `moveValue` are interpreted as position 0-100% and need to be numeric only.
### `switch`
| parameter | optional | default | description |
|-------------------------|----------|-------------|-------------|
| `onValue` | no | - | A special value that represents `ON` |
| `offValue` | no | - | A special value that represents `OFF` |
**Note:** Special values need to be exact matches, i.e. no leading or trailing characters and comparison is case-sensitive.
## URL Formatting
After concatenation of the `baseURL` and the `commandExtension` or the `stateExtension` (if provided) the URL is formatted using the [java.util.Formatter](http://docs.oracle.com/javase/6/docs/api/java/util/Formatter.html).
The URL is used as format string and two parameters are added:
- the current date (referenced as `%1$`)
- the transformed command (referenced as `%2$`)
After the parameter reference the format needs to be appended.
See the link above for more information about the available format parameters (e.g. to use the string representation, you need to append `s` to the reference).
When sending an OFF command on 2020-07-06, the URL
```
http://www.domain.org/home/lights/23871/?status=%2$s&date=%1$tY-%1$tm-%1$td
```
is transformed to
```
http://www.domain.org/home/lights/23871/?status=OFF&date=2020-07-06
```

View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.openhab.addons.bundles</groupId>
<artifactId>org.openhab.addons.reactor.bundles</artifactId>
<version>3.0.0-SNAPSHOT</version>
</parent>
<artifactId>org.openhab.binding.http</artifactId>
<name>openHAB Add-ons :: Bundles :: HTTP Binding</name>
</project>

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<features name="org.openhab.binding.http-${project.version}" xmlns="http://karaf.apache.org/xmlns/features/v1.4.0">
<repository>mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features</repository>
<feature name="openhab-binding-http" description="HTTP Binding" version="${project.version}">
<feature>openhab-runtime-base</feature>
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.http/${project.version}</bundle>
</feature>
</features>

View File

@ -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");
}

View File

@ -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();
}

View File

@ -0,0 +1,78 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.http.internal;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.thing.Channel;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.ThingUID;
import org.openhab.core.thing.type.DynamicStateDescriptionProvider;
import org.openhab.core.types.StateDescription;
import org.osgi.service.component.annotations.Component;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Dynamic channel state description provider.
* Overrides the state description for the controls, which receive its configuration in the runtime.
*
* @author Jan N. Klug - Initial contribution
*/
@NonNullByDefault
@Component(service = { DynamicStateDescriptionProvider.class,
HttpDynamicStateDescriptionProvider.class }, immediate = true)
public class HttpDynamicStateDescriptionProvider implements DynamicStateDescriptionProvider {
private final Map<ChannelUID, StateDescription> descriptions = new ConcurrentHashMap<>();
private final Logger logger = LoggerFactory.getLogger(HttpDynamicStateDescriptionProvider.class);
/**
* Set a state description for a channel. This description will be used when preparing the channel state by
* the framework for presentation. A previous description, if existed, will be replaced.
*
* @param channelUID
* channel UID
* @param description
* state description for the channel
*/
public void setDescription(ChannelUID channelUID, StateDescription description) {
logger.trace("adding state description for channel {}", channelUID);
descriptions.put(channelUID, description);
}
/**
* remove all descriptions for a given thing
*
* @param thingUID the thing's UID
*/
public void removeDescriptionsForThing(ThingUID thingUID) {
logger.trace("removing state description for thing {}", thingUID);
descriptions.entrySet().removeIf(entry -> entry.getKey().getThingUID().equals(thingUID));
}
@Override
public @Nullable StateDescription getStateDescription(Channel channel,
@Nullable StateDescription originalStateDescription, @Nullable Locale locale) {
if (descriptions.containsKey(channel.getUID())) {
logger.trace("returning new stateDescription for {}", channel.getUID());
return descriptions.get(channel.getUID());
} else {
return null;
}
}
}

View File

@ -0,0 +1,120 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.http.internal;
import static org.openhab.binding.http.internal.HttpBindingConstants.*;
import java.util.Set;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.util.ssl.SslContextFactory;
import org.openhab.binding.http.internal.transform.CascadedValueTransformationImpl;
import org.openhab.binding.http.internal.transform.NoOpValueTransformation;
import org.openhab.binding.http.internal.transform.ValueTransformation;
import org.openhab.binding.http.internal.transform.ValueTransformationProvider;
import org.openhab.core.io.net.http.HttpClientFactory;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.binding.BaseThingHandlerFactory;
import org.openhab.core.thing.binding.ThingHandler;
import org.openhab.core.thing.binding.ThingHandlerFactory;
import org.openhab.core.transform.TransformationHelper;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Deactivate;
import org.osgi.service.component.annotations.Reference;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link HttpHandlerFactory} is responsible for creating things and thing
* handlers.
*
* @author Jan N. Klug - Initial contribution
*/
@NonNullByDefault
@Component(configurationPid = "binding.http", service = ThingHandlerFactory.class)
public class HttpHandlerFactory extends BaseThingHandlerFactory
implements ValueTransformationProvider, HttpClientProvider {
private static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Set.of(THING_TYPE_URL);
private final Logger logger = LoggerFactory.getLogger(HttpHandlerFactory.class);
private final HttpClient secureClient;
private final HttpClient insecureClient;
private final HttpDynamicStateDescriptionProvider httpDynamicStateDescriptionProvider;
@Activate
public HttpHandlerFactory(@Reference HttpClientFactory httpClientFactory,
@Reference HttpDynamicStateDescriptionProvider httpDynamicStateDescriptionProvider) {
this.secureClient = new HttpClient(new SslContextFactory());
this.insecureClient = new HttpClient(new SslContextFactory(true));
try {
this.secureClient.start();
this.insecureClient.start();
} catch (Exception e) {
// catching exception is necessary due to the signature of HttpClient.start()
logger.warn("Failed to start insecure http client: {}", e.getMessage());
throw new IllegalStateException("Could not create insecure HttpClient");
}
this.httpDynamicStateDescriptionProvider = httpDynamicStateDescriptionProvider;
}
@Deactivate
public void deactivate() {
try {
secureClient.stop();
insecureClient.stop();
} catch (Exception e) {
// catching exception is necessary due to the signature of HttpClient.stop()
logger.warn("Failed to stop insecure http client: {}", e.getMessage());
}
}
@Override
public boolean supportsThingType(ThingTypeUID thingTypeUID) {
return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID);
}
@Override
protected @Nullable ThingHandler createHandler(Thing thing) {
ThingTypeUID thingTypeUID = thing.getThingTypeUID();
if (THING_TYPE_URL.equals(thingTypeUID)) {
return new HttpThingHandler(thing, this, this, httpDynamicStateDescriptionProvider);
}
return null;
}
@Override
public ValueTransformation getValueTransformation(@Nullable String pattern) {
if (pattern == null) {
return NoOpValueTransformation.getInstance();
}
return new CascadedValueTransformationImpl(pattern,
name -> TransformationHelper.getTransformationService(bundleContext, name));
}
@Override
public HttpClient getSecureClient() {
return secureClient;
}
@Override
public HttpClient getInsecureClient() {
return insecureClient;
}
}

View File

@ -0,0 +1,367 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.http.internal;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.client.api.Authentication;
import org.eclipse.jetty.client.api.AuthenticationStore;
import org.eclipse.jetty.client.api.Request;
import org.eclipse.jetty.client.util.BasicAuthentication;
import org.eclipse.jetty.client.util.DigestAuthentication;
import org.eclipse.jetty.client.util.StringContentProvider;
import org.eclipse.jetty.http.HttpMethod;
import org.openhab.binding.http.internal.config.HttpChannelConfig;
import org.openhab.binding.http.internal.config.HttpChannelMode;
import org.openhab.binding.http.internal.config.HttpThingConfig;
import org.openhab.binding.http.internal.converter.AbstractTransformingItemConverter;
import org.openhab.binding.http.internal.converter.ColorItemConverter;
import org.openhab.binding.http.internal.converter.DimmerItemConverter;
import org.openhab.binding.http.internal.converter.FixedValueMappingItemConverter;
import org.openhab.binding.http.internal.converter.GenericItemConverter;
import org.openhab.binding.http.internal.converter.ImageItemConverter;
import org.openhab.binding.http.internal.converter.ItemValueConverter;
import org.openhab.binding.http.internal.converter.PlayerItemConverter;
import org.openhab.binding.http.internal.converter.RollershutterItemConverter;
import org.openhab.binding.http.internal.http.Content;
import org.openhab.binding.http.internal.http.HttpAuthException;
import org.openhab.binding.http.internal.http.HttpResponseListener;
import org.openhab.binding.http.internal.http.RefreshingUrlCache;
import org.openhab.binding.http.internal.transform.ValueTransformationProvider;
import org.openhab.core.library.types.DateTimeType;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.PointType;
import org.openhab.core.library.types.StringType;
import org.openhab.core.thing.Channel;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingStatusDetail;
import org.openhab.core.thing.binding.BaseThingHandler;
import org.openhab.core.types.Command;
import org.openhab.core.types.RefreshType;
import org.openhab.core.types.State;
import org.openhab.core.types.StateDescription;
import org.openhab.core.types.StateDescriptionFragmentBuilder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link HttpThingHandler} is responsible for handling commands, which are
* sent to one of the channels.
*
* @author Jan N. Klug - Initial contribution
*/
@NonNullByDefault
public class HttpThingHandler extends BaseThingHandler {
private static final Set<Character> URL_PART_DELIMITER = Set.of('/', '?', '&');
private final Logger logger = LoggerFactory.getLogger(HttpThingHandler.class);
private final ValueTransformationProvider valueTransformationProvider;
private final HttpClientProvider httpClientProvider;
private HttpClient httpClient;
private final HttpDynamicStateDescriptionProvider httpDynamicStateDescriptionProvider;
private HttpThingConfig config = new HttpThingConfig();
private final Map<String, RefreshingUrlCache> urlHandlers = new HashMap<>();
private final Map<ChannelUID, ItemValueConverter> channels = new HashMap<>();
private final Map<ChannelUID, String> channelUrls = new HashMap<>();
private @Nullable Authentication authentication;
public HttpThingHandler(Thing thing, HttpClientProvider httpClientProvider,
ValueTransformationProvider valueTransformationProvider,
HttpDynamicStateDescriptionProvider httpDynamicStateDescriptionProvider) {
super(thing);
this.httpClientProvider = httpClientProvider;
this.httpClient = httpClientProvider.getSecureClient();
this.valueTransformationProvider = valueTransformationProvider;
this.httpDynamicStateDescriptionProvider = httpDynamicStateDescriptionProvider;
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
ItemValueConverter itemValueConverter = channels.get(channelUID);
if (itemValueConverter == null) {
logger.warn("Cannot find channel implementation for channel {}.", channelUID);
return;
}
if (command instanceof RefreshType) {
String stateUrl = channelUrls.get(channelUID);
if (stateUrl != null) {
RefreshingUrlCache refreshingUrlCache = urlHandlers.get(stateUrl);
if (refreshingUrlCache != null) {
try {
refreshingUrlCache.get().ifPresent(itemValueConverter::process);
} catch (IllegalArgumentException | IllegalStateException e) {
logger.warn("Failed processing REFRESH command for channel {}: {}", channelUID, e.getMessage());
}
}
}
} else {
try {
itemValueConverter.send(command);
} catch (IllegalArgumentException e) {
logger.warn("Failed to convert command '{}' to channel '{}' for sending", command, channelUID);
} catch (IllegalStateException e) {
logger.debug("Writing to read-only channel {} not permitted", channelUID);
}
}
}
@Override
public void initialize() {
config = getConfigAs(HttpThingConfig.class);
if (config.baseURL.isEmpty()) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
"Parameter baseURL must not be empty!");
return;
}
authentication = null;
if (!config.username.isEmpty()) {
try {
URI uri = new URI(config.baseURL);
switch (config.authMode) {
case BASIC:
authentication = new BasicAuthentication(uri, Authentication.ANY_REALM, config.username,
config.password);
logger.debug("Basic Authentication configured for thing '{}'", thing.getUID());
break;
case DIGEST:
authentication = new DigestAuthentication(uri, Authentication.ANY_REALM, config.username,
config.password);
logger.debug("Digest Authentication configured for thing '{}'", thing.getUID());
break;
default:
logger.warn("Unknown authentication method '{}' for thing '{}'", config.authMode,
thing.getUID());
}
if (authentication != null) {
AuthenticationStore authStore = httpClient.getAuthenticationStore();
authStore.addAuthentication(authentication);
}
} catch (URISyntaxException e) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
"failed to create authentication: baseUrl is invalid");
}
} else {
logger.debug("No authentication configured for thing '{}'", thing.getUID());
}
if (config.ignoreSSLErrors) {
logger.info("Using the insecure client for thing '{}'.", thing.getUID());
httpClient = httpClientProvider.getInsecureClient();
} else {
logger.info("Using the secure client for thing '{}'.", thing.getUID());
httpClient = httpClientProvider.getSecureClient();
}
thing.getChannels().forEach(this::createChannel);
updateStatus(ThingStatus.ONLINE);
}
@Override
public void dispose() {
// stop update tasks
urlHandlers.values().forEach(RefreshingUrlCache::stop);
// clear lists
urlHandlers.clear();
channels.clear();
channelUrls.clear();
// remove state descriptions
httpDynamicStateDescriptionProvider.removeDescriptionsForThing(thing.getUID());
super.dispose();
}
/**
* create all necessary information to handle every channel
*
* @param channel a thing channel
*/
private void createChannel(Channel channel) {
ChannelUID channelUID = channel.getUID();
HttpChannelConfig channelConfig = channel.getConfiguration().as(HttpChannelConfig.class);
String stateUrl = concatenateUrlParts(config.baseURL, channelConfig.stateExtension);
String commandUrl = channelConfig.commandExtension == null ? stateUrl
: concatenateUrlParts(config.baseURL, channelConfig.commandExtension);
String acceptedItemType = channel.getAcceptedItemType();
if (acceptedItemType == null) {
logger.warn("Cannot determine item-type for channel '{}'", channelUID);
return;
}
ItemValueConverter itemValueConverter;
switch (acceptedItemType) {
case "Color":
itemValueConverter = createItemConverter(ColorItemConverter::new, commandUrl, channelUID,
channelConfig);
break;
case "DateTime":
itemValueConverter = createGenericItemConverter(commandUrl, channelUID, channelConfig,
DateTimeType::new);
break;
case "Dimmer":
itemValueConverter = createItemConverter(DimmerItemConverter::new, commandUrl, channelUID,
channelConfig);
break;
case "Contact":
case "Switch":
itemValueConverter = createItemConverter(FixedValueMappingItemConverter::new, commandUrl, channelUID,
channelConfig);
break;
case "Image":
itemValueConverter = new ImageItemConverter(state -> updateState(channelUID, state));
break;
case "Location":
itemValueConverter = createGenericItemConverter(commandUrl, channelUID, channelConfig, PointType::new);
break;
case "Number":
itemValueConverter = createGenericItemConverter(commandUrl, channelUID, channelConfig,
DecimalType::new);
break;
case "Player":
itemValueConverter = createItemConverter(PlayerItemConverter::new, commandUrl, channelUID,
channelConfig);
break;
case "Rollershutter":
itemValueConverter = createItemConverter(RollershutterItemConverter::new, commandUrl, channelUID,
channelConfig);
break;
case "String":
itemValueConverter = createGenericItemConverter(commandUrl, channelUID, channelConfig, StringType::new);
break;
default:
logger.warn("Unsupported item-type '{}'", channel.getAcceptedItemType());
return;
}
channels.put(channelUID, itemValueConverter);
if (channelConfig.mode != HttpChannelMode.WRITEONLY) {
channelUrls.put(channelUID, stateUrl);
urlHandlers.computeIfAbsent(stateUrl, url -> new RefreshingUrlCache(scheduler, httpClient, url, config))
.addConsumer(itemValueConverter::process);
}
StateDescription stateDescription = StateDescriptionFragmentBuilder.create()
.withReadOnly(channelConfig.mode == HttpChannelMode.READONLY).build().toStateDescription();
if (stateDescription != null) {
// if the state description is not available, we don'tneed to add it
httpDynamicStateDescriptionProvider.setDescription(channelUID, stateDescription);
}
}
private void sendHttpValue(String commandUrl, String command) {
sendHttpValue(commandUrl, command, false);
}
private void sendHttpValue(String commandUrl, String command, boolean isRetry) {
try {
// format URL
URI finalUrl = new URI(String.format(commandUrl, new Date(), command));
// build request
Request request = httpClient.newRequest(finalUrl).timeout(config.timeout, TimeUnit.MILLISECONDS)
.method(config.commandMethod);
if (config.commandMethod != HttpMethod.GET) {
final String contentType = config.contentType;
if (contentType != null) {
request.content(new StringContentProvider(command), contentType);
} else {
request.content(new StringContentProvider(command));
}
}
config.headers.forEach(header -> {
String[] keyValuePair = header.split("=", 2);
if (keyValuePair.length == 2) {
request.header(keyValuePair[0], keyValuePair[1]);
} else {
logger.warn("Splitting header '{}' failed. No '=' was found. Ignoring", header);
}
});
if (logger.isTraceEnabled()) {
logger.trace("Sending to '{}': {}", finalUrl, Util.requestToLogString(request));
}
CompletableFuture<@Nullable Content> f = new CompletableFuture<>();
f.exceptionally(e -> {
if (e instanceof HttpAuthException) {
if (isRetry) {
logger.warn("Retry after authentication failure failed again for '{}', failing here", finalUrl);
} else {
AuthenticationStore authStore = httpClient.getAuthenticationStore();
Authentication.Result authResult = authStore.findAuthenticationResult(finalUrl);
if (authResult != null) {
authStore.removeAuthenticationResult(authResult);
logger.debug("Cleared authentication result for '{}', retrying immediately", finalUrl);
sendHttpValue(commandUrl, command, true);
} else {
logger.warn("Could not find authentication result for '{}', failing here", finalUrl);
}
}
}
return null;
});
request.send(new HttpResponseListener(f));
} catch (IllegalArgumentException | URISyntaxException e) {
logger.warn("Creating request for '{}' failed: {}", commandUrl, e.getMessage());
}
}
private String concatenateUrlParts(String baseUrl, @Nullable String extension) {
if (extension != null && !extension.isEmpty()) {
if (!URL_PART_DELIMITER.contains(baseUrl.charAt(baseUrl.length() - 1))
&& !URL_PART_DELIMITER.contains(extension.charAt(0))) {
return baseUrl + "/" + extension;
} else {
return baseUrl + extension;
}
} else {
return baseUrl;
}
}
private ItemValueConverter createItemConverter(AbstractTransformingItemConverter.Factory factory, String commandUrl,
ChannelUID channelUID, HttpChannelConfig channelConfig) {
return factory.create(state -> updateState(channelUID, state), command -> postCommand(channelUID, command),
command -> sendHttpValue(commandUrl, command),
valueTransformationProvider.getValueTransformation(channelConfig.stateTransformation),
valueTransformationProvider.getValueTransformation(channelConfig.commandTransformation), channelConfig);
}
private ItemValueConverter createGenericItemConverter(String commandUrl, ChannelUID channelUID,
HttpChannelConfig channelConfig, Function<String, State> toState) {
AbstractTransformingItemConverter.Factory factory = (state, command, value, stateTrans, commandTrans,
config) -> new GenericItemConverter(toState, state, command, value, stateTrans, commandTrans, config);
return createItemConverter(factory, commandUrl, channelUID, channelConfig);
}
}

View File

@ -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;
}
}

View File

@ -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
}

View File

@ -0,0 +1,137 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.http.internal.config;
import java.math.BigDecimal;
import java.util.HashMap;
import java.util.Map;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.http.internal.converter.ColorItemConverter;
import org.openhab.core.library.types.IncreaseDecreaseType;
import org.openhab.core.library.types.NextPreviousType;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.OpenClosedType;
import org.openhab.core.library.types.PlayPauseType;
import org.openhab.core.library.types.RewindFastforwardType;
import org.openhab.core.library.types.StopMoveType;
import org.openhab.core.library.types.UpDownType;
import org.openhab.core.types.Command;
import org.openhab.core.types.State;
/**
* The {@link HttpChannelConfig} class contains fields mapping channel configuration parameters.
*
* @author Jan N. Klug - Initial contribution
*/
@NonNullByDefault
public class HttpChannelConfig {
private final Map<String, State> stringStateMap = new HashMap<>();
private final Map<Command, @Nullable String> commandStringMap = new HashMap<>();
private boolean initialized = false;
public @Nullable String stateExtension;
public @Nullable String commandExtension;
public @Nullable String stateTransformation;
public @Nullable String commandTransformation;
public HttpChannelMode mode = HttpChannelMode.READWRITE;
// switch, dimmer, color
public @Nullable String onValue;
public @Nullable String offValue;
// dimmer, color
public BigDecimal step = BigDecimal.ONE;
public @Nullable String increaseValue;
public @Nullable String decreaseValue;
// color
public ColorItemConverter.ColorMode colorMode = ColorItemConverter.ColorMode.RGB;
// contact
public @Nullable String openValue;
public @Nullable String closedValue;
// rollershutter
public @Nullable String upValue;
public @Nullable String downValue;
public @Nullable String stopValue;
public @Nullable String moveValue;
// player
public @Nullable String playValue;
public @Nullable String pauseValue;
public @Nullable String nextValue;
public @Nullable String previousValue;
public @Nullable String rewindValue;
public @Nullable String fastforwardValue;
/**
* maps a command to a user-defined string
*
* @param command the command to map
* @return a string or null if no mapping found
*/
public @Nullable String commandToFixedValue(Command command) {
if (!initialized) {
createMaps();
}
return commandStringMap.get(command);
}
/**
* maps a user-defined string to a state
*
* @param string the string to map
* @return the state or null if no mapping found
*/
public @Nullable State fixedValueToState(String string) {
if (!initialized) {
createMaps();
}
return stringStateMap.get(string);
}
private void createMaps() {
addToMaps(this.onValue, OnOffType.ON);
addToMaps(this.offValue, OnOffType.OFF);
addToMaps(this.openValue, OpenClosedType.OPEN);
addToMaps(this.closedValue, OpenClosedType.CLOSED);
addToMaps(this.upValue, UpDownType.UP);
addToMaps(this.downValue, UpDownType.DOWN);
commandStringMap.put(IncreaseDecreaseType.INCREASE, increaseValue);
commandStringMap.put(IncreaseDecreaseType.DECREASE, decreaseValue);
commandStringMap.put(StopMoveType.STOP, stopValue);
commandStringMap.put(StopMoveType.MOVE, moveValue);
commandStringMap.put(PlayPauseType.PLAY, playValue);
commandStringMap.put(PlayPauseType.PAUSE, pauseValue);
commandStringMap.put(NextPreviousType.NEXT, nextValue);
commandStringMap.put(NextPreviousType.PREVIOUS, previousValue);
commandStringMap.put(RewindFastforwardType.REWIND, rewindValue);
commandStringMap.put(RewindFastforwardType.FASTFORWARD, fastforwardValue);
initialized = true;
}
private void addToMaps(@Nullable String value, State state) {
if (value != null) {
commandStringMap.put((Command) state, value);
stringStateMap.put(value, state);
}
}
}

View File

@ -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
}

View File

@ -0,0 +1,45 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.http.internal.config;
import java.util.Collections;
import java.util.List;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jetty.http.HttpMethod;
/**
* The {@link HttpThingConfig} class contains fields mapping thing configuration parameters.
*
* @author Jan N. Klug - Initial contribution
*/
@NonNullByDefault
public class HttpThingConfig {
public String baseURL = "";
public int refresh = 30;
public int timeout = 3000;
public String username = "";
public String password = "";
public HttpAuthMode authMode = HttpAuthMode.BASIC;
public HttpMethod commandMethod = HttpMethod.GET;
public @Nullable String encoding = null;
public @Nullable String contentType = null;
public boolean ignoreSSLErrors = false;
public List<String> headers = Collections.emptyList();
}

View File

@ -0,0 +1,108 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.http.internal.converter;
import java.util.function.Consumer;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.http.internal.config.HttpChannelConfig;
import org.openhab.binding.http.internal.config.HttpChannelMode;
import org.openhab.binding.http.internal.http.Content;
import org.openhab.binding.http.internal.transform.ValueTransformation;
import org.openhab.core.types.Command;
import org.openhab.core.types.State;
/**
* The {@link AbstractTransformingItemConverter} is a base class for an item converter with transformations
*
* @author Jan N. Klug - Initial contribution
*/
@NonNullByDefault
public abstract class AbstractTransformingItemConverter implements ItemValueConverter {
private final Consumer<State> updateState;
private final Consumer<Command> postCommand;
private final @Nullable Consumer<String> sendHttpValue;
private final ValueTransformation stateTransformations;
private final ValueTransformation commandTransformations;
protected HttpChannelConfig channelConfig;
public AbstractTransformingItemConverter(Consumer<State> updateState, Consumer<Command> postCommand,
@Nullable Consumer<String> sendHttpValue, ValueTransformation stateTransformations,
ValueTransformation commandTransformations, HttpChannelConfig channelConfig) {
this.updateState = updateState;
this.postCommand = postCommand;
this.sendHttpValue = sendHttpValue;
this.stateTransformations = stateTransformations;
this.commandTransformations = commandTransformations;
this.channelConfig = channelConfig;
}
@Override
public void process(Content content) {
if (channelConfig.mode != HttpChannelMode.WRITEONLY) {
stateTransformations.apply(content.getAsString()).ifPresent(transformedValue -> {
Command command = toCommand(transformedValue);
if (command != null) {
postCommand.accept(command);
} else {
updateState.accept(toState(transformedValue));
}
});
} else {
throw new IllegalStateException("Write-only channel");
}
}
@Override
public void send(Command command) {
Consumer<String> sendHttpValue = this.sendHttpValue;
if (sendHttpValue != null && channelConfig.mode != HttpChannelMode.READONLY) {
commandTransformations.apply(toString(command)).ifPresent(sendHttpValue);
} else {
throw new IllegalStateException("Read-only channel");
}
}
/**
* check if this converter received a value that needs to be sent as command
*
* @param value the value
* @return the command or null
*/
protected abstract @Nullable Command toCommand(String value);
/**
* convert the received value to a state
*
* @param value the value
* @return the state that represents the value of UNDEF if conversion failed
*/
protected abstract State toState(String value);
/**
* convert a command to a string
*
* @param command the command
* @return the string representation of the command
*/
protected abstract String toString(Command command);
@FunctionalInterface
public interface Factory {
ItemValueConverter create(Consumer<State> updateState, Consumer<Command> postCommand,
@Nullable Consumer<String> sendHttpValue, ValueTransformation stateTransformations,
ValueTransformation commandTransformations, HttpChannelConfig channelConfig);
}
}

View File

@ -0,0 +1,145 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.http.internal.converter;
import java.math.BigDecimal;
import java.util.function.Consumer;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.http.internal.config.HttpChannelConfig;
import org.openhab.binding.http.internal.transform.ValueTransformation;
import org.openhab.core.library.types.HSBType;
import org.openhab.core.library.types.PercentType;
import org.openhab.core.types.Command;
import org.openhab.core.types.State;
import org.openhab.core.types.UnDefType;
/**
* The {@link ColorItemConverter} implements {@link org.openhab.core.library.items.ColorItem} conversions
*
* @author Jan N. Klug - Initial contribution
*/
@NonNullByDefault
public class ColorItemConverter extends AbstractTransformingItemConverter {
private static final BigDecimal BYTE_FACTOR = BigDecimal.valueOf(2.55);
private static final BigDecimal HUNDRED = BigDecimal.valueOf(100);
private static final Pattern TRIPLE_MATCHER = Pattern.compile("(\\d+),(\\d+),(\\d+)");
private State state = UnDefType.UNDEF;
public ColorItemConverter(Consumer<State> updateState, Consumer<Command> postCommand,
@Nullable Consumer<String> sendHttpValue, ValueTransformation stateTransformations,
ValueTransformation commandTransformations, HttpChannelConfig channelConfig) {
super(updateState, postCommand, sendHttpValue, stateTransformations, commandTransformations, channelConfig);
this.channelConfig = channelConfig;
}
@Override
protected @Nullable Command toCommand(String value) {
return null;
}
@Override
public String toString(Command command) {
String string = channelConfig.commandToFixedValue(command);
if (string != null) {
return string;
}
if (command instanceof HSBType) {
HSBType newState = (HSBType) command;
state = newState;
return hsbToString(newState);
} else if (command instanceof PercentType && state instanceof HSBType) {
HSBType newState = new HSBType(((HSBType) state).getBrightness(), ((HSBType) state).getSaturation(),
(PercentType) command);
state = newState;
return hsbToString(newState);
}
throw new IllegalArgumentException("Command type '" + command.toString() + "' not supported");
}
@Override
public State toState(String string) {
State newState = UnDefType.UNDEF;
if (string.equals(channelConfig.onValue)) {
if (state instanceof HSBType) {
newState = new HSBType(((HSBType) state).getHue(), ((HSBType) state).getSaturation(),
PercentType.HUNDRED);
} else {
newState = HSBType.WHITE;
}
} else if (string.equals(channelConfig.offValue)) {
if (state instanceof HSBType) {
newState = new HSBType(((HSBType) state).getHue(), ((HSBType) state).getSaturation(), PercentType.ZERO);
} else {
newState = HSBType.BLACK;
}
} else if (string.equals(channelConfig.increaseValue) && state instanceof HSBType) {
BigDecimal newBrightness = ((HSBType) state).getBrightness().toBigDecimal().add(channelConfig.step);
if (HUNDRED.compareTo(newBrightness) < 0) {
newBrightness = HUNDRED;
}
newState = new HSBType(((HSBType) state).getHue(), ((HSBType) state).getSaturation(),
new PercentType(newBrightness));
} else if (string.equals(channelConfig.decreaseValue) && state instanceof HSBType) {
BigDecimal newBrightness = ((HSBType) state).getBrightness().toBigDecimal().subtract(channelConfig.step);
if (BigDecimal.ZERO.compareTo(newBrightness) > 0) {
newBrightness = BigDecimal.ZERO;
}
newState = new HSBType(((HSBType) state).getHue(), ((HSBType) state).getSaturation(),
new PercentType(newBrightness));
} else {
Matcher matcher = TRIPLE_MATCHER.matcher(string);
if (matcher.matches()) {
switch (channelConfig.colorMode) {
case RGB:
int r = Integer.parseInt(matcher.group(0));
int g = Integer.parseInt(matcher.group(1));
int b = Integer.parseInt(matcher.group(2));
newState = HSBType.fromRGB(r, g, b);
break;
case HSB:
newState = new HSBType(string);
break;
}
}
}
state = newState;
return newState;
}
private String hsbToString(HSBType state) {
switch (channelConfig.colorMode) {
case RGB:
PercentType[] rgb = state.toRGB();
return String.format("%1$d,%2$d,%3$d", rgb[0].toBigDecimal().multiply(BYTE_FACTOR).intValue(),
rgb[1].toBigDecimal().multiply(BYTE_FACTOR).intValue(),
rgb[2].toBigDecimal().multiply(BYTE_FACTOR).intValue());
case HSB:
return state.toString();
}
throw new IllegalStateException("Invalid colorMode setting");
}
public enum ColorMode {
RGB,
HSB
}
}

View File

@ -0,0 +1,103 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.http.internal.converter;
import java.math.BigDecimal;
import java.util.function.Consumer;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.http.internal.config.HttpChannelConfig;
import org.openhab.binding.http.internal.transform.ValueTransformation;
import org.openhab.core.library.types.PercentType;
import org.openhab.core.types.Command;
import org.openhab.core.types.State;
import org.openhab.core.types.UnDefType;
/**
* The {@link DimmerItemConverter} implements {@link org.openhab.core.library.items.DimmerItem} conversions
*
* @author Jan N. Klug - Initial contribution
*/
@NonNullByDefault
public class DimmerItemConverter extends AbstractTransformingItemConverter {
private static final BigDecimal HUNDRED = BigDecimal.valueOf(100);
private State state = UnDefType.UNDEF;
public DimmerItemConverter(Consumer<State> updateState, Consumer<Command> postCommand,
@Nullable Consumer<String> sendHttpValue, ValueTransformation stateTransformations,
ValueTransformation commandTransformations, HttpChannelConfig channelConfig) {
super(updateState, postCommand, sendHttpValue, stateTransformations, commandTransformations, channelConfig);
this.channelConfig = channelConfig;
}
@Override
protected @Nullable Command toCommand(String value) {
return null;
}
@Override
public String toString(Command command) {
String string = channelConfig.commandToFixedValue(command);
if (string != null) {
return string;
}
if (command instanceof PercentType) {
return ((PercentType) command).toString();
}
throw new IllegalArgumentException("Command type '" + command.toString() + "' not supported");
}
@Override
public State toState(String string) {
State newState = UnDefType.UNDEF;
if (string.equals(channelConfig.onValue)) {
newState = PercentType.HUNDRED;
} else if (string.equals(channelConfig.offValue)) {
newState = PercentType.ZERO;
} else if (string.equals(channelConfig.increaseValue) && state instanceof PercentType) {
BigDecimal newBrightness = ((PercentType) state).toBigDecimal().add(channelConfig.step);
if (HUNDRED.compareTo(newBrightness) < 0) {
newBrightness = HUNDRED;
}
newState = new PercentType(newBrightness);
} else if (string.equals(channelConfig.decreaseValue) && state instanceof PercentType) {
BigDecimal newBrightness = ((PercentType) state).toBigDecimal().subtract(channelConfig.step);
if (BigDecimal.ZERO.compareTo(newBrightness) > 0) {
newBrightness = BigDecimal.ZERO;
}
newState = new PercentType(newBrightness);
} else {
try {
BigDecimal value = new BigDecimal(string);
if (value.compareTo(PercentType.HUNDRED.toBigDecimal()) > 0) {
value = PercentType.HUNDRED.toBigDecimal();
}
if (value.compareTo(PercentType.ZERO.toBigDecimal()) < 0) {
value = PercentType.ZERO.toBigDecimal();
}
newState = new PercentType(value);
} catch (NumberFormatException e) {
// ignore
}
}
state = newState;
return newState;
}
}

View File

@ -0,0 +1,62 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.http.internal.converter;
import java.util.function.Consumer;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.http.internal.config.HttpChannelConfig;
import org.openhab.binding.http.internal.transform.ValueTransformation;
import org.openhab.core.types.Command;
import org.openhab.core.types.State;
import org.openhab.core.types.UnDefType;
/**
* The {@link FixedValueMappingItemConverter} implements mapping conversions for different item-types
*
* @author Jan N. Klug - Initial contribution
*/
@NonNullByDefault
public class FixedValueMappingItemConverter extends AbstractTransformingItemConverter {
public FixedValueMappingItemConverter(Consumer<State> updateState, Consumer<Command> postCommand,
@Nullable Consumer<String> sendHttpValue, ValueTransformation stateTransformations,
ValueTransformation commandTransformations, HttpChannelConfig channelConfig) {
super(updateState, postCommand, sendHttpValue, stateTransformations, commandTransformations, channelConfig);
}
@Override
protected @Nullable Command toCommand(String value) {
return null;
}
@Override
public String toString(Command command) {
String value = channelConfig.commandToFixedValue(command);
if (value != null) {
return value;
}
throw new IllegalArgumentException(
"Command type '" + command.toString() + "' not supported or mapping not defined.");
}
@Override
public State toState(String string) {
State state = channelConfig.fixedValueToState(string);
return state != null ? state : UnDefType.UNDEF;
}
}

View File

@ -0,0 +1,59 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.http.internal.converter;
import java.util.function.Consumer;
import java.util.function.Function;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.http.internal.config.HttpChannelConfig;
import org.openhab.binding.http.internal.transform.ValueTransformation;
import org.openhab.core.types.Command;
import org.openhab.core.types.State;
import org.openhab.core.types.UnDefType;
/**
* The {@link GenericItemConverter} implements simple conversions for different item types
*
* @author Jan N. Klug - Initial contribution
*/
@NonNullByDefault
public class GenericItemConverter extends AbstractTransformingItemConverter {
private final Function<String, State> toState;
public GenericItemConverter(Function<String, State> toState, Consumer<State> updateState,
Consumer<Command> postCommand, @Nullable Consumer<String> sendHttpValue,
ValueTransformation stateTransformations, ValueTransformation commandTransformations,
HttpChannelConfig channelConfig) {
super(updateState, postCommand, sendHttpValue, stateTransformations, commandTransformations, channelConfig);
this.toState = toState;
}
protected State toState(String value) {
try {
return toState.apply(value);
} catch (IllegalArgumentException e) {
return UnDefType.UNDEF;
}
}
@Override
protected @Nullable Command toCommand(String value) {
return null;
}
protected String toString(Command command) {
return command.toString();
}
}

View File

@ -0,0 +1,48 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.http.internal.converter;
import java.util.function.Consumer;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.http.internal.http.Content;
import org.openhab.core.library.types.RawType;
import org.openhab.core.types.Command;
import org.openhab.core.types.State;
/**
* The {@link ImageItemConverter} implements {@link org.openhab.core.library.items.ImageItem} conversions
*
* @author Jan N. Klug - Initial contribution
*/
@NonNullByDefault
public class ImageItemConverter implements ItemValueConverter {
private final Consumer<State> updateState;
public ImageItemConverter(Consumer<State> updateState) {
this.updateState = updateState;
}
@Override
public void process(Content content) {
String mediaType = content.getMediaType();
updateState.accept(
new RawType(content.getRawContent(), mediaType != null ? mediaType : RawType.DEFAULT_MIME_TYPE));
}
@Override
public void send(Command command) {
throw new IllegalStateException("Read-only channel");
}
}

View File

@ -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);
}

View File

@ -0,0 +1,79 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.http.internal.converter;
import java.util.function.Consumer;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.http.internal.config.HttpChannelConfig;
import org.openhab.binding.http.internal.transform.ValueTransformation;
import org.openhab.core.library.types.NextPreviousType;
import org.openhab.core.library.types.PlayPauseType;
import org.openhab.core.library.types.RewindFastforwardType;
import org.openhab.core.types.Command;
import org.openhab.core.types.State;
import org.openhab.core.types.UnDefType;
/**
* The {@link PlayerItemConverter} implements {@link org.openhab.core.library.items.RollershutterItem}
* conversions
*
* @author Jan N. Klug - Initial contribution
*/
@NonNullByDefault
public class PlayerItemConverter extends AbstractTransformingItemConverter {
private final HttpChannelConfig channelConfig;
public PlayerItemConverter(Consumer<State> updateState, Consumer<Command> postCommand,
@Nullable Consumer<String> sendHttpValue, ValueTransformation stateTransformations,
ValueTransformation commandTransformations, HttpChannelConfig channelConfig) {
super(updateState, postCommand, sendHttpValue, stateTransformations, commandTransformations, channelConfig);
this.channelConfig = channelConfig;
}
@Override
public String toString(Command command) {
String string = channelConfig.commandToFixedValue(command);
if (string != null) {
return string;
}
throw new IllegalArgumentException("Command type '" + command.toString() + "' not supported");
}
@Override
protected @Nullable Command toCommand(String string) {
if (string.equals(channelConfig.playValue)) {
return PlayPauseType.PLAY;
} else if (string.equals(channelConfig.pauseValue)) {
return PlayPauseType.PAUSE;
} else if (string.equals(channelConfig.nextValue)) {
return NextPreviousType.NEXT;
} else if (string.equals(channelConfig.previousValue)) {
return NextPreviousType.PREVIOUS;
} else if (string.equals(channelConfig.rewindValue)) {
return RewindFastforwardType.REWIND;
} else if (string.equals(channelConfig.fastforwardValue)) {
return RewindFastforwardType.FASTFORWARD;
}
return null;
}
@Override
public State toState(String string) {
return UnDefType.UNDEF;
}
}

View File

@ -0,0 +1,100 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.http.internal.converter;
import java.math.BigDecimal;
import java.util.function.Consumer;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.http.internal.config.HttpChannelConfig;
import org.openhab.binding.http.internal.transform.ValueTransformation;
import org.openhab.core.library.types.PercentType;
import org.openhab.core.library.types.StopMoveType;
import org.openhab.core.library.types.UpDownType;
import org.openhab.core.types.Command;
import org.openhab.core.types.State;
import org.openhab.core.types.UnDefType;
/**
* The {@link RollershutterItemConverter} implements {@link org.openhab.core.library.items.RollershutterItem}
* conversions
*
* @author Jan N. Klug - Initial contribution
*/
@NonNullByDefault
public class RollershutterItemConverter extends AbstractTransformingItemConverter {
private final HttpChannelConfig channelConfig;
public RollershutterItemConverter(Consumer<State> updateState, Consumer<Command> postCommand,
@Nullable Consumer<String> sendHttpValue, ValueTransformation stateTransformations,
ValueTransformation commandTransformations, HttpChannelConfig channelConfig) {
super(updateState, postCommand, sendHttpValue, stateTransformations, commandTransformations, channelConfig);
this.channelConfig = channelConfig;
}
@Override
public String toString(Command command) {
String string = channelConfig.commandToFixedValue(command);
if (string != null) {
return string;
}
if (command instanceof PercentType) {
final String downValue = channelConfig.downValue;
final String upValue = channelConfig.upValue;
if (command.equals(PercentType.HUNDRED) && downValue != null) {
return downValue;
} else if (command.equals(PercentType.ZERO) && upValue != null) {
return upValue;
} else {
return ((PercentType) command).toString();
}
}
throw new IllegalArgumentException("Command type '" + command.toString() + "' not supported");
}
@Override
protected @Nullable Command toCommand(String string) {
if (string.equals(channelConfig.upValue)) {
return UpDownType.UP;
} else if (string.equals(channelConfig.downValue)) {
return UpDownType.DOWN;
} else if (string.equals(channelConfig.moveValue)) {
return StopMoveType.MOVE;
} else if (string.equals(channelConfig.stopValue)) {
return StopMoveType.STOP;
}
return null;
}
@Override
public State toState(String string) {
try {
BigDecimal value = new BigDecimal(string);
if (value.compareTo(PercentType.HUNDRED.toBigDecimal()) > 0) {
return PercentType.HUNDRED;
}
if (value.compareTo(PercentType.ZERO.toBigDecimal()) < 0) {
return PercentType.ZERO;
}
} catch (NumberFormatException e) {
// ignore
}
return UnDefType.UNDEF;
}
}

View File

@ -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;
}
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -0,0 +1,160 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.http.internal.http;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Date;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.client.api.Authentication;
import org.eclipse.jetty.client.api.AuthenticationStore;
import org.eclipse.jetty.client.api.Request;
import org.openhab.binding.http.internal.Util;
import org.openhab.binding.http.internal.config.HttpThingConfig;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link RefreshingUrlCache} is responsible for requesting from a single URL and passing the content to the
* channels
*
* @author Jan N. Klug - Initial contribution
*/
@NonNullByDefault
public class RefreshingUrlCache {
private final Logger logger = LoggerFactory.getLogger(RefreshingUrlCache.class);
private final String url;
private final HttpClient httpClient;
private final int timeout;
private final @Nullable String fallbackEncoding;
private final Set<Consumer<Content>> consumers = ConcurrentHashMap.newKeySet();
private final List<String> headers;
private final ScheduledFuture<?> future;
private @Nullable Content lastContent;
public RefreshingUrlCache(ScheduledExecutorService executor, HttpClient httpClient, String url,
HttpThingConfig thingConfig) {
this.httpClient = httpClient;
this.url = url;
this.timeout = thingConfig.timeout;
this.headers = thingConfig.headers;
fallbackEncoding = thingConfig.encoding;
future = executor.scheduleWithFixedDelay(this::refresh, 0, thingConfig.refresh, TimeUnit.SECONDS);
logger.trace("Started refresh task for URL '{}' with interval {}s", url, thingConfig.refresh);
}
private void refresh() {
refresh(false);
}
private void refresh(boolean isRetry) {
if (consumers.isEmpty()) {
// do not refresh if we don't have listeners
return;
}
// format URL
try {
URI finalUrl = new URI(String.format(this.url, new Date()));
logger.trace("Requesting refresh (retry={}) from '{}' with timeout {}ms", isRetry, finalUrl, timeout);
Request request = httpClient.newRequest(finalUrl).timeout(timeout, TimeUnit.MILLISECONDS);
headers.forEach(header -> {
String[] keyValuePair = header.split("=", 2);
if (keyValuePair.length == 2) {
request.header(keyValuePair[0].trim(), keyValuePair[1].trim());
} else {
logger.warn("Splitting header '{}' failed. No '=' was found. Ignoring", header);
}
});
CompletableFuture<@Nullable Content> response = new CompletableFuture<>();
response.exceptionally(e -> {
if (e instanceof HttpAuthException) {
if (isRetry) {
logger.warn("Retry after authentication failure failed again for '{}', failing here",
finalUrl);
} else {
AuthenticationStore authStore = httpClient.getAuthenticationStore();
Authentication.Result authResult = authStore.findAuthenticationResult(finalUrl);
if (authResult != null) {
authStore.removeAuthenticationResult(authResult);
logger.debug("Cleared authentication result for '{}', retrying immediately", finalUrl);
refresh(true);
} else {
logger.warn("Could not find authentication result for '{}', failing here", finalUrl);
}
}
}
return null;
}).thenAccept(this::processResult);
if (logger.isTraceEnabled()) {
logger.trace("Sending to '{}': {}", finalUrl, Util.requestToLogString(request));
}
request.send(new HttpResponseListener(response, fallbackEncoding));
} catch (IllegalArgumentException | URISyntaxException e) {
logger.warn("Creating request for '{}' failed: {}", url, e.getMessage());
}
}
public void stop() {
// clearing all listeners to prevent further updates
consumers.clear();
future.cancel(false);
logger.trace("Stopped refresh task for URL '{}'", url);
}
public void addConsumer(Consumer<Content> consumer) {
consumers.add(consumer);
}
public Optional<Content> get() {
final Content content = lastContent;
if (content == null) {
return Optional.empty();
} else {
return Optional.of(content);
}
}
private void processResult(@Nullable Content content) {
if (content != null) {
for (Consumer<Content> consumer : consumers) {
try {
consumer.accept(content);
} catch (IllegalArgumentException | IllegalStateException e) {
logger.warn("Failed processing result for URL {}: {}", url, e.getMessage());
}
}
}
lastContent = content;
}
}

View File

@ -0,0 +1,53 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.http.internal.transform;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import java.util.function.Function;
import java.util.stream.Collectors;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.transform.TransformationService;
/**
* The {@link CascadedValueTransformationImpl} implements {@link ValueTransformation for a cascaded set of
* transformations}
*
* @author Jan N. Klug - Initial contribution
*/
@NonNullByDefault
public class CascadedValueTransformationImpl implements ValueTransformation {
private final List<ValueTransformation> transformations;
public CascadedValueTransformationImpl(String transformationString,
Function<String, @Nullable TransformationService> transformationServiceSupplier) {
transformations = Arrays.stream(transformationString.split("")).filter(s -> !s.isEmpty())
.map(transformation -> new SingleValueTransformation(transformation, transformationServiceSupplier))
.collect(Collectors.toList());
}
@Override
public Optional<String> apply(String value) {
Optional<String> valueOptional = Optional.of(value);
// process all transformations
for (ValueTransformation transformation : transformations) {
valueOptional = valueOptional.flatMap(transformation::apply);
}
return valueOptional;
}
}

View File

@ -0,0 +1,41 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.http.internal.transform;
import java.util.Optional;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* The {@link NoOpValueTransformation} implements a no-op (identity) transformation
*
* @author Jan N. Klug - Initial contribution
*/
@NonNullByDefault
public class NoOpValueTransformation implements ValueTransformation {
private static final NoOpValueTransformation NO_OP_VALUE_TRANSFORMATION = new NoOpValueTransformation();
@Override
public Optional<String> apply(String value) {
return Optional.of(value);
}
/**
* get the static value transformation for identity
*
* @return
*/
public static ValueTransformation getInstance() {
return NO_OP_VALUE_TRANSFORMATION;
}
}

View File

@ -0,0 +1,89 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.http.internal.transform;
import java.lang.ref.WeakReference;
import java.util.Optional;
import java.util.function.Function;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.transform.TransformationException;
import org.openhab.core.transform.TransformationService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* A transformation for a value used in {@HttpChannel}.
*
* @author David Graeff - Initial contribution
* @author Jan N. Klug - adapted from MQTT binding to HTTP binding
*/
@NonNullByDefault
public class SingleValueTransformation implements ValueTransformation {
private final Logger logger = LoggerFactory.getLogger(SingleValueTransformation.class);
private final Function<String, @Nullable TransformationService> transformationServiceSupplier;
private WeakReference<@Nullable TransformationService> transformationService = new WeakReference<>(null);
private final String pattern;
private final String serviceName;
/**
* Creates a new channel state transformer.
*
* @param pattern A transformation pattern, starting with the transformation service
* name, followed by a colon and the transformation itself.
* @param transformationServiceSupplier
*/
public SingleValueTransformation(String pattern,
Function<String, @Nullable TransformationService> transformationServiceSupplier) {
this.transformationServiceSupplier = transformationServiceSupplier;
int index = pattern.indexOf(':');
if (index == -1) {
throw new IllegalArgumentException(
"The transformation pattern must consist of the type and the pattern separated by a colon");
}
this.serviceName = pattern.substring(0, index).toUpperCase();
this.pattern = pattern.substring(index + 1);
}
@Override
public Optional<String> apply(String value) {
TransformationService transformationService = this.transformationService.get();
if (transformationService == null) {
transformationService = transformationServiceSupplier.apply(serviceName);
if (transformationService == null) {
logger.warn("Transformation service {} for pattern {} not found!", serviceName, pattern);
return Optional.empty();
}
this.transformationService = new WeakReference<>(transformationService);
}
try {
String result = transformationService.transform(pattern, value);
if (result == null) {
logger.debug("Transformation {} returned empty result when applied to {}.", this, value);
return Optional.empty();
}
return Optional.of(result);
} catch (TransformationException e) {
logger.warn("Executing transformation {} failed: {}", this, e.getMessage());
}
return Optional.empty();
}
@Override
public String toString() {
return "ChannelStateTransformation{pattern='" + pattern + "', serviceName='" + serviceName + "'}";
}
}

View File

@ -0,0 +1,34 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.http.internal.transform;
import java.util.Optional;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* The {@link ValueTransformation} applies a set of transformations to a value
*
* @author Jan N. Klug - Initial contribution
*/
@NonNullByDefault
public interface ValueTransformation {
/**
* applies the value transformation to a value
*
* @param value The value
* @return Optional of string representing the transformed value (empty if transformation not present or failed)
*/
Optional<String> apply(String value);
}

View File

@ -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);
}

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<binding:binding id="http" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:binding="https://openhab.org/schemas/binding/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/binding/v1.0.0 https://openhab.org/schemas/binding-1.0.0.xsd">
<name>HTTP Binding</name>
<description>This is the binding for retrieving and processing HTTP resources.</description>
<author>Jan N. Klug</author>
</binding:binding>

View File

@ -0,0 +1,350 @@
<?xml version="1.0" encoding="UTF-8"?>
<config-description:config-descriptions
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:config-description="https://openhab.org/schemas/config-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/config-description/v1.0.0 https://openhab.org/schemas/config-description-1.0.0.xsd">
<config-description uri="channel-type:http:channel-config">
<parameter name="stateExtension" type="text">
<label>State URL Extension</label>
<description>This value is added to the base URL configured in the thing for retrieving values.</description>
<advanced>true</advanced>
</parameter>
<parameter name="commandExtension" type="text">
<label>Command URL Extension</label>
<description>This value is added to the base URL configured in the thing for sending values.</description>
<advanced>true</advanced>
</parameter>
<parameter name="stateTransformation" type="text">
<label>State Transformation</label>
<description>Transformation pattern used when receiving values.</description>
</parameter>
<parameter name="commandTransformation" type="text">
<label>Command Transformation</label>
<description>Transformation pattern used when sending values.</description>
</parameter>
<parameter name="mode" type="text">
<label>Read/Write Mode</label>
<options>
<option value="READWRITE">Read/Write</option>
<option value="READONLY">Read Only</option>
<option value="WRITEONLY">Write Only</option>
</options>
<limitToOptions>true</limitToOptions>
<advanced>true</advanced>
<default>READWRITE</default>
</parameter>
</config-description>
<config-description uri="channel-type:http:channel-config-color">
<parameter name="stateExtension" type="text">
<label>State URL Extension</label>
<description>This value is added to the base URL configured in the thing for retrieving values.</description>
<advanced>true</advanced>
</parameter>
<parameter name="commandExtension" type="text">
<label>Command URL Extension</label>
<description>This value is added to the base URL configured in the thing for sending values.</description>
<advanced>true</advanced>
</parameter>
<parameter name="stateTransformation" type="text">
<label>State Transformation</label>
<description>Transformation pattern used when receiving values.</description>
</parameter>
<parameter name="commandTransformation" type="text">
<label>Command Transformation</label>
<description>Transformation pattern used when sending values.</description>
</parameter>
<parameter name="onValue" type="text">
<label>On Value</label>
<description>The value that represents ON</description>
</parameter>
<parameter name="offValue" type="text">
<label>Off Value</label>
<description>The value that represents OFF</description>
</parameter>
<parameter name="increaseValue" type="text">
<label>Increase Value</label>
<description>The value that represents INCREASE</description>
</parameter>
<parameter name="decreaseValue" type="text">
<label>Decrease Value</label>
<description>The value that represents DECREASE</description>
</parameter>
<parameter name="step" type="text">
<label>Increase/Decrease Step</label>
<description>The value by which the current brightness is increased/decreased if the corresponding command is
received</description>
<default>1</default>
</parameter>
<parameter name="colorMode" type="text">
<label>Color Mode</label>
<description>Color mode for parsing incoming and sending outgoing values</description>
<options>
<option value="HSB">HSB</option>
<option value="RGB">RGB</option>
</options>
<limitToOptions>true</limitToOptions>
<default>RGB</default>
</parameter>
<parameter name="mode" type="text">
<label>Read/Write Mode</label>
<options>
<option value="READWRITE">Read/Write</option>
<option value="READONLY">Read Only</option>
<option value="WRITEONLY">Write Only</option>
</options>
<limitToOptions>true</limitToOptions>
<advanced>true</advanced>
<default>READWRITE</default>
</parameter>
</config-description>
<config-description uri="channel-type:http:channel-config-contact">
<parameter name="stateExtension" type="text">
<label>State URL Extension</label>
<description>This value is added to the base URL configured in the thing for retrieving values.</description>
<advanced>true</advanced>
</parameter>
<parameter name="commandExtension" type="text">
<label>Command URL Extension</label>
<description>This value is added to the base URL configured in the thing for sending values.</description>
<advanced>true</advanced>
</parameter>
<parameter name="stateTransformation" type="text">
<label>State Transformation</label>
<description>Transformation pattern used when receiving values.</description>
</parameter>
<parameter name="commandTransformation" type="text">
<label>Command Transformation</label>
<description>Transformation pattern used when sending values.</description>
</parameter>
<parameter name="openValue" type="text" required="true">
<label>Open Value</label>
<description>The value that represents OPEN</description>
</parameter>
<parameter name="closedValue" type="text" required="true">
<label>Closed Value</label>
<description>The value that represents CLOSED</description>
</parameter>
<parameter name="mode" type="text">
<label>Read/Write Mode</label>
<options>
<option value="READWRITE">Read/Write</option>
<option value="READONLY">Read Only</option>
<option value="WRITEONLY">Write Only</option>
</options>
<limitToOptions>true</limitToOptions>
<advanced>true</advanced>
<default>READWRITE</default>
</parameter>
</config-description>
<config-description uri="channel-type:http:channel-config-dimmer">
<parameter name="stateExtension" type="text">
<label>State URL Extension</label>
<description>This value is added to the base URL configured in the thing for retrieving values.</description>
<advanced>true</advanced>
</parameter>
<parameter name="commandExtension" type="text">
<label>Command URL Extension</label>
<description>This value is added to the base URL configured in the thing for sending values.</description>
<advanced>true</advanced>
</parameter>
<parameter name="stateTransformation" type="text">
<label>State Transformation</label>
<description>Transformation pattern used when receiving values.</description>
</parameter>
<parameter name="commandTransformation" type="text">
<label>Command Transformation</label>
<description>Transformation pattern used when sending values.</description>
</parameter>
<parameter name="onValue" type="text">
<label>On Value</label>
<description>The value that represents ON</description>
</parameter>
<parameter name="offValue" type="text">
<label>Off Value</label>
<description>The value that represents OFF</description>
</parameter>
<parameter name="increaseValue" type="text">
<label>Increase Value</label>
<description>The value that represents INCREASE</description>
</parameter>
<parameter name="decreaseValue" type="text">
<label>Decrease Value</label>
<description>The value that represents DECREASE</description>
</parameter>
<parameter name="step" type="text">
<label>Increase/Decrease Step</label>
<description>The value by which the current brightness is increased/decreased if the corresponding command is
received</description>
<default>1</default>
</parameter>
<parameter name="mode" type="text">
<label>Read/Write Mode</label>
<options>
<option value="READWRITE">Read/Write</option>
<option value="READONLY">Read Only</option>
<option value="WRITEONLY">Write Only</option>
</options>
<limitToOptions>true</limitToOptions>
<advanced>true</advanced>
<default>READWRITE</default>
</parameter>
</config-description>
<config-description uri="channel-type:http:channel-config-image">
<parameter name="stateExtension" type="text">
<label>State URL Extension</label>
<description>This value is added to the base URL configured in the thing for retrieving values.</description>
<advanced>true</advanced>
</parameter>
</config-description>
<config-description uri="channel-type:http:channel-config-player">
<parameter name="stateExtension" type="text">
<label>State URL Extension</label>
<description>This value is added to the base URL configured in the thing for retrieving values.</description>
<advanced>true</advanced>
</parameter>
<parameter name="commandExtension" type="text">
<label>Command URL Extension</label>
<description>This value is added to the base URL configured in the thing for sending values.</description>
<advanced>true</advanced>
</parameter>
<parameter name="stateTransformation" type="text">
<label>State Transformation</label>
<description>Transformation pattern used when receiving values.</description>
</parameter>
<parameter name="commandTransformation" type="text">
<label>Command Transformation</label>
<description>Transformation pattern used when sending values.</description>
</parameter>
<parameter name="playValue" type="text">
<label>Play Value</label>
<description>The value that represents PLAY</description>
</parameter>
<parameter name="pauseValue" type="text">
<label>Pause Value</label>
<description>The value that represents PAUSE</description>
</parameter>
<parameter name="nextValue" type="text">
<label>Next Value</label>
<description>The value that represents NEXT</description>
</parameter>
<parameter name="previousValue" type="text">
<label>Previous Value</label>
<description>The value that represents PREVIOUS</description>
</parameter>
<parameter name="rewindValue" type="text">
<label>Rewind Value</label>
<description>The value that represents REWIND</description>
</parameter>
<parameter name="fastforwardValue" type="text">
<label>Fast Forward Value</label>
<description>The value that represents FASTFORWARD</description>
</parameter>
<parameter name="mode" type="text">
<label>Read/Write Mode</label>
<options>
<option value="READWRITE">Read/Write</option>
<option value="READONLY">Read Only</option>
<option value="WRITEONLY">Write Only</option>
</options>
<limitToOptions>true</limitToOptions>
<advanced>true</advanced>
<default>READWRITE</default>
</parameter>
</config-description>
<config-description uri="channel-type:http:channel-config-rollershutter">
<parameter name="stateExtension" type="text">
<label>State URL Extension</label>
<description>This value is added to the base URL configured in the thing for retrieving values.</description>
<advanced>true</advanced>
</parameter>
<parameter name="commandExtension" type="text">
<label>Command URL Extension</label>
<description>This value is added to the base URL configured in the thing for sending values.</description>
<advanced>true</advanced>
</parameter>
<parameter name="stateTransformation" type="text">
<label>State Transformation</label>
<description>Transformation pattern used when receiving values.</description>
</parameter>
<parameter name="commandTransformation" type="text">
<label>Command Transformation</label>
<description>Transformation pattern used when sending values.</description>
</parameter>
<parameter name="upValue" type="text">
<label>Up Value</label>
<description>The value that represents UP</description>
</parameter>
<parameter name="downValue" type="text">
<label>Down Value</label>
<description>The value that represents DOWN</description>
</parameter>
<parameter name="stopValue" type="text">
<label>Stop Value</label>
<description>The value that represents STOP</description>
</parameter>
<parameter name="moveValue" type="text">
<label>Move Value</label>
<description>The value that represents MOVE</description>
</parameter>
<parameter name="mode" type="text">
<label>Read/Write Mode</label>
<options>
<option value="READWRITE">Read/Write</option>
<option value="READONLY">Read Only</option>
<option value="WRITEONLY">Write Only</option>
</options>
<limitToOptions>true</limitToOptions>
<advanced>true</advanced>
<default>READWRITE</default>
</parameter>
</config-description>
<config-description uri="channel-type:http:channel-config-switch">
<parameter name="stateExtension" type="text">
<label>State URL Extension</label>
<description>This value is added to the base URL configured in the thing for retrieving values.</description>
<advanced>true</advanced>
</parameter>
<parameter name="commandExtension" type="text">
<label>Command URL Extension</label>
<description>This value is added to the base URL configured in the thing for sending values.</description>
<advanced>true</advanced>
</parameter>
<parameter name="stateTransformation" type="text">
<label>State Transformation</label>
<description>Transformation pattern used when receiving values.</description>
</parameter>
<parameter name="commandTransformation" type="text">
<label>Command Transformation</label>
<description>Transformation pattern used when sending values.</description>
</parameter>
<parameter name="onValue" type="text" required="true">
<label>On Value</label>
<description>The value that represents ON</description>
</parameter>
<parameter name="offValue" type="text" required="true">
<label>Off Value</label>
<description>The value that represents OFF</description>
</parameter>
<parameter name="mode" type="text">
<label>Read/Write Mode</label>
<options>
<option value="READWRITE">Read/Write</option>
<option value="READONLY">Read Only</option>
<option value="WRITEONLY">Write Only</option>
</options>
<limitToOptions>true</limitToOptions>
<advanced>true</advanced>
<default>READWRITE</default>
</parameter>
</config-description>
</config-description:config-descriptions>

View File

@ -0,0 +1,158 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="http"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
<thing-type id="url"
extensible="color,contact,datetime,dimmer,image,location,number,rollershutter,string,switch">
<label>HTTP URL Thing</label>
<description>Represents a base URL and all associated requests.</description>
<config-description>
<parameter name="baseURL" type="text" required="true">
<label>Base URL</label>
<description>The URL set here can be extended in the channel configuration.</description>
<context>url</context>
</parameter>
<parameter name="refresh" type="integer" unit="s" min="1">
<label>Refresh Time</label>
<description>Time between two refreshes of all channels</description>
<default>30</default>
</parameter>
<parameter name="timeout" type="integer" unit="ms" min="0">
<label>Timeout</label>
<description>The timeout in ms for each request</description>
<default>3000</default>
</parameter>
<parameter name="username" type="text">
<label>Username</label>
<description>Basic Authentication username</description>
<advanced>true</advanced>
</parameter>
<parameter name="password" type="text">
<label>Password</label>
<description>Basic Authentication password</description>
<context>password</context>
<advanced>true</advanced>
</parameter>
<parameter name="authMode" type="text">
<label>Authentication Mode</label>
<options>
<option value="BASIC">Basic Authentication</option>
<option value="DIGEST">Digest Authentication</option>
</options>
<default>BASIC</default>
<limitToOptions>true</limitToOptions>
<advanced>true</advanced>
</parameter>
<parameter name="commandMethod" type="text">
<label>Command Method</label>
<description>HTTP method (GET,POST, PUT) for sending commands.</description>
<options>
<option value="GET">GET</option>
<option value="POST">POST</option>
<option value="PUT">PUT</option>
</options>
<limitToOptions>true</limitToOptions>
<default>GET</default>
<advanced>true</advanced>
</parameter>
<parameter name="contentType" type="text">
<label>Content Type</label>
<description>The MIME content type. Only used for `POST` and `PUT`.</description>
<options>
<option value="application/json">application/json</option>
<option value="application/xml">application/xml</option>
<option value="text/html">text/html</option>
<option value="text/plain">text/plain</option>
<option value="text/xml">text/xml</option>
</options>
<advanced>true</advanced>
</parameter>
<parameter name="encoding" type="text">
<label>Fallback Encoding</label>
<description>Fallback Encoding text received by this thing's channels.</description>
<advanced>true</advanced>
</parameter>
<parameter name="headers" type="text" multiple="true">
<label>Headers</label>
<description>Additional headers send along with the request</description>
<advanced>true</advanced>
</parameter>
<parameter name="ignoreSSLErrors" type="boolean">
<label>Ignore SSL Errors</label>
<description>If set to true ignores invalid SSL certificate errors. This is potentially dangerous.</description>
<default>false</default>
<advanced>true</advanced>
</parameter>
</config-description>
</thing-type>
<channel-type id="color">
<item-type>Color</item-type>
<label>Color Channel</label>
<config-description-ref uri="channel-type:http:channel-config-color"/>
</channel-type>
<channel-type id="contact">
<item-type>Contact</item-type>
<label>Contact Channel</label>
<config-description-ref uri="channel-type:http:channel-config-contact"/>
</channel-type>
<channel-type id="datetime">
<item-type>DateTime</item-type>
<label>DateTime Channel</label>
<config-description-ref uri="channel-type:http:channel-config"/>
</channel-type>
<channel-type id="dimmer">
<item-type>Dimmer</item-type>
<label>Dimmer Channel</label>
<config-description-ref uri="channel-type:http:channel-config-dimmer"/>
</channel-type>
<channel-type id="image">
<item-type>Image</item-type>
<label>Image Channel</label>
<config-description-ref uri="channel-type:http:channel-config-image"/>
</channel-type>
<channel-type id="location">
<item-type>Location</item-type>
<label>Location Channel</label>
<config-description-ref uri="channel-type:http:channel-config"/>
</channel-type>
<channel-type id="number">
<item-type>Number</item-type>
<label>Number Channel</label>
<config-description-ref uri="channel-type:http:channel-config"/>
</channel-type>
<channel-type id="player">
<item-type>Player</item-type>
<label>Player Channel</label>
<config-description-ref uri="channel-type:http:channel-config-player"/>
</channel-type>
<channel-type id="rollershutter">
<item-type>Rollershutter</item-type>
<label>Rollershutter Channel</label>
<config-description-ref uri="channel-type:http:channel-config-rollershutter"/>
</channel-type>
<channel-type id="string">
<item-type>String</item-type>
<label>String Channel</label>
<config-description-ref uri="channel-type:http:channel-config"/>
</channel-type>
<channel-type id="switch">
<item-type>Switch</item-type>
<label>Switch Channel</label>
<config-description-ref uri="channel-type:http:channel-config-switch"/>
</channel-type>
</thing:thing-descriptions>

View File

@ -0,0 +1,68 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.http.internal.converter;
import java.util.function.Function;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.openhab.binding.http.internal.config.HttpChannelConfig;
import org.openhab.binding.http.internal.transform.NoOpValueTransformation;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.PointType;
import org.openhab.core.library.types.StringType;
import org.openhab.core.types.Command;
import org.openhab.core.types.State;
/**
* The {@link ConverterTest} is a test class for state converters
*
* @author Jan N. Klug - Initial contribution
*/
@NonNullByDefault
public class ConverterTest {
@Test
public void stringTypeConverter() {
GenericItemConverter converter = createConverter(StringType::new);
Assertions.assertEquals(new StringType("Test"), converter.toState("Test"));
}
@Test
public void decimalTypeConverter() {
GenericItemConverter converter = createConverter(DecimalType::new);
Assertions.assertEquals(new DecimalType(15.6), converter.toState("15.6"));
}
@Test
public void pointTypeConverter() {
GenericItemConverter converter = createConverter(PointType::new);
Assertions.assertEquals(new PointType(new DecimalType(51.1), new DecimalType(7.2), new DecimalType(100)),
converter.toState("51.1, 7.2, 100"));
}
private void sendHttpValue(String value) {
}
private void updateState(State state) {
}
public void postCommand(Command command) {
}
public GenericItemConverter createConverter(Function<String, State> fcn) {
return new GenericItemConverter(fcn, this::updateState, this::postCommand, this::sendHttpValue,
NoOpValueTransformation.getInstance(), NoOpValueTransformation.getInstance(), new HttpChannelConfig());
}
}

View File

@ -121,6 +121,7 @@
<module>org.openhab.binding.heos</module>
<module>org.openhab.binding.homematic</module>
<module>org.openhab.binding.hpprinter</module>
<module>org.openhab.binding.http</module>
<module>org.openhab.binding.hue</module>
<module>org.openhab.binding.hydrawise</module>
<module>org.openhab.binding.hyperion</module>