mirror of
https://github.com/openhab/openhab-addons.git
synced 2025-01-25 14:55:55 +01:00
[http] Improve binding (#16282)
This adds many improvements, new features and contains bugfixes. Signed-off-by: Jan N. Klug <github@klug.nrw> Signed-off-by: Ciprian Pascu <contact@ciprianpascu.ro>
This commit is contained in:
parent
1b5eed74d1
commit
811bc00a5a
@ -9,24 +9,26 @@ 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. |
|
||||
| `bufferSize` | no | 2048 | The buffer size for the response data (in kB). |
|
||||
| `delay` | no | 0 | Delay between two requests in ms (advanced parameter). |
|
||||
| `username` | yes | - | Username for authentication (advanced parameter). |
|
||||
| `password` | yes | - | Password for authentication (advanced parameter). |
|
||||
| `authMode` | no | BASIC | Authentication mode, `BASIC`, `BASIC_PREEMPTIVE` or `DIGEST` (advanced parameter). |
|
||||
| `stateMethod` | no | GET | Method used for requesting the state: `GET`, `PUT`, `POST`. |
|
||||
| `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". Multiple values can be stored as `headers="key1=value1", "key2=value2", "key3=value3",`. When using text based configuration include at minimum 2 headers to avoid parsing errors.|
|
||||
| `ignoreSSLErrors` | no | false | If set to true ignores invalid SSL certificate errors. This is potentially dangerous.|
|
||||
| parameter | optional | default | description |
|
||||
|-----------------------|----------|---------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `baseURL` | no | - | The base URL (including protocol `http://` or `https://`) 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. |
|
||||
| `bufferSize` | no | 2048 | The buffer size for the response data (in kB). |
|
||||
| `delay` | no | 0 | Delay between two requests in ms (advanced parameter). |
|
||||
| `username` | yes | - | Username for authentication (advanced parameter). |
|
||||
| `password` | yes | - | Password for authentication (advanced parameter). Also used for the authentication token when using `TOKEN` authentication. |
|
||||
| `authMode` | no | BASIC | Authentication mode, `BASIC`, `BASIC_PREEMPTIVE`, `TOKEN` or `DIGEST` (advanced parameter). |
|
||||
| `stateMethod` | no | GET | Method used for requesting the state: `GET`, `PUT`, `POST`. |
|
||||
| `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". Multiple values can be stored as `headers="key1=value1", "key2=value2", "key3=value3",` |
|
||||
| `ignoreSSLErrors` | no | false | If set to true, ignores invalid SSL certificate errors. This is potentially dangerous. |
|
||||
| `strictErrorHandling` | no | false | If set to true, thing status is changed depending on last request result (failed = `OFFLINE`). Failed requests result in `UNDEF` for channel values. |
|
||||
| `userAgent` | yes | (yes ) | Sets a custom user agent (default is "Jetty/version", e.g. "Jetty/9.4.20.v20190813"). |
|
||||
|
||||
_Note:_ Optional "no" means that you have to configure a value unless a default is provided and you are ok with that setting.
|
||||
_Note:_ Optional "no" means that you have to configure a value unless a default is provided, and you are ok with that setting.
|
||||
|
||||
_Note:_ The `BASIC_PREEMPTIVE` mode adds basic authentication headers even if the server did not request authentication.
|
||||
This is dangerous and might be misused.
|
||||
@ -35,30 +37,29 @@ Authentication might fail if redirections are involved as headers are stripper p
|
||||
|
||||
_Note:_ If you rate-limit requests by using the `delay` parameter you have to make sure that the time between two refreshes is larger than the time needed for one refresh cycle.
|
||||
|
||||
**Attention:** `baseUrl` (and `stateExtension`/`commandExtension`) should not normally use escaping (e.g. `%22` instead of `"` or `%2c` instead of `,`).
|
||||
**Attention:** `baseUrl` (and `stateExtension`/`commandExtension`) should not use escaping (e.g. `%22` instead of `"` or `%2c` instead of `,`).
|
||||
URLs are properly escaped by the binding itself before the request is sent.
|
||||
Using escaped strings in URL parameters may lead to problems with the formatting (see below).
|
||||
|
||||
In certain scenarios you may need to manually escape your URL, for example if you need to include an escaped `=` (`%3D`) in this scenario include `%%3D` in the URL to preserve the `%` during formatting, and set the parameter `escapedUrl` to true on the channel.
|
||||
|
||||
## Channels
|
||||
|
||||
The thing has two channels of type `request-date-time` which provide the timestamp of the last successful (`last-success`) and last failed (`last-failure`) request.
|
||||
|
||||
Additionally, the thing can be extended with data 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`, `stateContent` and `escapedUrl` only.
|
||||
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. |
|
||||
| `escapedUrl` | yes | - | This specifies whether the URL is already escaped. |
|
||||
| `stateContent` | yes | - | Content for state requests (if method is `PUT` or `POST`) |
|
||||
| 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. |
|
||||
| `stateContent` | yes | - | Content for state requests (if method is `PUT` or `POST`) |
|
||||
| `mode` | no | `READWRITE` | Mode this channel is allowed to operate. `READONLY` means receive state, `WRITEONLY` means send commands. |
|
||||
|
||||
Transformations need to be specified in the same format as
|
||||
Some channels have additional parameters.
|
||||
When concatenating the `baseURL` and `stateExtension` or `commandExtension` the binding checks if a proper URL part separator (`/`, `&` or `?`) is present and adds a `/` if missing.
|
||||
|
||||
@ -80,41 +81,41 @@ The same mechanism works for commands (`commandTransformation`) for outgoing val
|
||||
|
||||
### `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` |
|
||||
| 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` |
|
||||
| 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` |
|
||||
| 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.
|
||||
|
||||
### `number`
|
||||
|
||||
| parameter | optional | default | description |
|
||||
|-------------------------|----------|-------------|-------------|
|
||||
| `unit` | yes | - | The unit label for this channel |
|
||||
| parameter | optional | default | description |
|
||||
|-----------|----------|---------|---------------------------------|
|
||||
| `unit` | yes | - | The unit label for this channel |
|
||||
|
||||
`number` channels can be used for `DecimalType` or `QuantityType` values.
|
||||
If a unit is given in the `unit` parameter, the binding tries to create a `QuantityType` state before updating the channel, if no unit is present, it creates a `DecimalType`.
|
||||
@ -122,38 +123,38 @@ Please note that incompatible units (e.g. `°C` for a `Number:Density` item) wil
|
||||
|
||||
### `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` |
|
||||
| parameter | optional | default | description |
|
||||
|--------------------|----------|---------|-----------------------------------------------|
|
||||
| `playValue` | yes | - | A special value that represents `PLAY` |
|
||||
| `pauseValue` | yes | - | A special value that represents `PAUSE` |
|
||||
| `nextValue` | yes | - | A special value that represents `NEXT` |
|
||||
| `previousValue` | yes | - | A special value that represents `PREVIOUS` |
|
||||
| `fastforwardValue` | yes | - | A special value that represents `FASTFORWARD` |
|
||||
| `rewindValue` | 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` |
|
||||
| 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` |
|
||||
| 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](https://docs.oracle.com/javase/6/docs/api/java/util/Formatter.html).
|
||||
After concatenation of the `baseURL` and the `commandExtension` or the `stateExtension` (if provided) the URL is formatted using the [java.util.Formatter](https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/Formatter.html).
|
||||
The URL is used as format string and two parameters are added:
|
||||
|
||||
- the current date (referenced as `%1$`)
|
||||
@ -163,13 +164,13 @@ 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, for a timestamp `t`).
|
||||
When sending an OFF command on 2020-07-06, the URL
|
||||
|
||||
```text
|
||||
http://www.domain.org/home/lights/23871/?status=%2$s&date=%1$tY-%1$tm-%1$td
|
||||
```
|
||||
http://www.domain.org/home/lights/23871/?status=%2$s&date=%1$tY-%1$tm-%1$td
|
||||
```
|
||||
|
||||
is transformed to
|
||||
|
||||
```text
|
||||
```
|
||||
http://www.domain.org/home/lights/23871/?status=OFF&date=2020-07-06
|
||||
```
|
||||
|
||||
@ -179,10 +180,10 @@ http://www.domain.org/home/lights/23871/?status=OFF&date=2020-07-06
|
||||
|
||||
```java
|
||||
Thing http:url:foo "Foo" [
|
||||
baseURL="https://example.com/api/v1/metadata-api/web/metadata",
|
||||
baseURL="https://example.com/api/v1/metadata-api/web/metadata",
|
||||
headers="key1=value1", "key2=value2", "key3=value3",
|
||||
refresh=15] {
|
||||
Channels:
|
||||
Type string : text "Text" [ stateTransformation="JSONPATH:$.metadata.data" ]
|
||||
Type string : text "Text" [ stateTransformation="JSONPATH:$.metadata.data" ]
|
||||
}
|
||||
```
|
||||
|
@ -14,4 +14,47 @@
|
||||
|
||||
<name>openHAB Add-ons :: Bundles :: HTTP Binding</name>
|
||||
|
||||
<properties>
|
||||
<jetty.version>9.4.52.v20230823</jetty.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.eclipse.jetty</groupId>
|
||||
<artifactId>jetty-server</artifactId>
|
||||
<version>${jetty.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.eclipse.jetty</groupId>
|
||||
<artifactId>jetty-servlet</artifactId>
|
||||
<version>${jetty.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.eclipse.jetty</groupId>
|
||||
<artifactId>jetty-servlets</artifactId>
|
||||
<version>${jetty.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.eclipse.jetty</groupId>
|
||||
<artifactId>jetty-proxy</artifactId>
|
||||
<version>${jetty.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.eclipse.jetty</groupId>
|
||||
<artifactId>jetty-webapp</artifactId>
|
||||
<version>${jetty.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<!-- testing, we need to exclude and declare jetty bundles because the declared transitive dependency 9.2.28 is too old -->
|
||||
<dependency>
|
||||
<groupId>com.github.tomakehurst</groupId>
|
||||
<artifactId>wiremock</artifactId>
|
||||
<version>2.27.2</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
|
@ -14,6 +14,7 @@ package org.openhab.binding.http.internal;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.core.thing.ThingTypeUID;
|
||||
import org.openhab.core.thing.type.ChannelTypeUID;
|
||||
|
||||
/**
|
||||
* The {@link HttpBindingConstants} class defines common constants, which are
|
||||
@ -23,8 +24,12 @@ import org.openhab.core.thing.ThingTypeUID;
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class HttpBindingConstants {
|
||||
|
||||
public static final String BINDING_ID = "http";
|
||||
private static final String BINDING_ID = "http";
|
||||
|
||||
public static final ThingTypeUID THING_TYPE_URL = new ThingTypeUID(BINDING_ID, "url");
|
||||
|
||||
public static final ChannelTypeUID REQUEST_DATE_TIME_CHANNELTYPE_UID = new ChannelTypeUID(BINDING_ID,
|
||||
"request-date-time");
|
||||
public static final String CHANNEL_LAST_SUCCESS = "last-success";
|
||||
public static final String CHANNEL_LAST_FAILURE = "last-failure";
|
||||
}
|
||||
|
@ -12,7 +12,7 @@
|
||||
*/
|
||||
package org.openhab.binding.http.internal;
|
||||
|
||||
import static org.openhab.binding.http.internal.HttpBindingConstants.*;
|
||||
import static org.openhab.binding.http.internal.HttpBindingConstants.THING_TYPE_URL;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
@ -20,17 +20,13 @@ 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.i18n.TimeZoneProvider;
|
||||
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;
|
||||
@ -46,8 +42,7 @@ import org.slf4j.LoggerFactory;
|
||||
*/
|
||||
@NonNullByDefault
|
||||
@Component(configurationPid = "binding.http", service = ThingHandlerFactory.class)
|
||||
public class HttpHandlerFactory extends BaseThingHandlerFactory
|
||||
implements ValueTransformationProvider, HttpClientProvider {
|
||||
public class HttpHandlerFactory extends BaseThingHandlerFactory implements HttpClientProvider {
|
||||
private static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Set.of(THING_TYPE_URL);
|
||||
private final Logger logger = LoggerFactory.getLogger(HttpHandlerFactory.class);
|
||||
|
||||
@ -55,22 +50,27 @@ public class HttpHandlerFactory extends BaseThingHandlerFactory
|
||||
private final HttpClient insecureClient;
|
||||
|
||||
private final HttpDynamicStateDescriptionProvider httpDynamicStateDescriptionProvider;
|
||||
private final TimeZoneProvider timeZoneProvider;
|
||||
|
||||
@Activate
|
||||
public HttpHandlerFactory(@Reference HttpClientFactory httpClientFactory,
|
||||
@Reference HttpDynamicStateDescriptionProvider httpDynamicStateDescriptionProvider) {
|
||||
this.secureClient = httpClientFactory.createHttpClient(BINDING_ID + "-secure", new SslContextFactory.Client());
|
||||
this.insecureClient = httpClientFactory.createHttpClient(BINDING_ID + "-insecure",
|
||||
new SslContextFactory.Client(true));
|
||||
@Reference HttpDynamicStateDescriptionProvider httpDynamicStateDescriptionProvider,
|
||||
@Reference TimeZoneProvider timeZoneProvider) {
|
||||
this.secureClient = new HttpClient(new SslContextFactory.Client());
|
||||
this.insecureClient = new HttpClient(new SslContextFactory.Client(true));
|
||||
// clear user agent, this needs to be set later in the thing configuration as additional header
|
||||
this.secureClient.setUserAgentField(null);
|
||||
this.insecureClient.setUserAgentField(null);
|
||||
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");
|
||||
logger.warn("Failed to start http client: {}", e.getMessage());
|
||||
throw new IllegalStateException("Could not create HttpClient", e);
|
||||
}
|
||||
this.httpDynamicStateDescriptionProvider = httpDynamicStateDescriptionProvider;
|
||||
this.timeZoneProvider = timeZoneProvider;
|
||||
}
|
||||
|
||||
@Deactivate
|
||||
@ -94,21 +94,12 @@ public class HttpHandlerFactory extends BaseThingHandlerFactory
|
||||
ThingTypeUID thingTypeUID = thing.getThingTypeUID();
|
||||
|
||||
if (THING_TYPE_URL.equals(thingTypeUID)) {
|
||||
return new HttpThingHandler(thing, this, this, httpDynamicStateDescriptionProvider);
|
||||
return new HttpThingHandler(thing, this, httpDynamicStateDescriptionProvider, timeZoneProvider);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ValueTransformation getValueTransformation(@Nullable String pattern) {
|
||||
if (pattern == null || pattern.isEmpty()) {
|
||||
return NoOpValueTransformation.getInstance();
|
||||
}
|
||||
return new CascadedValueTransformationImpl(pattern,
|
||||
name -> TransformationHelper.getTransformationService(bundleContext, name));
|
||||
}
|
||||
|
||||
@Override
|
||||
public HttpClient getSecureClient() {
|
||||
return secureClient;
|
||||
|
@ -12,13 +12,20 @@
|
||||
*/
|
||||
package org.openhab.binding.http.internal;
|
||||
|
||||
import static org.openhab.binding.http.internal.HttpBindingConstants.CHANNEL_LAST_FAILURE;
|
||||
import static org.openhab.binding.http.internal.HttpBindingConstants.CHANNEL_LAST_SUCCESS;
|
||||
import static org.openhab.binding.http.internal.HttpBindingConstants.REQUEST_DATE_TIME_CHANNELTYPE_UID;
|
||||
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.time.Instant;
|
||||
import java.util.Base64;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
@ -26,33 +33,18 @@ 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.NumberItemConverter;
|
||||
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.HttpStatusListener;
|
||||
import org.openhab.binding.http.internal.http.RateLimitedHttpClient;
|
||||
import org.openhab.binding.http.internal.http.RefreshingUrlCache;
|
||||
import org.openhab.binding.http.internal.transform.ValueTransformationProvider;
|
||||
import org.openhab.core.i18n.TimeZoneProvider;
|
||||
import org.openhab.core.library.types.DateTimeType;
|
||||
import org.openhab.core.library.types.PointType;
|
||||
import org.openhab.core.library.types.StringType;
|
||||
@ -62,6 +54,19 @@ 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.thing.binding.generic.ChannelHandler;
|
||||
import org.openhab.core.thing.binding.generic.ChannelHandlerContent;
|
||||
import org.openhab.core.thing.binding.generic.ChannelMode;
|
||||
import org.openhab.core.thing.binding.generic.ChannelTransformation;
|
||||
import org.openhab.core.thing.binding.generic.converter.ColorChannelHandler;
|
||||
import org.openhab.core.thing.binding.generic.converter.DimmerChannelHandler;
|
||||
import org.openhab.core.thing.binding.generic.converter.FixedValueMappingChannelHandler;
|
||||
import org.openhab.core.thing.binding.generic.converter.GenericChannelHandler;
|
||||
import org.openhab.core.thing.binding.generic.converter.ImageChannelHandler;
|
||||
import org.openhab.core.thing.binding.generic.converter.NumberChannelHandler;
|
||||
import org.openhab.core.thing.binding.generic.converter.PlayerChannelHandler;
|
||||
import org.openhab.core.thing.binding.generic.converter.RollershutterChannelHandler;
|
||||
import org.openhab.core.thing.internal.binding.generic.converter.AbstractTransformingChannelHandler;
|
||||
import org.openhab.core.types.Command;
|
||||
import org.openhab.core.types.RefreshType;
|
||||
import org.openhab.core.types.State;
|
||||
@ -77,35 +82,33 @@ import org.slf4j.LoggerFactory;
|
||||
* @author Jan N. Klug - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class HttpThingHandler extends BaseThingHandler {
|
||||
public class HttpThingHandler extends BaseThingHandler implements HttpStatusListener {
|
||||
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 RateLimitedHttpClient rateLimitedHttpClient;
|
||||
private final RateLimitedHttpClient rateLimitedHttpClient;
|
||||
private final HttpDynamicStateDescriptionProvider httpDynamicStateDescriptionProvider;
|
||||
private final TimeZoneProvider timeZoneProvider;
|
||||
|
||||
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, ChannelHandler> channels = new HashMap<>();
|
||||
private final Map<ChannelUID, String> channelUrls = new HashMap<>();
|
||||
|
||||
public HttpThingHandler(Thing thing, HttpClientProvider httpClientProvider,
|
||||
ValueTransformationProvider valueTransformationProvider,
|
||||
HttpDynamicStateDescriptionProvider httpDynamicStateDescriptionProvider) {
|
||||
HttpDynamicStateDescriptionProvider httpDynamicStateDescriptionProvider,
|
||||
TimeZoneProvider timeZoneProvider) {
|
||||
super(thing);
|
||||
this.httpClientProvider = httpClientProvider;
|
||||
this.httpClient = httpClientProvider.getSecureClient();
|
||||
this.rateLimitedHttpClient = new RateLimitedHttpClient(httpClient, scheduler);
|
||||
this.valueTransformationProvider = valueTransformationProvider;
|
||||
this.rateLimitedHttpClient = new RateLimitedHttpClient(httpClientProvider.getSecureClient(), scheduler);
|
||||
this.httpDynamicStateDescriptionProvider = httpDynamicStateDescriptionProvider;
|
||||
this.timeZoneProvider = timeZoneProvider;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleCommand(ChannelUID channelUID, Command command) {
|
||||
ItemValueConverter itemValueConverter = channels.get(channelUID);
|
||||
ChannelHandler itemValueConverter = channels.get(channelUID);
|
||||
if (itemValueConverter == null) {
|
||||
logger.warn("Cannot find channel implementation for channel {}.", channelUID);
|
||||
return;
|
||||
@ -117,7 +120,11 @@ public class HttpThingHandler extends BaseThingHandler {
|
||||
RefreshingUrlCache refreshingUrlCache = urlHandlers.get(key);
|
||||
if (refreshingUrlCache != null) {
|
||||
try {
|
||||
refreshingUrlCache.get().ifPresent(itemValueConverter::process);
|
||||
refreshingUrlCache.get().ifPresentOrElse(itemValueConverter::process, () -> {
|
||||
if (config.strictErrorHandling) {
|
||||
itemValueConverter.process(null);
|
||||
}
|
||||
});
|
||||
} catch (IllegalArgumentException | IllegalStateException e) {
|
||||
logger.warn("Failed processing REFRESH command for channel {}: {}", channelUID, e.getMessage());
|
||||
}
|
||||
@ -144,40 +151,68 @@ public class HttpThingHandler extends BaseThingHandler {
|
||||
return;
|
||||
}
|
||||
|
||||
// check protocol is set
|
||||
if (!config.baseURL.startsWith("http://") && !config.baseURL.startsWith("https://")) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
|
||||
"baseURL is invalid: protocol not defined.");
|
||||
return;
|
||||
}
|
||||
|
||||
// check SSL handling and initialize client
|
||||
if (config.ignoreSSLErrors) {
|
||||
logger.info("Using the insecure client for thing '{}'.", thing.getUID());
|
||||
httpClient = httpClientProvider.getInsecureClient();
|
||||
rateLimitedHttpClient.setHttpClient(httpClientProvider.getInsecureClient());
|
||||
} else {
|
||||
logger.info("Using the secure client for thing '{}'.", thing.getUID());
|
||||
httpClient = httpClientProvider.getSecureClient();
|
||||
rateLimitedHttpClient.setHttpClient(httpClientProvider.getSecureClient());
|
||||
}
|
||||
rateLimitedHttpClient.setHttpClient(httpClient);
|
||||
rateLimitedHttpClient.setDelay(config.delay);
|
||||
|
||||
int channelCount = thing.getChannels().size();
|
||||
if (channelCount * config.delay > config.refresh * 1000) {
|
||||
int urlHandlerCount = urlHandlers.size();
|
||||
if (urlHandlerCount * config.delay > config.refresh * 1000) {
|
||||
// this should prevent the rate limit queue from filling up
|
||||
config.refresh = (channelCount * config.delay) / 1000 + 1;
|
||||
config.refresh = (urlHandlerCount * config.delay) / 1000 + 1;
|
||||
logger.warn(
|
||||
"{} channels in thing {} with a delay of {} incompatible with the configured refresh time. Refresh-Time increased to the minimum of {}",
|
||||
channelCount, thing.getUID(), config.delay, config.refresh);
|
||||
urlHandlerCount, thing.getUID(), config.delay, config.refresh);
|
||||
}
|
||||
|
||||
// remove empty headers
|
||||
config.headers.removeIf(String::isBlank);
|
||||
|
||||
// configure authentication
|
||||
if (!config.username.isEmpty()) {
|
||||
try {
|
||||
AuthenticationStore authStore = httpClient.getAuthenticationStore();
|
||||
URI uri = new URI(config.baseURL);
|
||||
try {
|
||||
AuthenticationStore authStore = rateLimitedHttpClient.getAuthenticationStore();
|
||||
URI uri = new URI(config.baseURL);
|
||||
|
||||
// clear old auths if available
|
||||
Authentication.Result authResult = authStore.findAuthenticationResult(uri);
|
||||
if (authResult != null) {
|
||||
authStore.removeAuthenticationResult(authResult);
|
||||
}
|
||||
for (String authType : List.of("Basic", "Digest")) {
|
||||
Authentication authentication = authStore.findAuthentication(authType, uri, Authentication.ANY_REALM);
|
||||
if (authentication != null) {
|
||||
authStore.removeAuthentication(authentication);
|
||||
}
|
||||
}
|
||||
|
||||
if (!config.username.isEmpty() || !config.password.isEmpty()) {
|
||||
switch (config.authMode) {
|
||||
case BASIC_PREEMPTIVE:
|
||||
config.headers.add("Authorization=Basic " + Base64.getEncoder()
|
||||
.encodeToString((config.username + ":" + config.password).getBytes()));
|
||||
logger.debug("Preemptive Basic Authentication configured for thing '{}'", thing.getUID());
|
||||
break;
|
||||
case TOKEN:
|
||||
if (!config.password.isEmpty()) {
|
||||
config.headers.add("Authorization=Bearer " + config.password);
|
||||
logger.debug("Token/Bearer Authentication configured for thing '{}'", thing.getUID());
|
||||
} else {
|
||||
logger.warn("Token/Bearer Authentication configured for thing '{}' but token is empty!",
|
||||
thing.getUID());
|
||||
}
|
||||
break;
|
||||
case BASIC:
|
||||
authStore.addAuthentication(new BasicAuthentication(uri, Authentication.ANY_REALM,
|
||||
config.username, config.password));
|
||||
@ -192,18 +227,16 @@ public class HttpThingHandler extends BaseThingHandler {
|
||||
logger.warn("Unknown authentication method '{}' for thing '{}'", config.authMode,
|
||||
thing.getUID());
|
||||
}
|
||||
} 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());
|
||||
}
|
||||
} else {
|
||||
logger.debug("No authentication configured for thing '{}'", thing.getUID());
|
||||
} catch (URISyntaxException e) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Cannot create URI from baseUrl.");
|
||||
}
|
||||
|
||||
// create channels
|
||||
thing.getChannels().forEach(this::createChannel);
|
||||
|
||||
updateStatus(ThingStatus.ONLINE);
|
||||
updateStatus(ThingStatus.UNKNOWN);
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -229,6 +262,10 @@ public class HttpThingHandler extends BaseThingHandler {
|
||||
* @param channel a thing channel
|
||||
*/
|
||||
private void createChannel(Channel channel) {
|
||||
if (REQUEST_DATE_TIME_CHANNELTYPE_UID.equals(channel.getChannelTypeUID())) {
|
||||
// do not generate refreshUrls for lastSuccess / lastFailure channels
|
||||
return;
|
||||
}
|
||||
ChannelUID channelUID = channel.getUID();
|
||||
HttpChannelConfig channelConfig = channel.getConfiguration().as(HttpChannelConfig.class);
|
||||
|
||||
@ -242,45 +279,46 @@ public class HttpThingHandler extends BaseThingHandler {
|
||||
return;
|
||||
}
|
||||
|
||||
ItemValueConverter itemValueConverter;
|
||||
ChannelHandler itemValueConverter;
|
||||
switch (acceptedItemType) {
|
||||
case "Color":
|
||||
itemValueConverter = createItemConverter(ColorItemConverter::new, commandUrl, channelUID,
|
||||
itemValueConverter = createChannelHandler(ColorChannelHandler::new, commandUrl, channelUID,
|
||||
channelConfig);
|
||||
break;
|
||||
case "DateTime":
|
||||
itemValueConverter = createGenericItemConverter(commandUrl, channelUID, channelConfig,
|
||||
itemValueConverter = createGenericChannelHandler(commandUrl, channelUID, channelConfig,
|
||||
DateTimeType::new);
|
||||
break;
|
||||
case "Dimmer":
|
||||
itemValueConverter = createItemConverter(DimmerItemConverter::new, commandUrl, channelUID,
|
||||
itemValueConverter = createChannelHandler(DimmerChannelHandler::new, commandUrl, channelUID,
|
||||
channelConfig);
|
||||
break;
|
||||
case "Contact":
|
||||
case "Switch":
|
||||
itemValueConverter = createItemConverter(FixedValueMappingItemConverter::new, commandUrl, channelUID,
|
||||
itemValueConverter = createChannelHandler(FixedValueMappingChannelHandler::new, commandUrl, channelUID,
|
||||
channelConfig);
|
||||
break;
|
||||
case "Image":
|
||||
itemValueConverter = new ImageItemConverter(state -> updateState(channelUID, state));
|
||||
itemValueConverter = new ImageChannelHandler(state -> updateState(channelUID, state));
|
||||
break;
|
||||
case "Location":
|
||||
itemValueConverter = createGenericItemConverter(commandUrl, channelUID, channelConfig, PointType::new);
|
||||
itemValueConverter = createGenericChannelHandler(commandUrl, channelUID, channelConfig, PointType::new);
|
||||
break;
|
||||
case "Number":
|
||||
itemValueConverter = createItemConverter(NumberItemConverter::new, commandUrl, channelUID,
|
||||
itemValueConverter = createChannelHandler(NumberChannelHandler::new, commandUrl, channelUID,
|
||||
channelConfig);
|
||||
break;
|
||||
case "Player":
|
||||
itemValueConverter = createItemConverter(PlayerItemConverter::new, commandUrl, channelUID,
|
||||
itemValueConverter = createChannelHandler(PlayerChannelHandler::new, commandUrl, channelUID,
|
||||
channelConfig);
|
||||
break;
|
||||
case "Rollershutter":
|
||||
itemValueConverter = createItemConverter(RollershutterItemConverter::new, commandUrl, channelUID,
|
||||
itemValueConverter = createChannelHandler(RollershutterChannelHandler::new, commandUrl, channelUID,
|
||||
channelConfig);
|
||||
break;
|
||||
case "String":
|
||||
itemValueConverter = createGenericItemConverter(commandUrl, channelUID, channelConfig, StringType::new);
|
||||
itemValueConverter = createGenericChannelHandler(commandUrl, channelUID, channelConfig,
|
||||
StringType::new);
|
||||
break;
|
||||
default:
|
||||
logger.warn("Unsupported item-type '{}'", channel.getAcceptedItemType());
|
||||
@ -288,80 +326,75 @@ public class HttpThingHandler extends BaseThingHandler {
|
||||
}
|
||||
|
||||
channels.put(channelUID, itemValueConverter);
|
||||
if (channelConfig.mode != HttpChannelMode.WRITEONLY) {
|
||||
if (channelConfig.mode != ChannelMode.WRITEONLY) {
|
||||
// we need a key consisting of stateContent and URL, only if both are equal, we can use the same cache
|
||||
String key = channelConfig.stateContent + "$" + stateUrl;
|
||||
channelUrls.put(channelUID, key);
|
||||
urlHandlers
|
||||
.computeIfAbsent(key,
|
||||
k -> new RefreshingUrlCache(scheduler, rateLimitedHttpClient, stateUrl,
|
||||
channelConfig.escapedUrl, config, channelConfig.stateContent))
|
||||
Objects.requireNonNull(urlHandlers.computeIfAbsent(key,
|
||||
k -> new RefreshingUrlCache(scheduler, rateLimitedHttpClient, stateUrl, config,
|
||||
channelConfig.stateContent, config.contentType, this)))
|
||||
.addConsumer(itemValueConverter::process);
|
||||
}
|
||||
|
||||
StateDescription stateDescription = StateDescriptionFragmentBuilder.create()
|
||||
.withReadOnly(channelConfig.mode == HttpChannelMode.READONLY).build().toStateDescription();
|
||||
.withReadOnly(channelConfig.mode == ChannelMode.READONLY).build().toStateDescription();
|
||||
if (stateDescription != null) {
|
||||
// if the state description is not available, we don't need to add it
|
||||
httpDynamicStateDescriptionProvider.setDescription(channelUID, stateDescription);
|
||||
}
|
||||
}
|
||||
|
||||
private void sendHttpValue(String commandUrl, boolean escapedUrl, String command) {
|
||||
sendHttpValue(commandUrl, escapedUrl, command, false);
|
||||
@Override
|
||||
public void onHttpError(@Nullable String message) {
|
||||
updateState(CHANNEL_LAST_FAILURE, new DateTimeType(Instant.now().atZone(timeZoneProvider.getTimeZone())));
|
||||
if (config.strictErrorHandling) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
|
||||
Objects.requireNonNullElse(message, ""));
|
||||
}
|
||||
}
|
||||
|
||||
private void sendHttpValue(String commandUrl, boolean escapedUrl, String command, boolean isRetry) {
|
||||
@Override
|
||||
public void onHttpSuccess() {
|
||||
updateState(CHANNEL_LAST_SUCCESS, new DateTimeType(Instant.now().atZone(timeZoneProvider.getTimeZone())));
|
||||
updateStatus(ThingStatus.ONLINE);
|
||||
}
|
||||
|
||||
private void sendHttpValue(String commandUrl, String command) {
|
||||
sendHttpValue(commandUrl, command, false);
|
||||
}
|
||||
|
||||
private void sendHttpValue(String commandUrl, String command, boolean isRetry) {
|
||||
try {
|
||||
// format URL
|
||||
String url = String.format(commandUrl, new Date(), command);
|
||||
URI uri = escapedUrl ? new URI(url) : Util.uriFromString(url);
|
||||
URI uri = Util.uriFromString(String.format(commandUrl, new Date(), command));
|
||||
|
||||
// build request
|
||||
Request request = httpClient.newRequest(uri).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));
|
||||
}
|
||||
}
|
||||
rateLimitedHttpClient.newPriorityRequest(uri, config.commandMethod, command, config.contentType)
|
||||
.thenAccept(request -> {
|
||||
request.timeout(config.timeout, TimeUnit.MILLISECONDS);
|
||||
config.getHeaders().forEach(request::header);
|
||||
|
||||
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);
|
||||
}
|
||||
});
|
||||
CompletableFuture<@Nullable ChannelHandlerContent> responseContentFuture = new CompletableFuture<>();
|
||||
responseContentFuture.exceptionally(t -> {
|
||||
if (t instanceof HttpAuthException) {
|
||||
if (isRetry || !rateLimitedHttpClient.reAuth(uri)) {
|
||||
logger.warn(
|
||||
"Retry after authentication failure failed again for '{}', failing here",
|
||||
uri);
|
||||
onHttpError("Authentication failed");
|
||||
} else {
|
||||
sendHttpValue(commandUrl, command, true);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace("Sending to '{}': {}", uri, 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", uri);
|
||||
} else {
|
||||
AuthenticationStore authStore = httpClient.getAuthenticationStore();
|
||||
Authentication.Result authResult = authStore.findAuthenticationResult(uri);
|
||||
if (authResult != null) {
|
||||
authStore.removeAuthenticationResult(authResult);
|
||||
logger.debug("Cleared authentication result for '{}', retrying immediately", uri);
|
||||
sendHttpValue(commandUrl, escapedUrl, command, true);
|
||||
} else {
|
||||
logger.warn("Could not find authentication result for '{}', failing here", uri);
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace("Sending to '{}': {}", uri, Util.requestToLogString(request));
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
});
|
||||
request.send(new HttpResponseListener(f, null, config.bufferSize));
|
||||
|
||||
request.send(new HttpResponseListener(responseContentFuture, null, config.bufferSize, this));
|
||||
});
|
||||
} catch (IllegalArgumentException | URISyntaxException | MalformedURLException e) {
|
||||
logger.warn("Creating request for '{}' failed: {}", commandUrl, e.getMessage());
|
||||
}
|
||||
@ -380,18 +413,18 @@ public class HttpThingHandler extends BaseThingHandler {
|
||||
}
|
||||
}
|
||||
|
||||
private ItemValueConverter createItemConverter(AbstractTransformingItemConverter.Factory factory, String commandUrl,
|
||||
private ChannelHandler createChannelHandler(AbstractTransformingChannelHandler.Factory factory, String commandUrl,
|
||||
ChannelUID channelUID, HttpChannelConfig channelConfig) {
|
||||
return factory.create(state -> updateState(channelUID, state), command -> postCommand(channelUID, command),
|
||||
command -> sendHttpValue(commandUrl, channelConfig.escapedUrl, command),
|
||||
valueTransformationProvider.getValueTransformation(channelConfig.stateTransformation),
|
||||
valueTransformationProvider.getValueTransformation(channelConfig.commandTransformation), channelConfig);
|
||||
command -> sendHttpValue(commandUrl, command),
|
||||
new ChannelTransformation(channelConfig.stateTransformation),
|
||||
new ChannelTransformation(channelConfig.commandTransformation), channelConfig);
|
||||
}
|
||||
|
||||
private ItemValueConverter createGenericItemConverter(String commandUrl, ChannelUID channelUID,
|
||||
private ChannelHandler createGenericChannelHandler(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);
|
||||
AbstractTransformingChannelHandler.Factory factory = (state, command, value, stateTrans, commandTrans,
|
||||
config) -> new GenericChannelHandler(toState, state, command, value, stateTrans, commandTrans, config);
|
||||
return createChannelHandler(factory, commandUrl, channelUID, channelConfig);
|
||||
}
|
||||
}
|
||||
|
@ -54,13 +54,14 @@ public class Util {
|
||||
* create an URI from a string, escaping all necessary characters
|
||||
*
|
||||
* @param s the URI as unescaped string
|
||||
* @return URI correspondign to the input string
|
||||
* @throws MalformedURLException
|
||||
* @throws URISyntaxException
|
||||
* @return URI corresponding to the input string
|
||||
* @throws MalformedURLException if parameter is not an URL
|
||||
* @throws URISyntaxException if parameter could not be converted to an URI
|
||||
*/
|
||||
public static URI uriFromString(String s) throws MalformedURLException, URISyntaxException {
|
||||
URL url = new URL(s);
|
||||
return new URI(url.getProtocol(), url.getUserInfo(), IDN.toASCII(url.getHost()), url.getPort(), url.getPath(),
|
||||
url.getQuery(), url.getRef());
|
||||
URI uri = new URI(url.getProtocol(), url.getUserInfo(), IDN.toASCII(url.getHost()), url.getPort(),
|
||||
url.getPath(), url.getQuery(), url.getRef());
|
||||
return URI.create(uri.toASCIIString());
|
||||
}
|
||||
}
|
||||
|
@ -23,5 +23,6 @@ import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
public enum HttpAuthMode {
|
||||
BASIC_PREEMPTIVE,
|
||||
BASIC,
|
||||
DIGEST
|
||||
DIGEST,
|
||||
TOKEN
|
||||
}
|
||||
|
@ -12,23 +12,9 @@
|
||||
*/
|
||||
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;
|
||||
import org.openhab.core.thing.binding.generic.ChannelValueConverterConfig;
|
||||
|
||||
/**
|
||||
* The {@link HttpChannelConfig} class contains fields mapping channel configuration parameters.
|
||||
@ -36,107 +22,11 @@ import org.openhab.core.types.State;
|
||||
* @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 class HttpChannelConfig extends ChannelValueConverterConfig {
|
||||
|
||||
public @Nullable String stateExtension;
|
||||
public @Nullable String commandExtension;
|
||||
public @Nullable String stateTransformation;
|
||||
public @Nullable String commandTransformation;
|
||||
public String stateContent = "";
|
||||
public boolean escapedUrl = false;
|
||||
|
||||
public HttpChannelMode mode = HttpChannelMode.READWRITE;
|
||||
|
||||
// number
|
||||
public @Nullable String unit;
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,27 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2024 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
|
||||
}
|
@ -13,10 +13,16 @@
|
||||
package org.openhab.binding.http.internal.config;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.eclipse.jetty.http.HttpHeader;
|
||||
import org.eclipse.jetty.http.HttpMethod;
|
||||
import org.eclipse.jetty.util.Jetty;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* The {@link HttpThingConfig} class contains fields mapping thing configuration parameters.
|
||||
@ -25,6 +31,8 @@ import org.eclipse.jetty.http.HttpMethod;
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class HttpThingConfig {
|
||||
private final Logger logger = LoggerFactory.getLogger(HttpThingConfig.class);
|
||||
|
||||
public String baseURL = "";
|
||||
public int refresh = 30;
|
||||
public int timeout = 3000;
|
||||
@ -43,7 +51,26 @@ public class HttpThingConfig {
|
||||
public @Nullable String contentType = null;
|
||||
|
||||
public boolean ignoreSSLErrors = false;
|
||||
public boolean strictErrorHandling = false;
|
||||
|
||||
// ArrayList is required as implementation because list may be modified later
|
||||
public ArrayList<String> headers = new ArrayList<>();
|
||||
public String userAgent = "";
|
||||
|
||||
public Map<String, String> getHeaders() {
|
||||
Map<String, String> headersMap = new HashMap<>();
|
||||
// add user agent first, in case it is also defined in the headers, it'll be overwritten
|
||||
headersMap.put(HttpHeader.USER_AGENT.asString(),
|
||||
userAgent.isBlank() ? "Jetty/" + Jetty.VERSION : userAgent.trim());
|
||||
headers.forEach(header -> {
|
||||
String[] keyValuePair = header.split("=", 2);
|
||||
if (keyValuePair.length == 2) {
|
||||
headersMap.put(keyValuePair[0].trim(), keyValuePair[1].trim());
|
||||
} else {
|
||||
logger.warn("Splitting header '{}' failed. No '=' was found. Ignoring", header);
|
||||
}
|
||||
});
|
||||
|
||||
return headersMap;
|
||||
}
|
||||
}
|
||||
|
@ -1,108 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2024 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);
|
||||
}
|
||||
}
|
@ -1,140 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2024 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 newState) {
|
||||
state = newState;
|
||||
return hsbToString(newState);
|
||||
} else if (command instanceof PercentType percentCommand && state instanceof HSBType hsb) {
|
||||
HSBType newState = new HSBType(hsb.getHue(), hsb.getSaturation(), percentCommand);
|
||||
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 hsb) {
|
||||
newState = new HSBType(hsb.getHue(), hsb.getSaturation(), PercentType.HUNDRED);
|
||||
} else {
|
||||
newState = HSBType.WHITE;
|
||||
}
|
||||
} else if (string.equals(channelConfig.offValue)) {
|
||||
if (state instanceof HSBType hsb) {
|
||||
newState = new HSBType(hsb.getHue(), hsb.getSaturation(), PercentType.ZERO);
|
||||
} else {
|
||||
newState = HSBType.BLACK;
|
||||
}
|
||||
} else if (string.equals(channelConfig.increaseValue) && state instanceof HSBType hsb) {
|
||||
BigDecimal newBrightness = hsb.getBrightness().toBigDecimal().add(channelConfig.step);
|
||||
if (HUNDRED.compareTo(newBrightness) < 0) {
|
||||
newBrightness = HUNDRED;
|
||||
}
|
||||
newState = new HSBType(hsb.getHue(), hsb.getSaturation(), new PercentType(newBrightness));
|
||||
} else if (string.equals(channelConfig.decreaseValue) && state instanceof HSBType hsb) {
|
||||
BigDecimal newBrightness = hsb.getBrightness().toBigDecimal().subtract(channelConfig.step);
|
||||
if (BigDecimal.ZERO.compareTo(newBrightness) > 0) {
|
||||
newBrightness = BigDecimal.ZERO;
|
||||
}
|
||||
newState = new HSBType(hsb.getHue(), hsb.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(1));
|
||||
int g = Integer.parseInt(matcher.group(2));
|
||||
int b = Integer.parseInt(matcher.group(3));
|
||||
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
|
||||
}
|
||||
}
|
@ -1,103 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2024 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 percentCommand) {
|
||||
return percentCommand.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 brightnessState) {
|
||||
BigDecimal newBrightness = brightnessState.toBigDecimal().add(channelConfig.step);
|
||||
if (HUNDRED.compareTo(newBrightness) < 0) {
|
||||
newBrightness = HUNDRED;
|
||||
}
|
||||
newState = new PercentType(newBrightness);
|
||||
} else if (string.equals(channelConfig.decreaseValue) && state instanceof PercentType brightnessState) {
|
||||
BigDecimal newBrightness = brightnessState.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 (IllegalArgumentException e) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
state = newState;
|
||||
return newState;
|
||||
}
|
||||
}
|
@ -1,62 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2024 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;
|
||||
}
|
||||
}
|
@ -1,61 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2024 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;
|
||||
}
|
||||
|
||||
@Override
|
||||
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;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String toString(Command command) {
|
||||
return command.toString();
|
||||
}
|
||||
}
|
@ -1,48 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2024 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");
|
||||
}
|
||||
}
|
@ -1,41 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2024 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);
|
||||
}
|
@ -1,74 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2024 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.DecimalType;
|
||||
import org.openhab.core.library.types.QuantityType;
|
||||
import org.openhab.core.types.Command;
|
||||
import org.openhab.core.types.State;
|
||||
import org.openhab.core.types.UnDefType;
|
||||
|
||||
/**
|
||||
* The {@link NumberItemConverter} implements {@link org.openhab.core.library.items.NumberItem} conversions
|
||||
*
|
||||
* @author Jan N. Klug - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class NumberItemConverter extends AbstractTransformingItemConverter {
|
||||
|
||||
public NumberItemConverter(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
|
||||
protected State toState(String value) {
|
||||
String trimmedValue = value.trim();
|
||||
if (!trimmedValue.isEmpty()) {
|
||||
try {
|
||||
if (channelConfig.unit != null) {
|
||||
// we have a given unit - use that
|
||||
return new QuantityType<>(trimmedValue + " " + channelConfig.unit);
|
||||
} else {
|
||||
try {
|
||||
// try if we have a simple number
|
||||
return new DecimalType(trimmedValue);
|
||||
} catch (IllegalArgumentException e1) {
|
||||
// not a plain number, maybe with unit?
|
||||
return new QuantityType<>(trimmedValue);
|
||||
}
|
||||
}
|
||||
} catch (IllegalArgumentException e) {
|
||||
// finally failed
|
||||
}
|
||||
}
|
||||
return UnDefType.UNDEF;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String toString(Command command) {
|
||||
return command.toString();
|
||||
}
|
||||
}
|
@ -1,96 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2024 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;
|
||||
private @Nullable String lastCommand; // store last command to prevent duplicate commands
|
||||
|
||||
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(lastCommand)) {
|
||||
// only send commands once
|
||||
return null;
|
||||
}
|
||||
lastCommand = 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) {
|
||||
if (string.equals(channelConfig.playValue)) {
|
||||
return PlayPauseType.PLAY;
|
||||
} else if (string.equals(channelConfig.pauseValue)) {
|
||||
return PlayPauseType.PAUSE;
|
||||
} else if (string.equals(channelConfig.rewindValue)) {
|
||||
return RewindFastforwardType.REWIND;
|
||||
} else if (string.equals(channelConfig.fastforwardValue)) {
|
||||
return RewindFastforwardType.FASTFORWARD;
|
||||
}
|
||||
|
||||
return UnDefType.UNDEF;
|
||||
}
|
||||
}
|
@ -1,101 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2024 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 brightnessState) {
|
||||
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 brightnessState.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;
|
||||
}
|
||||
return new PercentType(value);
|
||||
} catch (NumberFormatException e) {
|
||||
// ignore
|
||||
}
|
||||
|
||||
return UnDefType.UNDEF;
|
||||
}
|
||||
}
|
@ -1,55 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2024 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;
|
||||
}
|
||||
}
|
@ -13,7 +13,6 @@
|
||||
package org.openhab.binding.http.internal.http;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@ -25,6 +24,7 @@ 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.openhab.core.thing.binding.generic.ChannelHandlerContent;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
@ -36,7 +36,8 @@ import org.slf4j.LoggerFactory;
|
||||
@NonNullByDefault
|
||||
public class HttpResponseListener extends BufferingResponseListener {
|
||||
private final Logger logger = LoggerFactory.getLogger(HttpResponseListener.class);
|
||||
private final CompletableFuture<@Nullable Content> future;
|
||||
private final CompletableFuture<@Nullable ChannelHandlerContent> future;
|
||||
private final HttpStatusListener httpStatusListener;
|
||||
private final String fallbackEncoding;
|
||||
|
||||
/**
|
||||
@ -46,11 +47,12 @@ public class HttpResponseListener extends BufferingResponseListener {
|
||||
* @param fallbackEncoding a fallback encoding for the content (UTF-8 if null)
|
||||
* @param bufferSize the buffer size for the content in kB (default 2048 kB)
|
||||
*/
|
||||
public HttpResponseListener(CompletableFuture<@Nullable Content> future, @Nullable String fallbackEncoding,
|
||||
int bufferSize) {
|
||||
public HttpResponseListener(CompletableFuture<@Nullable ChannelHandlerContent> future,
|
||||
@Nullable String fallbackEncoding, int bufferSize, HttpStatusListener httpStatusListener) {
|
||||
super(bufferSize * 1024);
|
||||
this.future = future;
|
||||
this.fallbackEncoding = fallbackEncoding != null ? fallbackEncoding : StandardCharsets.UTF_8.name();
|
||||
this.httpStatusListener = httpStatusListener;
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -60,31 +62,49 @@ public class HttpResponseListener extends BufferingResponseListener {
|
||||
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().toString());
|
||||
if (response == null || (result.isFailed() && response.getStatus() != HttpStatus.UNAUTHORIZED_401)) {
|
||||
logger.debug("Requesting '{}' (method='{}', content='{}') failed: {}", request.getURI(),
|
||||
request.getMethod(), request.getContent(), result.getFailure().getMessage());
|
||||
future.complete(null);
|
||||
} else if (HttpStatus.isSuccess(response.getStatus())) {
|
||||
String encoding = Objects.requireNonNullElse(getEncoding(), fallbackEncoding);
|
||||
future.complete(new Content(getContent(), encoding, getMediaType()));
|
||||
httpStatusListener.onHttpError(result.getFailure().getMessage());
|
||||
} else {
|
||||
switch (response.getStatus()) {
|
||||
case HttpStatus.OK_200:
|
||||
case HttpStatus.CREATED_201:
|
||||
case HttpStatus.ACCEPTED_202:
|
||||
case HttpStatus.NON_AUTHORITATIVE_INFORMATION_203:
|
||||
case HttpStatus.NO_CONTENT_204:
|
||||
case HttpStatus.RESET_CONTENT_205:
|
||||
case HttpStatus.PARTIAL_CONTENT_206:
|
||||
case HttpStatus.MULTI_STATUS_207:
|
||||
byte[] content = getContent();
|
||||
String encoding = getEncoding();
|
||||
if (content != null) {
|
||||
future.complete(new ChannelHandlerContent(content,
|
||||
encoding == null ? fallbackEncoding : encoding, getMediaType()));
|
||||
} else {
|
||||
future.complete(null);
|
||||
}
|
||||
httpStatusListener.onHttpSuccess();
|
||||
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(),
|
||||
logger.debug("Requesting '{}' (method='{}', content='{}') failed: {} {}", request.getURI(),
|
||||
request.getMethod(), request.getContent(), response.getStatus(), response.getReason());
|
||||
future.completeExceptionally(new IllegalStateException("Response - Code" + response.getStatus()));
|
||||
future.complete(null);
|
||||
httpStatusListener.onHttpError(response.getReason());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private String responseToLogString(Response response) {
|
||||
return "Code = {" + response.getStatus() + "}, Headers = {"
|
||||
String logString = "Code = {" + response.getStatus() + "}, Headers = {"
|
||||
+ response.getHeaders().stream().map(HttpField::toString).collect(Collectors.joining(", "))
|
||||
+ "}, Content = {" + getContentAsString() + "}";
|
||||
return logString;
|
||||
}
|
||||
}
|
||||
|
@ -10,24 +10,27 @@
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.http.internal.transform;
|
||||
package org.openhab.binding.http.internal.http;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* The {@link ValueTransformationProvider} allows to retrieve a transformation service by name
|
||||
* The {@link HttpStatusListener} is an interface for reporting HTTP request states
|
||||
*
|
||||
* @author Jan N. Klug - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public interface ValueTransformationProvider {
|
||||
public interface HttpStatusListener {
|
||||
/**
|
||||
* report an error
|
||||
*
|
||||
* @param message optional error message
|
||||
*/
|
||||
void onHttpError(@Nullable String message);
|
||||
|
||||
/**
|
||||
*
|
||||
* @param pattern A transformation pattern, starting with the transformation service
|
||||
* * name, followed by a colon and the transformation itself.
|
||||
* @return
|
||||
* report a successful request
|
||||
*/
|
||||
ValueTransformation getValueTransformation(@Nullable String pattern);
|
||||
void onHttpSuccess();
|
||||
}
|
@ -24,10 +24,13 @@ import java.util.concurrent.TimeUnit;
|
||||
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.StringContentProvider;
|
||||
import org.eclipse.jetty.http.HttpMethod;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* The {@link RateLimitedHttpClient} is a wrapper for a Jetty HTTP client that limits the number of requests by delaying
|
||||
@ -38,10 +41,14 @@ import org.eclipse.jetty.http.HttpMethod;
|
||||
@NonNullByDefault
|
||||
public class RateLimitedHttpClient {
|
||||
private static final int MAX_QUEUE_SIZE = 1000; // maximum queue size
|
||||
private final Logger logger = LoggerFactory.getLogger(RateLimitedHttpClient.class);
|
||||
|
||||
private HttpClient httpClient;
|
||||
private int delay = 0; // in ms
|
||||
private final ScheduledExecutorService scheduler;
|
||||
private final LinkedBlockingQueue<RequestQueueEntry> requestQueue = new LinkedBlockingQueue<>(MAX_QUEUE_SIZE);
|
||||
private final LinkedBlockingQueue<RequestQueueEntry> priorityRequestQueue = new LinkedBlockingQueue<>(
|
||||
MAX_QUEUE_SIZE);
|
||||
|
||||
private @Nullable ScheduledFuture<?> processJob;
|
||||
|
||||
@ -55,7 +62,7 @@ public class RateLimitedHttpClient {
|
||||
*/
|
||||
public void shutdown() {
|
||||
stopProcessJob();
|
||||
requestQueue.forEach(queueEntry -> queueEntry.future.completeExceptionally(new CancellationException()));
|
||||
requestQueue.forEach(RequestQueueEntry::cancel);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -77,7 +84,7 @@ public class RateLimitedHttpClient {
|
||||
/**
|
||||
* Set the HTTP client
|
||||
*
|
||||
* @param httpClient secure or insecure Jetty http client
|
||||
* @param httpClient secure or insecure {@link HttpClient}
|
||||
*/
|
||||
public void setHttpClient(HttpClient httpClient) {
|
||||
this.httpClient = httpClient;
|
||||
@ -89,31 +96,70 @@ public class RateLimitedHttpClient {
|
||||
* @param finalUrl the request URL
|
||||
* @param method http request method GET/PUT/POST
|
||||
* @param content the content (if method PUT/POST)
|
||||
* @return a CompletableFuture that completes with the request
|
||||
* @return a {@link CompletableFuture} that completes with the request
|
||||
*/
|
||||
public CompletableFuture<Request> newRequest(URI finalUrl, HttpMethod method, String content) {
|
||||
public CompletableFuture<Request> newRequest(URI finalUrl, HttpMethod method, String content,
|
||||
@Nullable String contentType) {
|
||||
return queueRequest(finalUrl, method, content, contentType, requestQueue);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new priority request (executed as next request) to the given URL respecting rate-limits
|
||||
*
|
||||
* @param finalUrl the request URL
|
||||
* @param method http request method GET/PUT/POST
|
||||
* @param content the content (if method PUT/POST)
|
||||
* @return a {@link CompletableFuture} that completes with the request
|
||||
*/
|
||||
public CompletableFuture<Request> newPriorityRequest(URI finalUrl, HttpMethod method, String content,
|
||||
@Nullable String contentType) {
|
||||
return queueRequest(finalUrl, method, content, contentType, priorityRequestQueue);
|
||||
}
|
||||
|
||||
private CompletableFuture<Request> queueRequest(URI finalUrl, HttpMethod method, String content,
|
||||
@Nullable String contentType, LinkedBlockingQueue<RequestQueueEntry> queue) {
|
||||
// if no delay is set, return a completed CompletableFuture
|
||||
CompletableFuture<Request> future = new CompletableFuture<>();
|
||||
RequestQueueEntry queueEntry = new RequestQueueEntry(finalUrl, method, content, future);
|
||||
RequestQueueEntry queueEntry = new RequestQueueEntry(finalUrl, method, content, contentType, future);
|
||||
if (delay == 0) {
|
||||
queueEntry.completeFuture(httpClient);
|
||||
} else {
|
||||
if (!requestQueue.offer(queueEntry)) {
|
||||
if (!queue.offer(queueEntry)) {
|
||||
future.completeExceptionally(new RejectedExecutionException("Maximum queue size exceeded."));
|
||||
}
|
||||
|
||||
}
|
||||
return future;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the AuthenticationStore from the wrapped client
|
||||
* Get the {@link AuthenticationStore} from the wrapped {@link HttpClient}
|
||||
*
|
||||
* @return
|
||||
* @return the AuthenticationStore of the client
|
||||
*/
|
||||
public AuthenticationStore getAuthenticationStore() {
|
||||
return httpClient.getAuthenticationStore();
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove authentication result from the wrapped {@link HttpClient} and force re-auth
|
||||
*
|
||||
* @param uri the {@link URI} associated with the authentication result
|
||||
* @return true if a result was found and cleared, false if not authenticated at all
|
||||
*/
|
||||
public boolean reAuth(URI uri) {
|
||||
AuthenticationStore authStore = httpClient.getAuthenticationStore();
|
||||
Authentication.Result authResult = authStore.findAuthenticationResult(uri);
|
||||
if (authResult != null) {
|
||||
authStore.removeAuthenticationResult(authResult);
|
||||
logger.debug("Cleared authentication result for '{}', retrying immediately", uri);
|
||||
return true;
|
||||
} else {
|
||||
logger.warn("Could not find authentication result for '{}', failing here", uri);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private void stopProcessJob() {
|
||||
ScheduledFuture<?> processJob = this.processJob;
|
||||
if (processJob != null) {
|
||||
@ -122,23 +168,33 @@ public class RateLimitedHttpClient {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a request from either the priority queue or tge regular queue and creates the request
|
||||
*/
|
||||
private void processQueue() {
|
||||
RequestQueueEntry queueEntry = requestQueue.poll();
|
||||
RequestQueueEntry queueEntry = priorityRequestQueue.poll();
|
||||
if (queueEntry == null) {
|
||||
// no entry in priorityRequestQueue, try the regular queue
|
||||
queueEntry = requestQueue.poll();
|
||||
}
|
||||
if (queueEntry != null) {
|
||||
queueEntry.completeFuture(httpClient);
|
||||
}
|
||||
}
|
||||
|
||||
private static class RequestQueueEntry {
|
||||
private URI finalUrl;
|
||||
private HttpMethod method;
|
||||
private String content;
|
||||
private CompletableFuture<Request> future;
|
||||
private final URI finalUrl;
|
||||
private final HttpMethod method;
|
||||
private final String content;
|
||||
private final @Nullable String contentType;
|
||||
private final CompletableFuture<Request> future;
|
||||
|
||||
public RequestQueueEntry(URI finalUrl, HttpMethod method, String content, CompletableFuture<Request> future) {
|
||||
public RequestQueueEntry(URI finalUrl, HttpMethod method, String content, @Nullable String contentType,
|
||||
CompletableFuture<Request> future) {
|
||||
this.finalUrl = finalUrl;
|
||||
this.method = method;
|
||||
this.content = content;
|
||||
this.contentType = contentType;
|
||||
this.future = future;
|
||||
}
|
||||
|
||||
@ -149,10 +205,21 @@ public class RateLimitedHttpClient {
|
||||
*/
|
||||
public void completeFuture(HttpClient httpClient) {
|
||||
Request request = httpClient.newRequest(finalUrl).method(method);
|
||||
if (method != HttpMethod.GET && !content.isEmpty()) {
|
||||
request.content(new StringContentProvider(content));
|
||||
if ((method == HttpMethod.POST || method == HttpMethod.PUT) && !content.isEmpty()) {
|
||||
if (contentType == null) {
|
||||
request.content(new StringContentProvider(content));
|
||||
} else {
|
||||
request.content(new StringContentProvider(content), contentType);
|
||||
}
|
||||
}
|
||||
future.complete(request);
|
||||
}
|
||||
|
||||
/**
|
||||
* cancel this request and complete the future with a {@link CancellationException}
|
||||
*/
|
||||
public void cancel() {
|
||||
future.completeExceptionally(new CancellationException());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -16,7 +16,7 @@ import java.net.MalformedURLException;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.CancellationException;
|
||||
@ -29,11 +29,10 @@ import java.util.function.Consumer;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.eclipse.jetty.client.api.Authentication;
|
||||
import org.eclipse.jetty.client.api.AuthenticationStore;
|
||||
import org.eclipse.jetty.http.HttpMethod;
|
||||
import org.openhab.binding.http.internal.Util;
|
||||
import org.openhab.binding.http.internal.config.HttpThingConfig;
|
||||
import org.openhab.core.thing.binding.generic.ChannelHandlerContent;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
@ -48,29 +47,34 @@ public class RefreshingUrlCache {
|
||||
private final Logger logger = LoggerFactory.getLogger(RefreshingUrlCache.class);
|
||||
|
||||
private final String url;
|
||||
private final boolean escapedUrl;
|
||||
private final RateLimitedHttpClient httpClient;
|
||||
private final boolean strictErrorHandling;
|
||||
private final int timeout;
|
||||
private final int bufferSize;
|
||||
private final @Nullable String fallbackEncoding;
|
||||
private final Set<Consumer<Content>> consumers = ConcurrentHashMap.newKeySet();
|
||||
private final List<String> headers;
|
||||
private final Set<Consumer<@Nullable ChannelHandlerContent>> consumers = ConcurrentHashMap.newKeySet();
|
||||
private final Map<String, String> headers;
|
||||
private final HttpMethod httpMethod;
|
||||
private final String httpContent;
|
||||
private final @Nullable String httpContentType;
|
||||
private final HttpStatusListener httpStatusListener;
|
||||
|
||||
private final ScheduledFuture<?> future;
|
||||
private @Nullable Content lastContent;
|
||||
private @Nullable ChannelHandlerContent lastContent;
|
||||
|
||||
public RefreshingUrlCache(ScheduledExecutorService executor, RateLimitedHttpClient httpClient, String url,
|
||||
boolean escapedUrl, HttpThingConfig thingConfig, String httpContent) {
|
||||
HttpThingConfig thingConfig, String httpContent, @Nullable String httpContentType,
|
||||
HttpStatusListener httpStatusListener) {
|
||||
this.httpClient = httpClient;
|
||||
this.url = url;
|
||||
this.escapedUrl = escapedUrl;
|
||||
this.strictErrorHandling = thingConfig.strictErrorHandling;
|
||||
this.timeout = thingConfig.timeout;
|
||||
this.bufferSize = thingConfig.bufferSize;
|
||||
this.headers = thingConfig.headers;
|
||||
this.httpMethod = thingConfig.stateMethod;
|
||||
this.headers = thingConfig.getHeaders();
|
||||
this.httpContent = httpContent;
|
||||
this.httpContentType = httpContentType;
|
||||
this.httpStatusListener = httpStatusListener;
|
||||
fallbackEncoding = thingConfig.encoding;
|
||||
|
||||
future = executor.scheduleWithFixedDelay(this::refresh, 1, thingConfig.refresh, TimeUnit.SECONDS);
|
||||
@ -89,37 +93,21 @@ public class RefreshingUrlCache {
|
||||
|
||||
// format URL
|
||||
try {
|
||||
String url = String.format(this.url, new Date());
|
||||
URI uri = escapedUrl ? new URI(url) : Util.uriFromString(url);
|
||||
URI uri = Util.uriFromString(String.format(this.url, new Date()));
|
||||
logger.trace("Requesting refresh (retry={}) from '{}' with timeout {}ms", isRetry, uri, timeout);
|
||||
|
||||
httpClient.newRequest(uri, httpMethod, httpContent).thenAccept(request -> {
|
||||
httpClient.newRequest(uri, httpMethod, httpContent, httpContentType).thenAccept(request -> {
|
||||
request.timeout(timeout, TimeUnit.MILLISECONDS);
|
||||
headers.forEach(request::header);
|
||||
|
||||
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", uri);
|
||||
CompletableFuture<@Nullable ChannelHandlerContent> responseContentFuture = new CompletableFuture<>();
|
||||
responseContentFuture.exceptionally(t -> {
|
||||
if (t instanceof HttpAuthException) {
|
||||
if (isRetry || !httpClient.reAuth(uri)) {
|
||||
logger.debug("Authentication failed for '{}', retry={}", uri, isRetry);
|
||||
httpStatusListener.onHttpError("Authentication failed");
|
||||
} else {
|
||||
AuthenticationStore authStore = httpClient.getAuthenticationStore();
|
||||
Authentication.Result authResult = authStore.findAuthenticationResult(uri);
|
||||
if (authResult != null) {
|
||||
authStore.removeAuthenticationResult(authResult);
|
||||
logger.debug("Cleared authentication result for '{}', retrying immediately", uri);
|
||||
refresh(true);
|
||||
} else {
|
||||
logger.warn("Could not find authentication result for '{}', failing here", uri);
|
||||
}
|
||||
refresh(true);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
@ -129,7 +117,8 @@ public class RefreshingUrlCache {
|
||||
logger.trace("Sending to '{}': {}", uri, Util.requestToLogString(request));
|
||||
}
|
||||
|
||||
request.send(new HttpResponseListener(response, fallbackEncoding, bufferSize));
|
||||
request.send(new HttpResponseListener(responseContentFuture, fallbackEncoding, bufferSize,
|
||||
httpStatusListener));
|
||||
}).exceptionally(e -> {
|
||||
if (e instanceof CancellationException) {
|
||||
logger.debug("Request to URL {} was cancelled by thing handler.", uri);
|
||||
@ -150,22 +139,17 @@ public class RefreshingUrlCache {
|
||||
logger.trace("Stopped refresh task for URL '{}'", url);
|
||||
}
|
||||
|
||||
public void addConsumer(Consumer<Content> consumer) {
|
||||
public void addConsumer(Consumer<@Nullable ChannelHandlerContent> consumer) {
|
||||
consumers.add(consumer);
|
||||
}
|
||||
|
||||
public Optional<Content> get() {
|
||||
final Content content = lastContent;
|
||||
if (content == null) {
|
||||
return Optional.empty();
|
||||
} else {
|
||||
return Optional.of(content);
|
||||
}
|
||||
public Optional<ChannelHandlerContent> get() {
|
||||
return Optional.ofNullable(lastContent);
|
||||
}
|
||||
|
||||
private void processResult(@Nullable Content content) {
|
||||
if (content != null) {
|
||||
for (Consumer<Content> consumer : consumers) {
|
||||
private void processResult(@Nullable ChannelHandlerContent content) {
|
||||
if (content != null || strictErrorHandling) {
|
||||
for (Consumer<@Nullable ChannelHandlerContent> consumer : consumers) {
|
||||
try {
|
||||
consumer.accept(content);
|
||||
} catch (IllegalArgumentException | IllegalStateException e) {
|
||||
|
@ -1,63 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2024 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;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* 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 Logger logger = LoggerFactory.getLogger(CascadedValueTransformationImpl.class);
|
||||
private final List<ValueTransformation> transformations;
|
||||
|
||||
public CascadedValueTransformationImpl(String transformationString,
|
||||
Function<String, @Nullable TransformationService> transformationServiceSupplier) {
|
||||
List<ValueTransformation> transformations;
|
||||
try {
|
||||
transformations = Arrays.stream(transformationString.split("∩")).filter(s -> !s.isEmpty())
|
||||
.map(transformation -> new SingleValueTransformation(transformation, transformationServiceSupplier))
|
||||
.collect(Collectors.toList());
|
||||
} catch (IllegalArgumentException e) {
|
||||
transformations = List.of(NoOpValueTransformation.getInstance());
|
||||
logger.warn("Transformation ignore, failed to parse {}: {}", transformationString, e.getMessage());
|
||||
}
|
||||
this.transformations = transformations;
|
||||
}
|
||||
|
||||
@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;
|
||||
}
|
||||
}
|
@ -1,41 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2024 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;
|
||||
}
|
||||
}
|
@ -1,89 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2024 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 {@link 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 + "'}";
|
||||
}
|
||||
}
|
@ -1,34 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2024 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);
|
||||
}
|
@ -6,6 +6,6 @@
|
||||
<type>binding</type>
|
||||
<name>HTTP Binding</name>
|
||||
<description>This is the binding for retrieving and processing HTTP resources.</description>
|
||||
<connection>local</connection>
|
||||
<connection>hybrid</connection>
|
||||
|
||||
</addon:addon>
|
||||
|
@ -7,13 +7,11 @@
|
||||
<config-description uri="channel-type:http:channel-config">
|
||||
<parameter name="stateTransformation" type="text">
|
||||
<label>State Transformation</label>
|
||||
<description>Transformation pattern used when receiving values. Chain multiple transformations with the mathematical
|
||||
intersection character "∩".</description>
|
||||
<description>Transformation pattern used when receiving values. Multiple transformation can be chained using "∩".</description>
|
||||
</parameter>
|
||||
<parameter name="commandTransformation" type="text">
|
||||
<label>Command Transformation</label>
|
||||
<description>Transformation pattern used when sending values. Chain multiple transformations with the mathematical
|
||||
intersection character "∩".</description>
|
||||
<description>Transformation pattern used when sending values. Multiple transformation can be chained using "∩".</description>
|
||||
</parameter>
|
||||
<parameter name="stateExtension" type="text">
|
||||
<label>State URL Extension</label>
|
||||
@ -25,13 +23,6 @@
|
||||
<description>This value is added to the base URL configured in the thing for sending values.</description>
|
||||
<advanced>true</advanced>
|
||||
</parameter>
|
||||
<parameter name="escapedUrl" type="boolean">
|
||||
<label>Escaped URL</label>
|
||||
<description>This specifies whether the URL is already escaped. Applies to the base URL, commandExtension and
|
||||
stateExtension.</description>
|
||||
<advanced>true</advanced>
|
||||
<default>false</default>
|
||||
</parameter>
|
||||
<parameter name="stateContent" type="text">
|
||||
<label>State Content</label>
|
||||
<description>Content for state request (only used if method is POST/PUT)</description>
|
||||
@ -53,13 +44,11 @@
|
||||
<config-description uri="channel-type:http:channel-config-color">
|
||||
<parameter name="stateTransformation" type="text">
|
||||
<label>State Transformation</label>
|
||||
<description>Transformation pattern used when receiving values. Chain multiple transformations with the mathematical
|
||||
intersection character "∩".</description>
|
||||
<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. Chain multiple transformations with the mathematical
|
||||
intersection character "∩".</description>
|
||||
<description>Transformation pattern used when sending value. Multiple transformation can be chained using "∩".</description>
|
||||
</parameter>
|
||||
<parameter name="stateExtension" type="text">
|
||||
<label>State URL Extension</label>
|
||||
@ -71,13 +60,6 @@
|
||||
<description>This value is added to the base URL configured in the thing for sending values.</description>
|
||||
<advanced>true</advanced>
|
||||
</parameter>
|
||||
<parameter name="escapedUrl" type="boolean">
|
||||
<label>Escaped URL</label>
|
||||
<description>This specifies whether the URL is already escaped. Applies to the base URL, commandExtension and
|
||||
stateExtension.</description>
|
||||
<advanced>true</advanced>
|
||||
<default>false</default>
|
||||
</parameter>
|
||||
<parameter name="stateContent" type="text">
|
||||
<label>State Content</label>
|
||||
<description>Content for state request (only used if method is POST/PUT)</description>
|
||||
@ -131,13 +113,11 @@
|
||||
<config-description uri="channel-type:http:channel-config-contact">
|
||||
<parameter name="stateTransformation" type="text">
|
||||
<label>State Transformation</label>
|
||||
<description>Transformation pattern used when receiving values. Chain multiple transformations with the mathematical
|
||||
intersection character "∩".</description>
|
||||
<description>Transformation pattern used when receiving value. Multiple transformation can be chained using "∩".</description>
|
||||
</parameter>
|
||||
<parameter name="commandTransformation" type="text">
|
||||
<label>Command Transformation</label>
|
||||
<description>Transformation pattern used when sending values. Chain multiple transformations with the mathematical
|
||||
intersection character "∩".</description>
|
||||
<description>Transformation pattern used when sending value. Multiple transformation can be chained using "∩".</description>
|
||||
</parameter>
|
||||
<parameter name="stateExtension" type="text">
|
||||
<label>State URL Extension</label>
|
||||
@ -149,13 +129,6 @@
|
||||
<description>This value is added to the base URL configured in the thing for sending values.</description>
|
||||
<advanced>true</advanced>
|
||||
</parameter>
|
||||
<parameter name="escapedUrl" type="boolean">
|
||||
<label>Escaped URL</label>
|
||||
<description>This specifies whether the URL is already escaped. Applies to the base URL, commandExtension and
|
||||
stateExtension.</description>
|
||||
<advanced>true</advanced>
|
||||
<default>false</default>
|
||||
</parameter>
|
||||
<parameter name="stateContent" type="text">
|
||||
<label>State Content</label>
|
||||
<description>Content for state request (only used if method is POST/PUT)</description>
|
||||
@ -185,13 +158,11 @@
|
||||
<config-description uri="channel-type:http:channel-config-dimmer">
|
||||
<parameter name="stateTransformation" type="text">
|
||||
<label>State Transformation</label>
|
||||
<description>Transformation pattern used when receiving values. Chain multiple transformations with the mathematical
|
||||
intersection character "∩".</description>
|
||||
<description>Transformation pattern used when receiving value. Multiple transformation can be chained using "∩".</description>
|
||||
</parameter>
|
||||
<parameter name="commandTransformation" type="text">
|
||||
<label>Command Transformation</label>
|
||||
<description>Transformation pattern used when sending values. Chain multiple transformations with the mathematical
|
||||
intersection character "∩".</description>
|
||||
<description>Transformation pattern used when sending value. Multiple transformation can be chained using "∩".</description>
|
||||
</parameter>
|
||||
<parameter name="stateExtension" type="text">
|
||||
<label>State URL Extension</label>
|
||||
@ -203,13 +174,6 @@
|
||||
<description>This value is added to the base URL configured in the thing for sending values.</description>
|
||||
<advanced>true</advanced>
|
||||
</parameter>
|
||||
<parameter name="escapedUrl" type="boolean">
|
||||
<label>Escaped URL</label>
|
||||
<description>This specifies whether the URL is already escaped. Applies to the base URL, commandExtension and
|
||||
stateExtension.</description>
|
||||
<advanced>true</advanced>
|
||||
<default>false</default>
|
||||
</parameter>
|
||||
<parameter name="stateContent" type="text">
|
||||
<label>State Content</label>
|
||||
<description>Content for state request (only used if method is POST/PUT)</description>
|
||||
@ -256,12 +220,6 @@
|
||||
<description>This value is added to the base URL configured in the thing for retrieving values.</description>
|
||||
<advanced>true</advanced>
|
||||
</parameter>
|
||||
<parameter name="escapedUrl" type="boolean">
|
||||
<label>Escaped URL</label>
|
||||
<description>This specifies whether the URL is already escaped. Applies to the base URL and stateExtension.</description>
|
||||
<advanced>true</advanced>
|
||||
<default>false</default>
|
||||
</parameter>
|
||||
<parameter name="stateContent" type="text">
|
||||
<label>State Content</label>
|
||||
<description>Content for state request (only used if method is POST/PUT)</description>
|
||||
@ -272,13 +230,11 @@
|
||||
<config-description uri="channel-type:http:channel-config-number">
|
||||
<parameter name="stateTransformation" type="text">
|
||||
<label>State Transformation</label>
|
||||
<description>Transformation pattern used when receiving values. Chain multiple transformations with the mathematical
|
||||
intersection character "∩".</description>
|
||||
<description>Transformation pattern used when receiving value. Multiple transformation can be chained using "∩".</description>
|
||||
</parameter>
|
||||
<parameter name="commandTransformation" type="text">
|
||||
<label>Command Transformation</label>
|
||||
<description>Transformation pattern used when sending values. Chain multiple transformations with the mathematical
|
||||
intersection character "∩".</description>
|
||||
<description>Transformation pattern used when sending value. Multiple transformation can be chained using "∩".</description>
|
||||
</parameter>
|
||||
<parameter name="stateExtension" type="text">
|
||||
<label>State URL Extension</label>
|
||||
@ -290,13 +246,6 @@
|
||||
<description>This value is added to the base URL configured in the thing for sending values.</description>
|
||||
<advanced>true</advanced>
|
||||
</parameter>
|
||||
<parameter name="escapedUrl" type="boolean">
|
||||
<label>Escaped URL</label>
|
||||
<description>This specifies whether the URL is already escaped. Applies to the base URL, commandExtension and
|
||||
stateExtension.</description>
|
||||
<advanced>true</advanced>
|
||||
<default>false</default>
|
||||
</parameter>
|
||||
<parameter name="stateContent" type="text">
|
||||
<label>State Content</label>
|
||||
<description>Content for state request (only used if method is POST/PUT)</description>
|
||||
@ -323,13 +272,11 @@
|
||||
<config-description uri="channel-type:http:channel-config-player">
|
||||
<parameter name="stateTransformation" type="text">
|
||||
<label>State Transformation</label>
|
||||
<description>Transformation pattern used when receiving values. Chain multiple transformations with the mathematical
|
||||
intersection character "∩".</description>
|
||||
<description>Transformation pattern used when receiving value. Multiple transformation can be chained using "∩".</description>
|
||||
</parameter>
|
||||
<parameter name="commandTransformation" type="text">
|
||||
<label>Command Transformation</label>
|
||||
<description>Transformation pattern used when sending values. Chain multiple transformations with the mathematical
|
||||
intersection character "∩".</description>
|
||||
<description>Transformation pattern used when sending value. Multiple transformation can be chained using "∩".</description>
|
||||
</parameter>
|
||||
<parameter name="stateExtension" type="text">
|
||||
<label>State URL Extension</label>
|
||||
@ -341,13 +288,6 @@
|
||||
<description>This value is added to the base URL configured in the thing for sending values.</description>
|
||||
<advanced>true</advanced>
|
||||
</parameter>
|
||||
<parameter name="escapedUrl" type="boolean">
|
||||
<label>Escaped URL</label>
|
||||
<description>This specifies whether the URL is already escaped. Applies to the base URL, commandExtension and
|
||||
stateExtension.</description>
|
||||
<advanced>true</advanced>
|
||||
<default>false</default>
|
||||
</parameter>
|
||||
<parameter name="stateContent" type="text">
|
||||
<label>State Content</label>
|
||||
<description>Content for state request (only used if method is POST/PUT)</description>
|
||||
@ -393,13 +333,11 @@
|
||||
<config-description uri="channel-type:http:channel-config-rollershutter">
|
||||
<parameter name="stateTransformation" type="text">
|
||||
<label>State Transformation</label>
|
||||
<description>Transformation pattern used when receiving values. Chain multiple transformations with the mathematical
|
||||
intersection character "∩".</description>
|
||||
<description>Transformation pattern used when receiving value. Multiple transformation can be chained using "∩".</description>
|
||||
</parameter>
|
||||
<parameter name="commandTransformation" type="text">
|
||||
<label>Command Transformation</label>
|
||||
<description>Transformation pattern used when sending values Chain multiple transformations with the mathematical
|
||||
intersection character "∩"..</description>
|
||||
<description>Transformation pattern used when sending value. Multiple transformation can be chained using "∩".</description>
|
||||
</parameter>
|
||||
<parameter name="stateExtension" type="text">
|
||||
<label>State URL Extension</label>
|
||||
@ -411,13 +349,6 @@
|
||||
<description>This value is added to the base URL configured in the thing for sending values.</description>
|
||||
<advanced>true</advanced>
|
||||
</parameter>
|
||||
<parameter name="escapedUrl" type="boolean">
|
||||
<label>Escaped URL</label>
|
||||
<description>This specifies whether the URL is already escaped. Applies to the base URL, commandExtension and
|
||||
stateExtension.</description>
|
||||
<advanced>true</advanced>
|
||||
<default>false</default>
|
||||
</parameter>
|
||||
<parameter name="stateContent" type="text">
|
||||
<label>State Content</label>
|
||||
<description>Content for state request (only used if method is POST/PUT)</description>
|
||||
@ -455,13 +386,11 @@
|
||||
<config-description uri="channel-type:http:channel-config-switch">
|
||||
<parameter name="stateTransformation" type="text">
|
||||
<label>State Transformation</label>
|
||||
<description>Transformation pattern used when receiving values. Chain multiple transformations with the mathematical
|
||||
intersection character "∩".</description>
|
||||
<description>Transformation pattern used when receiving value. Multiple transformation can be chained using "∩".</description>
|
||||
</parameter>
|
||||
<parameter name="commandTransformation" type="text">
|
||||
<label>Command Transformation</label>
|
||||
<description>Transformation pattern used when sending values. Chain multiple transformations with the mathematical
|
||||
intersection character "∩".</description>
|
||||
<description>Transformation pattern used when sending value. Multiple transformation can be chained using "∩".</description>
|
||||
</parameter>
|
||||
<parameter name="stateExtension" type="text">
|
||||
<label>State URL Extension</label>
|
||||
@ -473,13 +402,6 @@
|
||||
<description>This value is added to the base URL configured in the thing for sending values.</description>
|
||||
<advanced>true</advanced>
|
||||
</parameter>
|
||||
<parameter name="escapedUrl" type="boolean">
|
||||
<label>Escaped URL</label>
|
||||
<description>This specifies whether the URL is already escaped. Applies to the base URL, commandExtension and
|
||||
stateExtension.</description>
|
||||
<advanced>true</advanced>
|
||||
<default>false</default>
|
||||
</parameter>
|
||||
<parameter name="stateContent" type="text">
|
||||
<label>State Content</label>
|
||||
<description>Content for state request (only used if method is POST/PUT)</description>
|
||||
|
@ -9,6 +9,19 @@
|
||||
<label>HTTP URL Thing</label>
|
||||
<description>Represents a base URL and all associated requests.</description>
|
||||
|
||||
<channels>
|
||||
<channel typeId="request-date-time" id="last-failure">
|
||||
<label>Last Failure</label>
|
||||
</channel>
|
||||
<channel typeId="request-date-time" id="last-success">
|
||||
<label>Last Success</label>
|
||||
</channel>
|
||||
</channels>
|
||||
|
||||
<properties>
|
||||
<property name="thingTypeVersion">2</property>
|
||||
</properties>
|
||||
|
||||
<config-description>
|
||||
<parameter name="baseURL" type="text" required="true">
|
||||
<label>Base URL</label>
|
||||
@ -44,7 +57,7 @@
|
||||
</parameter>
|
||||
<parameter name="password" type="text">
|
||||
<label>Password</label>
|
||||
<description>Basic Authentication password</description>
|
||||
<description>Authentication password or token</description>
|
||||
<context>password</context>
|
||||
<advanced>true</advanced>
|
||||
</parameter>
|
||||
@ -54,6 +67,7 @@
|
||||
<option value="BASIC">Basic Authentication</option>
|
||||
<option value="BASIC_PREEMPTIVE">Preemptive Basic Authentication</option>
|
||||
<option value="DIGEST">Digest Authentication</option>
|
||||
<option value="TOKEN">Token/Bearer Authentication</option>
|
||||
</options>
|
||||
<default>BASIC</default>
|
||||
<limitToOptions>true</limitToOptions>
|
||||
@ -112,9 +126,20 @@
|
||||
<default>false</default>
|
||||
<advanced>true</advanced>
|
||||
</parameter>
|
||||
<parameter name="userAgent" type="text">
|
||||
<label>User Agent</label>
|
||||
<description>Sets a custom user agent (default is "Jetty/version", e.g. "Jetty/9.4.20.v20190813").</description>
|
||||
<advanced>true</advanced>
|
||||
</parameter>
|
||||
</config-description>
|
||||
</thing-type>
|
||||
|
||||
<channel-type id="request-date-time">
|
||||
<item-type>DateTime</item-type>
|
||||
<label>Dummy</label>
|
||||
<state readOnly="true" pattern="%1$tY-%1$tm-%1$td %1$tH:%1$tM:%1$tS"/>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="color">
|
||||
<item-type>Color</item-type>
|
||||
<label>Color Channel</label>
|
||||
|
@ -0,0 +1,31 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>
|
||||
<update:update-descriptions xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:update="https://openhab.org/schemas/update-description/v1.0.0"
|
||||
xsi:schemaLocation="https://openhab.org/schemas/update-description/v1.0.0 https://openhab.org/schemas/update-description-1.0.0.xsd">
|
||||
|
||||
<thing-type uid="http:url">
|
||||
<instruction-set targetVersion="1">
|
||||
<add-channel id="lastSuccess">
|
||||
<type>http:requestDateTime</type>
|
||||
<label>Last Success</label>
|
||||
</add-channel>
|
||||
<add-channel id="lastFailure">
|
||||
<type>http:requestDateTime</type>
|
||||
<label>Last Failure</label>
|
||||
</add-channel>
|
||||
</instruction-set>
|
||||
<instruction-set targetVersion="2">
|
||||
<remove-channel id="lastSuccess"/>
|
||||
<remove-channel id="lastFailure"/>
|
||||
<add-channel id="last-success">
|
||||
<type>http:request-date-time</type>
|
||||
<label>Last Success</label>
|
||||
</add-channel>
|
||||
<add-channel id="last-failure">
|
||||
<type>http:request-date-time</type>
|
||||
<label>Last Failure</label>
|
||||
</add-channel>
|
||||
</instruction-set>
|
||||
</thing-type>
|
||||
|
||||
</update:update-descriptions>
|
@ -0,0 +1,70 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2024 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;
|
||||
|
||||
import static com.github.tomakehurst.wiremock.client.WireMock.configureFor;
|
||||
import static com.github.tomakehurst.wiremock.client.WireMock.removeAllMappings;
|
||||
import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.options;
|
||||
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.ScheduledThreadPoolExecutor;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jetty.client.HttpClient;
|
||||
import org.junit.jupiter.api.AfterAll;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.openhab.binding.http.internal.http.RateLimitedHttpClient;
|
||||
import org.openhab.core.test.TestPortUtil;
|
||||
import org.openhab.core.test.java.JavaTest;
|
||||
|
||||
import com.github.tomakehurst.wiremock.WireMockServer;
|
||||
import com.github.tomakehurst.wiremock.extension.responsetemplating.ResponseTemplateTransformer;
|
||||
|
||||
/**
|
||||
* The {@link AbstractWireMockTest} implements tests for the {@link RateLimitedHttpClient}
|
||||
*
|
||||
* @author Jan N. Klug - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public abstract class AbstractWireMockTest extends JavaTest {
|
||||
protected int port = 0;
|
||||
protected @NonNullByDefault({}) WireMockServer wireMockServer;
|
||||
protected @NonNullByDefault({}) HttpClient httpClient;
|
||||
protected ScheduledExecutorService scheduler = new ScheduledThreadPoolExecutor(4);
|
||||
|
||||
@BeforeAll
|
||||
public void initAll() throws Exception {
|
||||
port = TestPortUtil.findFreePort();
|
||||
|
||||
wireMockServer = new WireMockServer(options().port(port).extensions(new ResponseTemplateTransformer(false)));
|
||||
wireMockServer.start();
|
||||
|
||||
httpClient = new HttpClient();
|
||||
httpClient.start();
|
||||
|
||||
configureFor("localhost", port);
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
public void cleanUpTest() {
|
||||
removeAllMappings();
|
||||
}
|
||||
|
||||
@AfterAll
|
||||
public void cleanUpAll() throws Exception {
|
||||
wireMockServer.shutdown();
|
||||
scheduler.shutdown();
|
||||
httpClient.stop();
|
||||
}
|
||||
}
|
@ -0,0 +1,146 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2024 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;
|
||||
|
||||
import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
|
||||
import static com.github.tomakehurst.wiremock.client.WireMock.get;
|
||||
import static com.github.tomakehurst.wiremock.client.WireMock.stubFor;
|
||||
import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo;
|
||||
import static org.hamcrest.CoreMatchers.allOf;
|
||||
import static org.hamcrest.CoreMatchers.anyOf;
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.hamcrest.Matchers.equalTo;
|
||||
import static org.hamcrest.Matchers.greaterThanOrEqualTo;
|
||||
import static org.hamcrest.Matchers.lessThan;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotEquals;
|
||||
|
||||
import java.net.URI;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jetty.client.api.ContentResponse;
|
||||
import org.eclipse.jetty.client.api.Request;
|
||||
import org.eclipse.jetty.http.HttpMethod;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.TestInstance;
|
||||
import org.openhab.binding.http.internal.http.RateLimitedHttpClient;
|
||||
|
||||
/**
|
||||
* The {@link RateLimitedHttpClientTest} implements tests for the {@link RateLimitedHttpClient}
|
||||
*
|
||||
* @author Jan N. Klug - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
public class RateLimitedHttpClientTest extends AbstractWireMockTest {
|
||||
private static final String TEST_LOCATION = "/testlocation";
|
||||
private static final String TEST_CONTENT = "TESTCONTENT";
|
||||
|
||||
private List<Response> responses = new CopyOnWriteArrayList<>();
|
||||
|
||||
@AfterEach
|
||||
public void cleanUpTest() {
|
||||
responses.clear();
|
||||
super.cleanUpTest();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testWithoutLimit() {
|
||||
doLimitTest(0, List.of(false, false));
|
||||
|
||||
// we except to receive the responses in the correct order
|
||||
assertEquals(0, responses.get(0).seqNumber);
|
||||
assertEquals(1, responses.get(1).seqNumber);
|
||||
|
||||
// we expect a short delay between both requests, but less than 100ms
|
||||
long msBetween = responses.get(1).time - responses.get(0).time;
|
||||
assertThat((int) msBetween, allOf(greaterThanOrEqualTo(0), lessThan(100)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testWithLimit() {
|
||||
doLimitTest(500, List.of(false, false));
|
||||
// we except to receive the responses in the correct order
|
||||
assertEquals(0, responses.get(0).seqNumber);
|
||||
assertEquals(1, responses.get(1).seqNumber);
|
||||
|
||||
// we expect at least 500ms delay between both requests, but less than 500+100=600ms
|
||||
long msBetween = responses.get(1).time - responses.get(0).time;
|
||||
assertThat((int) msBetween, allOf(greaterThanOrEqualTo(500), lessThan(600)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testWithLimitAndPriority() {
|
||||
doLimitTest(500, List.of(false, false, true));
|
||||
|
||||
// we expect to receive the responses of request 3 before request two, exact order of 1 and 3 depends on timing,
|
||||
// so accept both
|
||||
assertThat(responses.get(0).seqNumber, anyOf(equalTo(0), equalTo(2)));
|
||||
assertThat(responses.get(1).seqNumber, anyOf(equalTo(0), equalTo(2)));
|
||||
assertNotEquals(responses.get(1).seqNumber, responses.get(0).seqNumber);
|
||||
assertEquals(1, responses.get(2).seqNumber);
|
||||
|
||||
// we expect at least 2*500=1000ms delay between the first and last request, but less than 2*500+100=1100 ms
|
||||
long msBetween = responses.get(2).time - responses.get(0).time;
|
||||
assertThat((int) msBetween, allOf(greaterThanOrEqualTo(1000), lessThan(1100)));
|
||||
}
|
||||
|
||||
private List<Response> doLimitTest(int setDelay, List<Boolean> config) {
|
||||
stubFor(get(urlEqualTo(TEST_LOCATION)).willReturn(aResponse().withBody(TEST_CONTENT)));
|
||||
|
||||
RateLimitedHttpClient rateLimitedHttpClient = new RateLimitedHttpClient(httpClient, scheduler);
|
||||
rateLimitedHttpClient.setDelay(setDelay);
|
||||
|
||||
URI url = URI.create("http://localhost:" + port + TEST_LOCATION);
|
||||
int seqNumber = 0;
|
||||
|
||||
for (boolean priority : config) {
|
||||
int nextSeqNumber = seqNumber++;
|
||||
CompletableFuture<Request> requestFuture;
|
||||
|
||||
if (priority) {
|
||||
requestFuture = rateLimitedHttpClient.newPriorityRequest(url, HttpMethod.GET, "", null);
|
||||
} else {
|
||||
requestFuture = rateLimitedHttpClient.newRequest(url, HttpMethod.GET, "", null);
|
||||
}
|
||||
|
||||
requestFuture.thenAccept(request -> {
|
||||
try {
|
||||
responses.add(new Response(nextSeqNumber, request.send()));
|
||||
} catch (Exception e) {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// wait until we got all results
|
||||
waitForAssert(() -> assertEquals(config.size(), responses.size()));
|
||||
rateLimitedHttpClient.shutdown();
|
||||
|
||||
return responses;
|
||||
}
|
||||
|
||||
private static class Response {
|
||||
public final int seqNumber;
|
||||
public final long time = System.currentTimeMillis();
|
||||
public final String content;
|
||||
|
||||
public Response(int seqNumber, ContentResponse contentResponse) {
|
||||
this.seqNumber = seqNumber;
|
||||
this.content = contentResponse.getContentAsString();
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,261 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2024 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;
|
||||
|
||||
import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
|
||||
import static com.github.tomakehurst.wiremock.client.WireMock.get;
|
||||
import static com.github.tomakehurst.wiremock.client.WireMock.post;
|
||||
import static com.github.tomakehurst.wiremock.client.WireMock.stubFor;
|
||||
import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo;
|
||||
import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.mockingDetails;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.timeout;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.eclipse.jetty.http.HttpMethod;
|
||||
import org.eclipse.jetty.util.Jetty;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.TestInstance;
|
||||
import org.openhab.binding.http.internal.config.HttpThingConfig;
|
||||
import org.openhab.binding.http.internal.http.HttpStatusListener;
|
||||
import org.openhab.binding.http.internal.http.RateLimitedHttpClient;
|
||||
import org.openhab.binding.http.internal.http.RefreshingUrlCache;
|
||||
import org.openhab.core.thing.binding.generic.ChannelHandlerContent;
|
||||
|
||||
/**
|
||||
* The {@link RefreshingUrlCacheTest} implements tests for the {@link RefreshingUrlCache}
|
||||
*
|
||||
* @author Jan N. Klug - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
public class RefreshingUrlCacheTest extends AbstractWireMockTest {
|
||||
private static final String TEST_LOCATION = "/testlocation";
|
||||
private static final String TEST_CONTENT = "TESTCONTENT";
|
||||
|
||||
private @NonNullByDefault({}) RateLimitedHttpClient rateLimitedHttpClient;
|
||||
private @NonNullByDefault({}) HttpThingConfig thingConfig;
|
||||
private @NonNullByDefault({}) String url;
|
||||
private @NonNullByDefault({}) HttpStatusListener statusListener;
|
||||
|
||||
private final List<@Nullable ChannelHandlerContent> contentWrappers = new CopyOnWriteArrayList<>();
|
||||
|
||||
@BeforeEach
|
||||
public void initTest() {
|
||||
// this is usually done inside the HttpHandlerFactory when creating the clients
|
||||
httpClient.setUserAgentField(null);
|
||||
|
||||
// create a RateLimitedHttpClient
|
||||
rateLimitedHttpClient = new RateLimitedHttpClient(httpClient, scheduler);
|
||||
rateLimitedHttpClient.setDelay(0);
|
||||
statusListener = mock(HttpStatusListener.class);
|
||||
|
||||
// initialize thing config with some default values
|
||||
thingConfig = new HttpThingConfig();
|
||||
thingConfig.baseURL = "http://localhost:" + port;
|
||||
thingConfig.timeout = 500;
|
||||
thingConfig.refresh = 1;
|
||||
|
||||
url = thingConfig.baseURL + TEST_LOCATION;
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
public void cleanUpTest() {
|
||||
rateLimitedHttpClient.shutdown();
|
||||
contentWrappers.clear();
|
||||
super.cleanUpTest();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUpdateOnSuccessfulRequest() {
|
||||
stubFor(get(urlEqualTo(TEST_LOCATION)).willReturn(aResponse().withBody(TEST_CONTENT)));
|
||||
|
||||
RefreshingUrlCache urlCache = getUrlCache(TEST_CONTENT);
|
||||
|
||||
// wait until we got at least four results or timeout (after 10s)
|
||||
waitForAssert(() -> assertEquals(4, contentWrappers.size()));
|
||||
urlCache.stop();
|
||||
|
||||
// verify we did not have errors and the number of responses matches the number of success calls
|
||||
verify(statusListener, never()).onHttpError(any());
|
||||
verify(statusListener, times(contentWrappers.size())).onHttpSuccess();
|
||||
|
||||
// assert all content equals the correct value
|
||||
assertTrue(contentWrappers.stream().map(Objects::requireNonNull).map(ChannelHandlerContent::getAsString)
|
||||
.allMatch(TEST_CONTENT::equals));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNoUpdateOn404ErrorInNormalMode() {
|
||||
stubFor(get(urlEqualTo(TEST_LOCATION)).willReturn(aResponse().withStatus(404)));
|
||||
|
||||
RefreshingUrlCache urlCache = getUrlCache(TEST_CONTENT);
|
||||
|
||||
// verify we get at least two error reports in 3s
|
||||
verify(statusListener, timeout(3000).atLeast(2)).onHttpError(any());
|
||||
verify(statusListener, never()).onHttpSuccess();
|
||||
urlCache.stop();
|
||||
|
||||
// assert all content equals the correct value
|
||||
assertEquals(true, contentWrappers.isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNullUpdateOn404ErrorInStrictMode() {
|
||||
stubFor(get(urlEqualTo(TEST_LOCATION)).willReturn(aResponse().withStatus(404)));
|
||||
thingConfig.strictErrorHandling = true;
|
||||
|
||||
RefreshingUrlCache urlCache = getUrlCache(TEST_CONTENT);
|
||||
|
||||
// verify we get at least two error reports in 3s
|
||||
verify(statusListener, timeout(3000).atLeast(2)).onHttpError(any());
|
||||
verify(statusListener, never()).onHttpSuccess();
|
||||
urlCache.stop();
|
||||
|
||||
int totalErrorCalls = mockingDetails(statusListener).getInvocations().size();
|
||||
|
||||
// assert we have the same number of consumer calls as error calls and all are null
|
||||
assertEquals(totalErrorCalls, contentWrappers.size());
|
||||
assertEquals(true, contentWrappers.stream().allMatch(Objects::isNull));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNoUpdateOnRequestTimedOutInNormalMode() {
|
||||
stubFor(get(urlEqualTo(TEST_LOCATION)).willReturn(aResponse().withFixedDelay(1000).withStatus(200)));
|
||||
|
||||
RefreshingUrlCache urlCache = getUrlCache(TEST_CONTENT);
|
||||
|
||||
// verify we get at least two error reports in 3s
|
||||
verify(statusListener, timeout(3000).atLeast(2)).onHttpError(any());
|
||||
verify(statusListener, never()).onHttpSuccess();
|
||||
urlCache.stop();
|
||||
|
||||
// assert all content equals the correct value
|
||||
assertEquals(true, contentWrappers.isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNullUpdateOnRequestTimedOutInStrictMode() {
|
||||
stubFor(get(urlEqualTo(TEST_LOCATION)).willReturn(aResponse().withFixedDelay(1000).withStatus(200)));
|
||||
thingConfig.strictErrorHandling = true;
|
||||
|
||||
RefreshingUrlCache urlCache = getUrlCache(TEST_CONTENT);
|
||||
|
||||
// verify we get at least two error reports in 3s
|
||||
verify(statusListener, timeout(3000).atLeast(2)).onHttpError(any());
|
||||
verify(statusListener, never()).onHttpSuccess();
|
||||
urlCache.stop();
|
||||
|
||||
int totalErrorCalls = mockingDetails(statusListener).getInvocations().size();
|
||||
|
||||
// assert we have the same number of consumer calls as error calls and all are null
|
||||
assertEquals(totalErrorCalls, contentWrappers.size());
|
||||
assertEquals(true, contentWrappers.stream().allMatch(Objects::isNull));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAdditionalHeaderIsSentWithRequest() {
|
||||
String testHeaderKey = "X-SMARTHOME";
|
||||
String testHeaderValue = "TESTVALUE";
|
||||
|
||||
stubFor(get(urlEqualTo(TEST_LOCATION)).willReturn(aResponse()
|
||||
.withBody("{{request.headers." + testHeaderKey + "}}").withTransformers("response-template")));
|
||||
thingConfig.headers = new ArrayList<>(List.of(testHeaderKey + "=" + testHeaderValue));
|
||||
|
||||
RefreshingUrlCache urlCache = getUrlCache(TEST_CONTENT);
|
||||
|
||||
// we need only one answer
|
||||
waitForAssert(() -> assertFalse(contentWrappers.isEmpty()));
|
||||
urlCache.stop();
|
||||
|
||||
String returnedHeaderValue = Objects.requireNonNull(contentWrappers.get(0)).getAsString();
|
||||
assertEquals(testHeaderValue, returnedHeaderValue);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUserAgentIsJettyWhenNotConfigured() {
|
||||
stubFor(get(urlEqualTo(TEST_LOCATION)).willReturn(
|
||||
aResponse().withBody("{{request.headers.User-Agent}}").withTransformers("response-template")));
|
||||
|
||||
RefreshingUrlCache urlCache = getUrlCache(TEST_CONTENT);
|
||||
|
||||
// we need only one answer
|
||||
waitForAssert(() -> assertFalse(contentWrappers.isEmpty()));
|
||||
urlCache.stop();
|
||||
|
||||
String returnedHeaderValue = Objects.requireNonNull(contentWrappers.get(0)).getAsString();
|
||||
assertEquals("Jetty/" + Jetty.VERSION, returnedHeaderValue);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testContentSentAlongWithPost() {
|
||||
stubFor(post(urlEqualTo(TEST_LOCATION))
|
||||
.willReturn(aResponse().withBody("{{request.body}}").withTransformers("response-template")));
|
||||
thingConfig.stateMethod = HttpMethod.POST;
|
||||
|
||||
RefreshingUrlCache urlCache = getUrlCache(TEST_CONTENT);
|
||||
|
||||
// we need only one answer
|
||||
waitForAssert(() -> assertFalse(contentWrappers.isEmpty()));
|
||||
urlCache.stop();
|
||||
|
||||
String returnedBody = Objects.requireNonNull(contentWrappers.get(0)).getAsString();
|
||||
assertEquals(TEST_CONTENT, returnedBody);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDateIsFormattedInURL() {
|
||||
stubFor(get(urlPathEqualTo(TEST_LOCATION))
|
||||
.willReturn(aResponse().withBody("{{request.query.date}}").withTransformers("response-template")));
|
||||
url += "?date=%1$tY-%1$tm-%1$td";
|
||||
|
||||
RefreshingUrlCache urlCache = getUrlCache(TEST_CONTENT);
|
||||
|
||||
// we need only one answer
|
||||
waitForAssert(() -> assertFalse(contentWrappers.isEmpty()));
|
||||
urlCache.stop();
|
||||
|
||||
String returnedQueryValue = Objects.requireNonNull(contentWrappers.get(0)).getAsString();
|
||||
assertTrue(returnedQueryValue.matches("\\d{4}-\\d{2}-\\d{2}"));
|
||||
}
|
||||
|
||||
/**
|
||||
* helper method to create a {@link RefreshingUrlCache} and add a test listener
|
||||
*
|
||||
* @param content HTTP content
|
||||
* @return the cache object
|
||||
*/
|
||||
private RefreshingUrlCache getUrlCache(String content) {
|
||||
RefreshingUrlCache urlCache = new RefreshingUrlCache(scheduler, rateLimitedHttpClient, url, thingConfig,
|
||||
content, null, statusListener);
|
||||
urlCache.addConsumer(contentWrappers::add);
|
||||
|
||||
return urlCache;
|
||||
}
|
||||
}
|
@ -0,0 +1,60 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2024 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;
|
||||
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URISyntaxException;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.openhab.binding.http.internal.Util;
|
||||
|
||||
/**
|
||||
* The {@link UtilTest} is a test class for URL encoding
|
||||
*
|
||||
* @author Jan N. Klug - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class UtilTest {
|
||||
|
||||
@Test
|
||||
public void uriUTF8InHostnameEncodeTest() throws MalformedURLException, URISyntaxException {
|
||||
String s = "https://foöo.bar/zhu.html?str=zin&tzz=678";
|
||||
Assertions.assertEquals("https://xn--foo-tna.bar/zhu.html?str=zin&tzz=678", Util.uriFromString(s).toString());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void uriUTF8InPathEncodeTest() throws MalformedURLException, URISyntaxException {
|
||||
String s = "https://foo.bar/zül.html?str=zin";
|
||||
Assertions.assertEquals("https://foo.bar/z%C3%BCl.html?str=zin", Util.uriFromString(s).toString());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void uriUTF8InQueryEncodeTest() throws MalformedURLException, URISyntaxException {
|
||||
String s = "https://foo.bar/zil.html?str=zän";
|
||||
Assertions.assertEquals("https://foo.bar/zil.html?str=z%C3%A4n", Util.uriFromString(s).toString());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void uriSpaceInPathEncodeTest() throws MalformedURLException, URISyntaxException {
|
||||
String s = "https://foo.bar/z l.html?str=zun";
|
||||
Assertions.assertEquals("https://foo.bar/z%20l.html?str=zun", Util.uriFromString(s).toString());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void uriSpaceInQueryEncodeTest() throws MalformedURLException, URISyntaxException {
|
||||
String s = "https://foo.bar/zzl.html?str=z n";
|
||||
Assertions.assertEquals("https://foo.bar/zzl.html?str=z%20n", Util.uriFromString(s).toString());
|
||||
}
|
||||
}
|
@ -1,106 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2024 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.QuantityType;
|
||||
import org.openhab.core.library.types.StringType;
|
||||
import org.openhab.core.library.unit.SIUnits;
|
||||
import org.openhab.core.library.unit.Units;
|
||||
import org.openhab.core.types.Command;
|
||||
import org.openhab.core.types.State;
|
||||
import org.openhab.core.types.UnDefType;
|
||||
|
||||
/**
|
||||
* The {@link ConverterTest} is a test class for state converters
|
||||
*
|
||||
* @author Jan N. Klug - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class ConverterTest {
|
||||
|
||||
@Test
|
||||
public void numberItemConverter() {
|
||||
NumberItemConverter converter = new NumberItemConverter(this::updateState, this::postCommand,
|
||||
this::sendHttpValue, NoOpValueTransformation.getInstance(), NoOpValueTransformation.getInstance(),
|
||||
new HttpChannelConfig());
|
||||
|
||||
// without unit
|
||||
Assertions.assertEquals(new DecimalType(1234), converter.toState("1234"));
|
||||
|
||||
// unit in transformation result
|
||||
Assertions.assertEquals(new QuantityType<>(100, SIUnits.CELSIUS), converter.toState("100°C"));
|
||||
|
||||
// no valid value
|
||||
Assertions.assertEquals(UnDefType.UNDEF, converter.toState("W"));
|
||||
Assertions.assertEquals(UnDefType.UNDEF, converter.toState(""));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void numberItemConverterWithUnit() {
|
||||
HttpChannelConfig channelConfig = new HttpChannelConfig();
|
||||
channelConfig.unit = "W";
|
||||
NumberItemConverter converter = new NumberItemConverter(this::updateState, this::postCommand,
|
||||
this::sendHttpValue, NoOpValueTransformation.getInstance(), NoOpValueTransformation.getInstance(),
|
||||
channelConfig);
|
||||
|
||||
// without unit
|
||||
Assertions.assertEquals(new QuantityType<>(500, Units.WATT), converter.toState("500"));
|
||||
|
||||
// no valid value
|
||||
Assertions.assertEquals(UnDefType.UNDEF, converter.toState("100foo"));
|
||||
Assertions.assertEquals(UnDefType.UNDEF, converter.toState("foo"));
|
||||
Assertions.assertEquals(UnDefType.UNDEF, converter.toState(""));
|
||||
}
|
||||
|
||||
@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());
|
||||
}
|
||||
}
|
@ -1,320 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2024 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 static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.CompletionException;
|
||||
|
||||
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.http.HttpFields;
|
||||
import org.eclipse.jetty.http.HttpHeader;
|
||||
import org.eclipse.jetty.http.HttpStatus;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
/**
|
||||
* Unit tests for {@link HttpResponseListenerTest}.
|
||||
*
|
||||
* @author Corubba Smith - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
public class HttpResponseListenerTest {
|
||||
|
||||
private Request request = mock(Request.class);
|
||||
private Response response = mock(Response.class);
|
||||
|
||||
// ******** Common methods ******** //
|
||||
|
||||
/**
|
||||
* Run the given listener with the given result.
|
||||
*/
|
||||
private void run(HttpResponseListener listener, Result result) {
|
||||
listener.onComplete(result);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a default Result using the request- and response-mocks and no failure.
|
||||
*/
|
||||
private Result createResult() {
|
||||
return new Result(request, response);
|
||||
}
|
||||
|
||||
/**
|
||||
* Run the given listener with a default result.
|
||||
*/
|
||||
private void run(HttpResponseListener listener) {
|
||||
run(listener, createResult());
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the given payload as body of the response in the buffer of the given listener.
|
||||
*/
|
||||
private void setPayload(HttpResponseListener listener, byte[] payload) {
|
||||
listener.onContent(null, ByteBuffer.wrap(payload));
|
||||
}
|
||||
|
||||
/**
|
||||
* Run a default listener with the given result and the given payload.
|
||||
*/
|
||||
private CompletableFuture<@Nullable Content> run(Result result, byte @Nullable [] payload) {
|
||||
CompletableFuture<@Nullable Content> future = new CompletableFuture<>();
|
||||
HttpResponseListener listener = new HttpResponseListener(future, null, 1024 * 1024);
|
||||
if (null != payload) {
|
||||
setPayload(listener, payload);
|
||||
}
|
||||
run(listener, result);
|
||||
return future;
|
||||
}
|
||||
|
||||
/**
|
||||
* Run a default listener with the given result.
|
||||
*/
|
||||
private CompletableFuture<@Nullable Content> run(Result result) {
|
||||
return run(result, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Run a default listener with a default result and the given payload.
|
||||
*/
|
||||
private CompletableFuture<@Nullable Content> run(byte @Nullable [] payload) {
|
||||
return run(createResult(), payload);
|
||||
}
|
||||
|
||||
/**
|
||||
* Run a default listener with a default result.
|
||||
*/
|
||||
private CompletableFuture<@Nullable Content> run() {
|
||||
return run(createResult());
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
void init() {
|
||||
// required for the request trace
|
||||
when(response.getHeaders()).thenReturn(new HttpFields());
|
||||
}
|
||||
|
||||
// ******** Tests ******** //
|
||||
|
||||
/**
|
||||
* When an exception is thrown during the request phase, the future completes unexceptionally
|
||||
* with no value.
|
||||
*/
|
||||
@Test
|
||||
public void requestException() {
|
||||
RuntimeException requestFailure = new RuntimeException("The request failed!");
|
||||
Result result = new Result(request, requestFailure, response);
|
||||
|
||||
CompletableFuture<@Nullable Content> future = run(result);
|
||||
|
||||
assertTrue(future.isDone());
|
||||
assertFalse(future.isCompletedExceptionally());
|
||||
assertNull(future.join());
|
||||
}
|
||||
|
||||
/**
|
||||
* When an exception is thrown during the response phase, the future completes unexceptionally
|
||||
* with no value.
|
||||
*/
|
||||
@Test
|
||||
public void responseException() {
|
||||
RuntimeException responseFailure = new RuntimeException("The response failed!");
|
||||
Result result = new Result(request, response, responseFailure);
|
||||
|
||||
CompletableFuture<@Nullable Content> future = run(result);
|
||||
|
||||
assertTrue(future.isDone());
|
||||
assertFalse(future.isCompletedExceptionally());
|
||||
assertNull(future.join());
|
||||
}
|
||||
|
||||
/**
|
||||
* When the remote side does not send any payload, the future completes normally and contains a
|
||||
* empty Content.
|
||||
*/
|
||||
@Test
|
||||
public void okWithNoBody() {
|
||||
when(response.getStatus()).thenReturn(HttpStatus.OK_200);
|
||||
|
||||
CompletableFuture<@Nullable Content> future = run();
|
||||
|
||||
assertTrue(future.isDone());
|
||||
assertFalse(future.isCompletedExceptionally());
|
||||
|
||||
Content content = future.join();
|
||||
assertNotNull(content);
|
||||
assertNotNull(content.getRawContent());
|
||||
assertEquals(0, content.getRawContent().length);
|
||||
assertNull(content.getMediaType());
|
||||
}
|
||||
|
||||
/**
|
||||
* When the remote side sends a payload, the future completes normally and contains a Content
|
||||
* object with the payload.
|
||||
*/
|
||||
@Test
|
||||
public void okWithBody() {
|
||||
when(response.getStatus()).thenReturn(HttpStatus.OK_200);
|
||||
|
||||
final String textPayload = "foobar";
|
||||
CompletableFuture<@Nullable Content> future = run(textPayload.getBytes());
|
||||
|
||||
assertTrue(future.isDone());
|
||||
assertFalse(future.isCompletedExceptionally());
|
||||
|
||||
Content content = future.join();
|
||||
assertNotNull(content);
|
||||
assertNotNull(content.getRawContent());
|
||||
assertEquals(textPayload, new String(content.getRawContent()));
|
||||
assertNull(content.getMediaType());
|
||||
}
|
||||
|
||||
/**
|
||||
* When the remote side sends a payload and encoding header, the future completes normally
|
||||
* and contains a Content object with the payload. The payload gets decoded using the encoding
|
||||
* the remote sent.
|
||||
*/
|
||||
@Test
|
||||
public void okWithEncodedBody() throws UnsupportedEncodingException {
|
||||
final String encodingName = "UTF-16LE";
|
||||
final String fallbackEncodingName = "UTF-8";
|
||||
|
||||
CompletableFuture<@Nullable Content> future = new CompletableFuture<>();
|
||||
HttpResponseListener listener = new HttpResponseListener(future, fallbackEncodingName, 1024 * 1024);
|
||||
|
||||
response.getHeaders().put(HttpHeader.CONTENT_TYPE, "text/plain; charset=" + encodingName);
|
||||
when(response.getRequest()).thenReturn(request);
|
||||
listener.onHeaders(response);
|
||||
|
||||
final String textPayload = "漢字編碼方法";
|
||||
setPayload(listener, textPayload.getBytes(encodingName));
|
||||
|
||||
when(response.getStatus()).thenReturn(HttpStatus.OK_200);
|
||||
run(listener);
|
||||
|
||||
assertTrue(future.isDone());
|
||||
assertFalse(future.isCompletedExceptionally());
|
||||
|
||||
Content content = future.join();
|
||||
assertNotNull(content);
|
||||
assertNotNull(content.getRawContent());
|
||||
assertEquals(textPayload, new String(content.getRawContent(), encodingName));
|
||||
assertEquals(textPayload, content.getAsString());
|
||||
assertEquals("text/plain", content.getMediaType());
|
||||
}
|
||||
|
||||
/**
|
||||
* When the remote side sends a payload but no encoding, the future completes normally and
|
||||
* contains a Content object with the payload. The payload gets decoded using the fallback
|
||||
* encoding of the listener.
|
||||
*/
|
||||
@Test
|
||||
public void okWithEncodedBodyFallback() throws UnsupportedEncodingException {
|
||||
final String encodingName = "UTF-16BE";
|
||||
|
||||
CompletableFuture<@Nullable Content> future = new CompletableFuture<>();
|
||||
HttpResponseListener listener = new HttpResponseListener(future, encodingName, 1024 * 1024);
|
||||
|
||||
final String textPayload = "汉字编码方法";
|
||||
setPayload(listener, textPayload.getBytes(encodingName));
|
||||
|
||||
when(response.getStatus()).thenReturn(HttpStatus.OK_200);
|
||||
run(listener);
|
||||
|
||||
assertTrue(future.isDone());
|
||||
assertFalse(future.isCompletedExceptionally());
|
||||
|
||||
Content content = future.join();
|
||||
assertNotNull(content);
|
||||
assertNotNull(content.getRawContent());
|
||||
assertEquals(textPayload, new String(content.getRawContent(), encodingName));
|
||||
assertEquals(textPayload, content.getAsString());
|
||||
assertNull(content.getMediaType());
|
||||
}
|
||||
|
||||
/**
|
||||
* When the remote side response with a HTTP/204 and no payload, the future completes normally
|
||||
* and contains an empty Content.
|
||||
*/
|
||||
@Test
|
||||
public void nocontent() {
|
||||
when(response.getStatus()).thenReturn(HttpStatus.NO_CONTENT_204);
|
||||
|
||||
CompletableFuture<@Nullable Content> future = run();
|
||||
|
||||
assertTrue(future.isDone());
|
||||
assertFalse(future.isCompletedExceptionally());
|
||||
|
||||
Content content = future.join();
|
||||
assertNotNull(content);
|
||||
assertNotNull(content.getRawContent());
|
||||
assertEquals(0, content.getRawContent().length);
|
||||
assertNull(content.getMediaType());
|
||||
}
|
||||
|
||||
/**
|
||||
* When the remote side response with a HTTP/401, the future completes exceptionally with a
|
||||
* HttpAuthException.
|
||||
*/
|
||||
@Test
|
||||
public void unauthorized() {
|
||||
when(response.getStatus()).thenReturn(HttpStatus.UNAUTHORIZED_401);
|
||||
|
||||
CompletableFuture<@Nullable Content> future = run();
|
||||
|
||||
assertTrue(future.isDone());
|
||||
assertTrue(future.isCompletedExceptionally());
|
||||
|
||||
@Nullable
|
||||
CompletionException exceptionWrapper = assertThrows(CompletionException.class, () -> future.join());
|
||||
assertNotNull(exceptionWrapper);
|
||||
|
||||
Throwable exception = exceptionWrapper.getCause();
|
||||
assertNotNull(exception);
|
||||
assertTrue(exception instanceof HttpAuthException);
|
||||
}
|
||||
|
||||
/**
|
||||
* When the remote side responds with anything we don't expect (in this case a HTTP/500), the
|
||||
* future completes exceptionally with an IllegalStateException.
|
||||
*/
|
||||
@Test
|
||||
public void unexpectedStatus() {
|
||||
when(response.getStatus()).thenReturn(HttpStatus.INTERNAL_SERVER_ERROR_500);
|
||||
|
||||
CompletableFuture<@Nullable Content> future = run();
|
||||
|
||||
assertTrue(future.isDone());
|
||||
assertTrue(future.isCompletedExceptionally());
|
||||
|
||||
@Nullable
|
||||
CompletionException exceptionWrapper = assertThrows(CompletionException.class, () -> future.join());
|
||||
assertNotNull(exceptionWrapper);
|
||||
|
||||
Throwable exception = exceptionWrapper.getCause();
|
||||
assertNotNull(exception);
|
||||
assertTrue(exception instanceof IllegalStateException);
|
||||
assertEquals("Response - Code500", exception.getMessage());
|
||||
}
|
||||
}
|
@ -1,2 +0,0 @@
|
||||
# to run through all code-branches
|
||||
org.slf4j.simpleLogger.log.org.openhab.binding.http=trace
|
Loading…
Reference in New Issue
Block a user