mirror of
https://github.com/openhab/openhab-addons.git
synced 2025-01-27 07:41:39 +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
|
## Thing Configuration
|
||||||
|
|
||||||
| parameter | optional | default | description |
|
| parameter | optional | default | description |
|
||||||
|-------------------|----------|---------|-------------|
|
|-----------------------|----------|---------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||||
| `baseURL` | no | - | The base URL for this thing. Can be extended in channel-configuration. |
|
| `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. |
|
| `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. |
|
| `timeout` | no | 3000 | Timeout for HTTP requests in ms. |
|
||||||
| `bufferSize` | no | 2048 | The buffer size for the response data (in kB). |
|
| `bufferSize` | no | 2048 | The buffer size for the response data (in kB). |
|
||||||
| `delay` | no | 0 | Delay between two requests in ms (advanced parameter). |
|
| `delay` | no | 0 | Delay between two requests in ms (advanced parameter). |
|
||||||
| `username` | yes | - | Username for authentication (advanced parameter). |
|
| `username` | yes | - | Username for authentication (advanced parameter). |
|
||||||
| `password` | yes | - | Password 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` or `DIGEST` (advanced parameter). |
|
| `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`. |
|
| `stateMethod` | no | GET | Method used for requesting the state: `GET`, `PUT`, `POST`. |
|
||||||
| `commandMethod` | no | GET | Method used for sending commands: `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`. |
|
| `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). |
|
| `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.|
|
| `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.|
|
| `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.
|
_Note:_ The `BASIC_PREEMPTIVE` mode adds basic authentication headers even if the server did not request authentication.
|
||||||
This is dangerous and might be misused.
|
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.
|
_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.
|
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).
|
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
|
## 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.
|
Each item type has its own channel-type.
|
||||||
Depending on the channel-type, channels have different configuration options.
|
Depending on the channel-type, channels have different configuration options.
|
||||||
All channel-types (except `image`) have `stateExtension`, `commandExtension`, `stateTransformation`, `commandTransformation` and `mode` parameters.
|
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 |
|
| parameter | optional | default | description |
|
||||||
|-------------------------|----------|-------------|-------------|
|
|-------------------------|----------|-------------|-----------------------------------------------------------------------------------------------------------|
|
||||||
| `stateExtension` | yes | - | Appended to the `baseURL` for requesting states. |
|
| `stateExtension` | yes | - | Appended to the `baseURL` for requesting states. |
|
||||||
| `commandExtension` | yes | - | Appended to the `baseURL` for sending commands. If empty, same as `stateExtension`. |
|
| `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. |
|
| `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. |
|
| `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`) |
|
||||||
| `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. |
|
| `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.
|
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.
|
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`
|
### `color`
|
||||||
|
|
||||||
| parameter | optional | default | description |
|
| parameter | optional | default | description |
|
||||||
|-------------------------|----------|-------------|-------------|
|
|-----------------|----------|---------|---------------------------------------------------------------------------|
|
||||||
| `onValue` | yes | - | A special value that represents `ON` |
|
| `onValue` | yes | - | A special value that represents `ON` |
|
||||||
| `offValue` | yes | - | A special value that represents `OFF` |
|
| `offValue` | yes | - | A special value that represents `OFF` |
|
||||||
| `increaseValue` | yes | - | A special value that represents `INCREASE` |
|
| `increaseValue` | yes | - | A special value that represents `INCREASE` |
|
||||||
| `decreaseValue` | yes | - | A special value that represents `DECREASE` |
|
| `decreaseValue` | yes | - | A special value that represents `DECREASE` |
|
||||||
| `step` | no | 1 | The amount the brightness is increased/decreased on `INCREASE`/`DECREASE` |
|
| `step` | no | 1 | The amount the brightness is increased/decreased on `INCREASE`/`DECREASE` |
|
||||||
| `colorMode` | no | RGB | Mode for color values: `RGB` or `HSB` |
|
| `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`.
|
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`
|
### `contact`
|
||||||
|
|
||||||
| parameter | optional | default | description |
|
| parameter | optional | default | description |
|
||||||
|-------------------------|----------|-------------|-------------|
|
|---------------|----------|---------|------------------------------------------|
|
||||||
| `openValue` | no | - | A special value that represents `OPEN` |
|
| `openValue` | no | - | A special value that represents `OPEN` |
|
||||||
| `closedValue` | no | - | A special value that represents `CLOSED` |
|
| `closedValue` | no | - | A special value that represents `CLOSED` |
|
||||||
|
|
||||||
### `dimmer`
|
### `dimmer`
|
||||||
|
|
||||||
| parameter | optional | default | description |
|
| parameter | optional | default | description |
|
||||||
|-------------------------|----------|-------------|-------------|
|
|-----------------|----------|---------|---------------------------------------------------------------------------|
|
||||||
| `onValue` | yes | - | A special value that represents `ON` |
|
| `onValue` | yes | - | A special value that represents `ON` |
|
||||||
| `offValue` | yes | - | A special value that represents `OFF` |
|
| `offValue` | yes | - | A special value that represents `OFF` |
|
||||||
| `increaseValue` | yes | - | A special value that represents `INCREASE` |
|
| `increaseValue` | yes | - | A special value that represents `INCREASE` |
|
||||||
| `decreaseValue` | yes | - | A special value that represents `DECREASE` |
|
| `decreaseValue` | yes | - | A special value that represents `DECREASE` |
|
||||||
| `step` | no | 1 | The amount the brightness is increased/decreased on `INCREASE`/`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.
|
All values that are not `onValue`, `offValue`, `increaseValue`, `decreaseValue` are interpreted as brightness 0-100% and need to be numeric only.
|
||||||
|
|
||||||
### `number`
|
### `number`
|
||||||
|
|
||||||
| parameter | optional | default | description |
|
| parameter | optional | default | description |
|
||||||
|-------------------------|----------|-------------|-------------|
|
|-----------|----------|---------|---------------------------------|
|
||||||
| `unit` | yes | - | The unit label for this channel |
|
| `unit` | yes | - | The unit label for this channel |
|
||||||
|
|
||||||
`number` channels can be used for `DecimalType` or `QuantityType` values.
|
`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`.
|
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`
|
### `player`
|
||||||
|
|
||||||
| parameter | optional | default | description |
|
| parameter | optional | default | description |
|
||||||
|-------------------------|----------|-------------|-------------|
|
|--------------------|----------|---------|-----------------------------------------------|
|
||||||
| `play` | yes | - | A special value that represents `PLAY` |
|
| `playValue` | yes | - | A special value that represents `PLAY` |
|
||||||
| `pause` | yes | - | A special value that represents `PAUSE` |
|
| `pauseValue` | yes | - | A special value that represents `PAUSE` |
|
||||||
| `next` | yes | - | A special value that represents `NEXT` |
|
| `nextValue` | yes | - | A special value that represents `NEXT` |
|
||||||
| `previous` | yes | - | A special value that represents `PREVIOUS` |
|
| `previousValue` | yes | - | A special value that represents `PREVIOUS` |
|
||||||
| `fastforward` | yes | - | A special value that represents `FASTFORWARD` |
|
| `fastforwardValue` | yes | - | A special value that represents `FASTFORWARD` |
|
||||||
| `rewind` | yes | - | A special value that represents `REWIND` |
|
| `rewindValue` | yes | - | A special value that represents `REWIND` |
|
||||||
|
|
||||||
### `rollershutter`
|
### `rollershutter`
|
||||||
|
|
||||||
| parameter | optional | default | description |
|
| parameter | optional | default | description |
|
||||||
|-------------------------|----------|-------------|-------------|
|
|-------------|----------|---------|----------------------------------------|
|
||||||
| `upValue` | yes | - | A special value that represents `UP` |
|
| `upValue` | yes | - | A special value that represents `UP` |
|
||||||
| `downValue` | yes | - | A special value that represents `DOWN` |
|
| `downValue` | yes | - | A special value that represents `DOWN` |
|
||||||
| `stopValue` | yes | - | A special value that represents `STOP` |
|
| `stopValue` | yes | - | A special value that represents `STOP` |
|
||||||
| `moveValue` | yes | - | A special value that represents `MOVE` |
|
| `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.
|
All values that are not `upValue`, `downValue`, `stopValue`, `moveValue` are interpreted as position 0-100% and need to be numeric only.
|
||||||
|
|
||||||
### `switch`
|
### `switch`
|
||||||
|
|
||||||
| parameter | optional | default | description |
|
| parameter | optional | default | description |
|
||||||
|-------------------------|----------|-------------|-------------|
|
|------------|----------|---------|---------------------------------------|
|
||||||
| `onValue` | no | - | A special value that represents `ON` |
|
| `onValue` | no | - | A special value that represents `ON` |
|
||||||
| `offValue` | no | - | A special value that represents `OFF` |
|
| `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.
|
**Note:** Special values need to be exact matches, i.e. no leading or trailing characters and comparison is case-sensitive.
|
||||||
|
|
||||||
## URL Formatting
|
## 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 URL is used as format string and two parameters are added:
|
||||||
|
|
||||||
- the current date (referenced as `%1$`)
|
- 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`).
|
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
|
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
|
is transformed to
|
||||||
|
|
||||||
```text
|
```
|
||||||
http://www.domain.org/home/lights/23871/?status=OFF&date=2020-07-06
|
http://www.domain.org/home/lights/23871/?status=OFF&date=2020-07-06
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -183,6 +184,6 @@ Thing http:url:foo "Foo" [
|
|||||||
headers="key1=value1", "key2=value2", "key3=value3",
|
headers="key1=value1", "key2=value2", "key3=value3",
|
||||||
refresh=15] {
|
refresh=15] {
|
||||||
Channels:
|
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>
|
<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>
|
</project>
|
||||||
|
@ -14,6 +14,7 @@ package org.openhab.binding.http.internal;
|
|||||||
|
|
||||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||||
import org.openhab.core.thing.ThingTypeUID;
|
import org.openhab.core.thing.ThingTypeUID;
|
||||||
|
import org.openhab.core.thing.type.ChannelTypeUID;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The {@link HttpBindingConstants} class defines common constants, which are
|
* The {@link HttpBindingConstants} class defines common constants, which are
|
||||||
@ -23,8 +24,12 @@ import org.openhab.core.thing.ThingTypeUID;
|
|||||||
*/
|
*/
|
||||||
@NonNullByDefault
|
@NonNullByDefault
|
||||||
public class HttpBindingConstants {
|
public class HttpBindingConstants {
|
||||||
|
private static final String BINDING_ID = "http";
|
||||||
public static final String BINDING_ID = "http";
|
|
||||||
|
|
||||||
public static final ThingTypeUID THING_TYPE_URL = new ThingTypeUID(BINDING_ID, "url");
|
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;
|
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;
|
import java.util.Set;
|
||||||
|
|
||||||
@ -20,17 +20,13 @@ import org.eclipse.jdt.annotation.NonNullByDefault;
|
|||||||
import org.eclipse.jdt.annotation.Nullable;
|
import org.eclipse.jdt.annotation.Nullable;
|
||||||
import org.eclipse.jetty.client.HttpClient;
|
import org.eclipse.jetty.client.HttpClient;
|
||||||
import org.eclipse.jetty.util.ssl.SslContextFactory;
|
import org.eclipse.jetty.util.ssl.SslContextFactory;
|
||||||
import org.openhab.binding.http.internal.transform.CascadedValueTransformationImpl;
|
import org.openhab.core.i18n.TimeZoneProvider;
|
||||||
import org.openhab.binding.http.internal.transform.NoOpValueTransformation;
|
|
||||||
import org.openhab.binding.http.internal.transform.ValueTransformation;
|
|
||||||
import org.openhab.binding.http.internal.transform.ValueTransformationProvider;
|
|
||||||
import org.openhab.core.io.net.http.HttpClientFactory;
|
import org.openhab.core.io.net.http.HttpClientFactory;
|
||||||
import org.openhab.core.thing.Thing;
|
import org.openhab.core.thing.Thing;
|
||||||
import org.openhab.core.thing.ThingTypeUID;
|
import org.openhab.core.thing.ThingTypeUID;
|
||||||
import org.openhab.core.thing.binding.BaseThingHandlerFactory;
|
import org.openhab.core.thing.binding.BaseThingHandlerFactory;
|
||||||
import org.openhab.core.thing.binding.ThingHandler;
|
import org.openhab.core.thing.binding.ThingHandler;
|
||||||
import org.openhab.core.thing.binding.ThingHandlerFactory;
|
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.Activate;
|
||||||
import org.osgi.service.component.annotations.Component;
|
import org.osgi.service.component.annotations.Component;
|
||||||
import org.osgi.service.component.annotations.Deactivate;
|
import org.osgi.service.component.annotations.Deactivate;
|
||||||
@ -46,8 +42,7 @@ import org.slf4j.LoggerFactory;
|
|||||||
*/
|
*/
|
||||||
@NonNullByDefault
|
@NonNullByDefault
|
||||||
@Component(configurationPid = "binding.http", service = ThingHandlerFactory.class)
|
@Component(configurationPid = "binding.http", service = ThingHandlerFactory.class)
|
||||||
public class HttpHandlerFactory extends BaseThingHandlerFactory
|
public class HttpHandlerFactory extends BaseThingHandlerFactory implements HttpClientProvider {
|
||||||
implements ValueTransformationProvider, HttpClientProvider {
|
|
||||||
private static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Set.of(THING_TYPE_URL);
|
private static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Set.of(THING_TYPE_URL);
|
||||||
private final Logger logger = LoggerFactory.getLogger(HttpHandlerFactory.class);
|
private final Logger logger = LoggerFactory.getLogger(HttpHandlerFactory.class);
|
||||||
|
|
||||||
@ -55,22 +50,27 @@ public class HttpHandlerFactory extends BaseThingHandlerFactory
|
|||||||
private final HttpClient insecureClient;
|
private final HttpClient insecureClient;
|
||||||
|
|
||||||
private final HttpDynamicStateDescriptionProvider httpDynamicStateDescriptionProvider;
|
private final HttpDynamicStateDescriptionProvider httpDynamicStateDescriptionProvider;
|
||||||
|
private final TimeZoneProvider timeZoneProvider;
|
||||||
|
|
||||||
@Activate
|
@Activate
|
||||||
public HttpHandlerFactory(@Reference HttpClientFactory httpClientFactory,
|
public HttpHandlerFactory(@Reference HttpClientFactory httpClientFactory,
|
||||||
@Reference HttpDynamicStateDescriptionProvider httpDynamicStateDescriptionProvider) {
|
@Reference HttpDynamicStateDescriptionProvider httpDynamicStateDescriptionProvider,
|
||||||
this.secureClient = httpClientFactory.createHttpClient(BINDING_ID + "-secure", new SslContextFactory.Client());
|
@Reference TimeZoneProvider timeZoneProvider) {
|
||||||
this.insecureClient = httpClientFactory.createHttpClient(BINDING_ID + "-insecure",
|
this.secureClient = new HttpClient(new SslContextFactory.Client());
|
||||||
new SslContextFactory.Client(true));
|
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 {
|
try {
|
||||||
this.secureClient.start();
|
this.secureClient.start();
|
||||||
this.insecureClient.start();
|
this.insecureClient.start();
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
// catching exception is necessary due to the signature of HttpClient.start()
|
// catching exception is necessary due to the signature of HttpClient.start()
|
||||||
logger.warn("Failed to start insecure http client: {}", e.getMessage());
|
logger.warn("Failed to start http client: {}", e.getMessage());
|
||||||
throw new IllegalStateException("Could not create insecure HttpClient");
|
throw new IllegalStateException("Could not create HttpClient", e);
|
||||||
}
|
}
|
||||||
this.httpDynamicStateDescriptionProvider = httpDynamicStateDescriptionProvider;
|
this.httpDynamicStateDescriptionProvider = httpDynamicStateDescriptionProvider;
|
||||||
|
this.timeZoneProvider = timeZoneProvider;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Deactivate
|
@Deactivate
|
||||||
@ -94,21 +94,12 @@ public class HttpHandlerFactory extends BaseThingHandlerFactory
|
|||||||
ThingTypeUID thingTypeUID = thing.getThingTypeUID();
|
ThingTypeUID thingTypeUID = thing.getThingTypeUID();
|
||||||
|
|
||||||
if (THING_TYPE_URL.equals(thingTypeUID)) {
|
if (THING_TYPE_URL.equals(thingTypeUID)) {
|
||||||
return new HttpThingHandler(thing, this, this, httpDynamicStateDescriptionProvider);
|
return new HttpThingHandler(thing, this, httpDynamicStateDescriptionProvider, timeZoneProvider);
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
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
|
@Override
|
||||||
public HttpClient getSecureClient() {
|
public HttpClient getSecureClient() {
|
||||||
return secureClient;
|
return secureClient;
|
||||||
|
@ -12,13 +12,20 @@
|
|||||||
*/
|
*/
|
||||||
package org.openhab.binding.http.internal;
|
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.MalformedURLException;
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
import java.net.URISyntaxException;
|
import java.net.URISyntaxException;
|
||||||
|
import java.time.Instant;
|
||||||
import java.util.Base64;
|
import java.util.Base64;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.CompletableFuture;
|
||||||
import java.util.concurrent.TimeUnit;
|
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.NonNullByDefault;
|
||||||
import org.eclipse.jdt.annotation.Nullable;
|
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.Authentication;
|
||||||
import org.eclipse.jetty.client.api.AuthenticationStore;
|
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.BasicAuthentication;
|
||||||
import org.eclipse.jetty.client.util.DigestAuthentication;
|
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.HttpChannelConfig;
|
||||||
import org.openhab.binding.http.internal.config.HttpChannelMode;
|
|
||||||
import org.openhab.binding.http.internal.config.HttpThingConfig;
|
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.HttpAuthException;
|
||||||
import org.openhab.binding.http.internal.http.HttpResponseListener;
|
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.RateLimitedHttpClient;
|
||||||
import org.openhab.binding.http.internal.http.RefreshingUrlCache;
|
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.DateTimeType;
|
||||||
import org.openhab.core.library.types.PointType;
|
import org.openhab.core.library.types.PointType;
|
||||||
import org.openhab.core.library.types.StringType;
|
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.ThingStatus;
|
||||||
import org.openhab.core.thing.ThingStatusDetail;
|
import org.openhab.core.thing.ThingStatusDetail;
|
||||||
import org.openhab.core.thing.binding.BaseThingHandler;
|
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.Command;
|
||||||
import org.openhab.core.types.RefreshType;
|
import org.openhab.core.types.RefreshType;
|
||||||
import org.openhab.core.types.State;
|
import org.openhab.core.types.State;
|
||||||
@ -77,35 +82,33 @@ import org.slf4j.LoggerFactory;
|
|||||||
* @author Jan N. Klug - Initial contribution
|
* @author Jan N. Klug - Initial contribution
|
||||||
*/
|
*/
|
||||||
@NonNullByDefault
|
@NonNullByDefault
|
||||||
public class HttpThingHandler extends BaseThingHandler {
|
public class HttpThingHandler extends BaseThingHandler implements HttpStatusListener {
|
||||||
private static final Set<Character> URL_PART_DELIMITER = Set.of('/', '?', '&');
|
private static final Set<Character> URL_PART_DELIMITER = Set.of('/', '?', '&');
|
||||||
|
|
||||||
private final Logger logger = LoggerFactory.getLogger(HttpThingHandler.class);
|
private final Logger logger = LoggerFactory.getLogger(HttpThingHandler.class);
|
||||||
private final ValueTransformationProvider valueTransformationProvider;
|
|
||||||
private final HttpClientProvider httpClientProvider;
|
private final HttpClientProvider httpClientProvider;
|
||||||
private HttpClient httpClient;
|
private final RateLimitedHttpClient rateLimitedHttpClient;
|
||||||
private RateLimitedHttpClient rateLimitedHttpClient;
|
|
||||||
private final HttpDynamicStateDescriptionProvider httpDynamicStateDescriptionProvider;
|
private final HttpDynamicStateDescriptionProvider httpDynamicStateDescriptionProvider;
|
||||||
|
private final TimeZoneProvider timeZoneProvider;
|
||||||
|
|
||||||
private HttpThingConfig config = new HttpThingConfig();
|
private HttpThingConfig config = new HttpThingConfig();
|
||||||
private final Map<String, RefreshingUrlCache> urlHandlers = new HashMap<>();
|
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<>();
|
private final Map<ChannelUID, String> channelUrls = new HashMap<>();
|
||||||
|
|
||||||
public HttpThingHandler(Thing thing, HttpClientProvider httpClientProvider,
|
public HttpThingHandler(Thing thing, HttpClientProvider httpClientProvider,
|
||||||
ValueTransformationProvider valueTransformationProvider,
|
HttpDynamicStateDescriptionProvider httpDynamicStateDescriptionProvider,
|
||||||
HttpDynamicStateDescriptionProvider httpDynamicStateDescriptionProvider) {
|
TimeZoneProvider timeZoneProvider) {
|
||||||
super(thing);
|
super(thing);
|
||||||
this.httpClientProvider = httpClientProvider;
|
this.httpClientProvider = httpClientProvider;
|
||||||
this.httpClient = httpClientProvider.getSecureClient();
|
this.rateLimitedHttpClient = new RateLimitedHttpClient(httpClientProvider.getSecureClient(), scheduler);
|
||||||
this.rateLimitedHttpClient = new RateLimitedHttpClient(httpClient, scheduler);
|
|
||||||
this.valueTransformationProvider = valueTransformationProvider;
|
|
||||||
this.httpDynamicStateDescriptionProvider = httpDynamicStateDescriptionProvider;
|
this.httpDynamicStateDescriptionProvider = httpDynamicStateDescriptionProvider;
|
||||||
|
this.timeZoneProvider = timeZoneProvider;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void handleCommand(ChannelUID channelUID, Command command) {
|
public void handleCommand(ChannelUID channelUID, Command command) {
|
||||||
ItemValueConverter itemValueConverter = channels.get(channelUID);
|
ChannelHandler itemValueConverter = channels.get(channelUID);
|
||||||
if (itemValueConverter == null) {
|
if (itemValueConverter == null) {
|
||||||
logger.warn("Cannot find channel implementation for channel {}.", channelUID);
|
logger.warn("Cannot find channel implementation for channel {}.", channelUID);
|
||||||
return;
|
return;
|
||||||
@ -117,7 +120,11 @@ public class HttpThingHandler extends BaseThingHandler {
|
|||||||
RefreshingUrlCache refreshingUrlCache = urlHandlers.get(key);
|
RefreshingUrlCache refreshingUrlCache = urlHandlers.get(key);
|
||||||
if (refreshingUrlCache != null) {
|
if (refreshingUrlCache != null) {
|
||||||
try {
|
try {
|
||||||
refreshingUrlCache.get().ifPresent(itemValueConverter::process);
|
refreshingUrlCache.get().ifPresentOrElse(itemValueConverter::process, () -> {
|
||||||
|
if (config.strictErrorHandling) {
|
||||||
|
itemValueConverter.process(null);
|
||||||
|
}
|
||||||
|
});
|
||||||
} catch (IllegalArgumentException | IllegalStateException e) {
|
} catch (IllegalArgumentException | IllegalStateException e) {
|
||||||
logger.warn("Failed processing REFRESH command for channel {}: {}", channelUID, e.getMessage());
|
logger.warn("Failed processing REFRESH command for channel {}: {}", channelUID, e.getMessage());
|
||||||
}
|
}
|
||||||
@ -144,40 +151,68 @@ public class HttpThingHandler extends BaseThingHandler {
|
|||||||
return;
|
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
|
// check SSL handling and initialize client
|
||||||
if (config.ignoreSSLErrors) {
|
if (config.ignoreSSLErrors) {
|
||||||
logger.info("Using the insecure client for thing '{}'.", thing.getUID());
|
logger.info("Using the insecure client for thing '{}'.", thing.getUID());
|
||||||
httpClient = httpClientProvider.getInsecureClient();
|
rateLimitedHttpClient.setHttpClient(httpClientProvider.getInsecureClient());
|
||||||
} else {
|
} else {
|
||||||
logger.info("Using the secure client for thing '{}'.", thing.getUID());
|
logger.info("Using the secure client for thing '{}'.", thing.getUID());
|
||||||
httpClient = httpClientProvider.getSecureClient();
|
rateLimitedHttpClient.setHttpClient(httpClientProvider.getSecureClient());
|
||||||
}
|
}
|
||||||
rateLimitedHttpClient.setHttpClient(httpClient);
|
|
||||||
rateLimitedHttpClient.setDelay(config.delay);
|
rateLimitedHttpClient.setDelay(config.delay);
|
||||||
|
|
||||||
int channelCount = thing.getChannels().size();
|
int urlHandlerCount = urlHandlers.size();
|
||||||
if (channelCount * config.delay > config.refresh * 1000) {
|
if (urlHandlerCount * config.delay > config.refresh * 1000) {
|
||||||
// this should prevent the rate limit queue from filling up
|
// 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(
|
logger.warn(
|
||||||
"{} channels in thing {} with a delay of {} incompatible with the configured refresh time. Refresh-Time increased to the minimum of {}",
|
"{} 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
|
// remove empty headers
|
||||||
config.headers.removeIf(String::isBlank);
|
config.headers.removeIf(String::isBlank);
|
||||||
|
|
||||||
// configure authentication
|
// configure authentication
|
||||||
if (!config.username.isEmpty()) {
|
try {
|
||||||
try {
|
AuthenticationStore authStore = rateLimitedHttpClient.getAuthenticationStore();
|
||||||
AuthenticationStore authStore = httpClient.getAuthenticationStore();
|
URI uri = new URI(config.baseURL);
|
||||||
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) {
|
switch (config.authMode) {
|
||||||
case BASIC_PREEMPTIVE:
|
case BASIC_PREEMPTIVE:
|
||||||
config.headers.add("Authorization=Basic " + Base64.getEncoder()
|
config.headers.add("Authorization=Basic " + Base64.getEncoder()
|
||||||
.encodeToString((config.username + ":" + config.password).getBytes()));
|
.encodeToString((config.username + ":" + config.password).getBytes()));
|
||||||
logger.debug("Preemptive Basic Authentication configured for thing '{}'", thing.getUID());
|
logger.debug("Preemptive Basic Authentication configured for thing '{}'", thing.getUID());
|
||||||
break;
|
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:
|
case BASIC:
|
||||||
authStore.addAuthentication(new BasicAuthentication(uri, Authentication.ANY_REALM,
|
authStore.addAuthentication(new BasicAuthentication(uri, Authentication.ANY_REALM,
|
||||||
config.username, config.password));
|
config.username, config.password));
|
||||||
@ -192,18 +227,16 @@ public class HttpThingHandler extends BaseThingHandler {
|
|||||||
logger.warn("Unknown authentication method '{}' for thing '{}'", config.authMode,
|
logger.warn("Unknown authentication method '{}' for thing '{}'", config.authMode,
|
||||||
thing.getUID());
|
thing.getUID());
|
||||||
}
|
}
|
||||||
} catch (URISyntaxException e) {
|
} else {
|
||||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
|
logger.debug("No authentication configured for thing '{}'", thing.getUID());
|
||||||
"failed to create authentication: baseUrl is invalid");
|
|
||||||
}
|
}
|
||||||
} else {
|
} catch (URISyntaxException e) {
|
||||||
logger.debug("No authentication configured for thing '{}'", thing.getUID());
|
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Cannot create URI from baseUrl.");
|
||||||
}
|
}
|
||||||
|
|
||||||
// create channels
|
// create channels
|
||||||
thing.getChannels().forEach(this::createChannel);
|
thing.getChannels().forEach(this::createChannel);
|
||||||
|
|
||||||
updateStatus(ThingStatus.ONLINE);
|
updateStatus(ThingStatus.UNKNOWN);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -229,6 +262,10 @@ public class HttpThingHandler extends BaseThingHandler {
|
|||||||
* @param channel a thing channel
|
* @param channel a thing channel
|
||||||
*/
|
*/
|
||||||
private void createChannel(Channel 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();
|
ChannelUID channelUID = channel.getUID();
|
||||||
HttpChannelConfig channelConfig = channel.getConfiguration().as(HttpChannelConfig.class);
|
HttpChannelConfig channelConfig = channel.getConfiguration().as(HttpChannelConfig.class);
|
||||||
|
|
||||||
@ -242,45 +279,46 @@ public class HttpThingHandler extends BaseThingHandler {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
ItemValueConverter itemValueConverter;
|
ChannelHandler itemValueConverter;
|
||||||
switch (acceptedItemType) {
|
switch (acceptedItemType) {
|
||||||
case "Color":
|
case "Color":
|
||||||
itemValueConverter = createItemConverter(ColorItemConverter::new, commandUrl, channelUID,
|
itemValueConverter = createChannelHandler(ColorChannelHandler::new, commandUrl, channelUID,
|
||||||
channelConfig);
|
channelConfig);
|
||||||
break;
|
break;
|
||||||
case "DateTime":
|
case "DateTime":
|
||||||
itemValueConverter = createGenericItemConverter(commandUrl, channelUID, channelConfig,
|
itemValueConverter = createGenericChannelHandler(commandUrl, channelUID, channelConfig,
|
||||||
DateTimeType::new);
|
DateTimeType::new);
|
||||||
break;
|
break;
|
||||||
case "Dimmer":
|
case "Dimmer":
|
||||||
itemValueConverter = createItemConverter(DimmerItemConverter::new, commandUrl, channelUID,
|
itemValueConverter = createChannelHandler(DimmerChannelHandler::new, commandUrl, channelUID,
|
||||||
channelConfig);
|
channelConfig);
|
||||||
break;
|
break;
|
||||||
case "Contact":
|
case "Contact":
|
||||||
case "Switch":
|
case "Switch":
|
||||||
itemValueConverter = createItemConverter(FixedValueMappingItemConverter::new, commandUrl, channelUID,
|
itemValueConverter = createChannelHandler(FixedValueMappingChannelHandler::new, commandUrl, channelUID,
|
||||||
channelConfig);
|
channelConfig);
|
||||||
break;
|
break;
|
||||||
case "Image":
|
case "Image":
|
||||||
itemValueConverter = new ImageItemConverter(state -> updateState(channelUID, state));
|
itemValueConverter = new ImageChannelHandler(state -> updateState(channelUID, state));
|
||||||
break;
|
break;
|
||||||
case "Location":
|
case "Location":
|
||||||
itemValueConverter = createGenericItemConverter(commandUrl, channelUID, channelConfig, PointType::new);
|
itemValueConverter = createGenericChannelHandler(commandUrl, channelUID, channelConfig, PointType::new);
|
||||||
break;
|
break;
|
||||||
case "Number":
|
case "Number":
|
||||||
itemValueConverter = createItemConverter(NumberItemConverter::new, commandUrl, channelUID,
|
itemValueConverter = createChannelHandler(NumberChannelHandler::new, commandUrl, channelUID,
|
||||||
channelConfig);
|
channelConfig);
|
||||||
break;
|
break;
|
||||||
case "Player":
|
case "Player":
|
||||||
itemValueConverter = createItemConverter(PlayerItemConverter::new, commandUrl, channelUID,
|
itemValueConverter = createChannelHandler(PlayerChannelHandler::new, commandUrl, channelUID,
|
||||||
channelConfig);
|
channelConfig);
|
||||||
break;
|
break;
|
||||||
case "Rollershutter":
|
case "Rollershutter":
|
||||||
itemValueConverter = createItemConverter(RollershutterItemConverter::new, commandUrl, channelUID,
|
itemValueConverter = createChannelHandler(RollershutterChannelHandler::new, commandUrl, channelUID,
|
||||||
channelConfig);
|
channelConfig);
|
||||||
break;
|
break;
|
||||||
case "String":
|
case "String":
|
||||||
itemValueConverter = createGenericItemConverter(commandUrl, channelUID, channelConfig, StringType::new);
|
itemValueConverter = createGenericChannelHandler(commandUrl, channelUID, channelConfig,
|
||||||
|
StringType::new);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
logger.warn("Unsupported item-type '{}'", channel.getAcceptedItemType());
|
logger.warn("Unsupported item-type '{}'", channel.getAcceptedItemType());
|
||||||
@ -288,80 +326,75 @@ public class HttpThingHandler extends BaseThingHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
channels.put(channelUID, itemValueConverter);
|
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
|
// 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;
|
String key = channelConfig.stateContent + "$" + stateUrl;
|
||||||
channelUrls.put(channelUID, key);
|
channelUrls.put(channelUID, key);
|
||||||
urlHandlers
|
Objects.requireNonNull(urlHandlers.computeIfAbsent(key,
|
||||||
.computeIfAbsent(key,
|
k -> new RefreshingUrlCache(scheduler, rateLimitedHttpClient, stateUrl, config,
|
||||||
k -> new RefreshingUrlCache(scheduler, rateLimitedHttpClient, stateUrl,
|
channelConfig.stateContent, config.contentType, this)))
|
||||||
channelConfig.escapedUrl, config, channelConfig.stateContent))
|
|
||||||
.addConsumer(itemValueConverter::process);
|
.addConsumer(itemValueConverter::process);
|
||||||
}
|
}
|
||||||
|
|
||||||
StateDescription stateDescription = StateDescriptionFragmentBuilder.create()
|
StateDescription stateDescription = StateDescriptionFragmentBuilder.create()
|
||||||
.withReadOnly(channelConfig.mode == HttpChannelMode.READONLY).build().toStateDescription();
|
.withReadOnly(channelConfig.mode == ChannelMode.READONLY).build().toStateDescription();
|
||||||
if (stateDescription != null) {
|
if (stateDescription != null) {
|
||||||
// if the state description is not available, we don't need to add it
|
// if the state description is not available, we don't need to add it
|
||||||
httpDynamicStateDescriptionProvider.setDescription(channelUID, stateDescription);
|
httpDynamicStateDescriptionProvider.setDescription(channelUID, stateDescription);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void sendHttpValue(String commandUrl, boolean escapedUrl, String command) {
|
@Override
|
||||||
sendHttpValue(commandUrl, escapedUrl, command, false);
|
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 {
|
try {
|
||||||
// format URL
|
// format URL
|
||||||
String url = String.format(commandUrl, new Date(), command);
|
URI uri = Util.uriFromString(String.format(commandUrl, new Date(), command));
|
||||||
URI uri = escapedUrl ? new URI(url) : Util.uriFromString(url);
|
|
||||||
|
|
||||||
// build request
|
// build request
|
||||||
Request request = httpClient.newRequest(uri).timeout(config.timeout, TimeUnit.MILLISECONDS)
|
rateLimitedHttpClient.newPriorityRequest(uri, config.commandMethod, command, config.contentType)
|
||||||
.method(config.commandMethod);
|
.thenAccept(request -> {
|
||||||
if (config.commandMethod != HttpMethod.GET) {
|
request.timeout(config.timeout, TimeUnit.MILLISECONDS);
|
||||||
final String contentType = config.contentType;
|
config.getHeaders().forEach(request::header);
|
||||||
if (contentType != null) {
|
|
||||||
request.content(new StringContentProvider(command), contentType);
|
|
||||||
} else {
|
|
||||||
request.content(new StringContentProvider(command));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
config.headers.forEach(header -> {
|
CompletableFuture<@Nullable ChannelHandlerContent> responseContentFuture = new CompletableFuture<>();
|
||||||
String[] keyValuePair = header.split("=", 2);
|
responseContentFuture.exceptionally(t -> {
|
||||||
if (keyValuePair.length == 2) {
|
if (t instanceof HttpAuthException) {
|
||||||
request.header(keyValuePair[0], keyValuePair[1]);
|
if (isRetry || !rateLimitedHttpClient.reAuth(uri)) {
|
||||||
} else {
|
logger.warn(
|
||||||
logger.warn("Splitting header '{}' failed. No '=' was found. Ignoring", header);
|
"Retry after authentication failure failed again for '{}', failing here",
|
||||||
}
|
uri);
|
||||||
});
|
onHttpError("Authentication failed");
|
||||||
|
} else {
|
||||||
|
sendHttpValue(commandUrl, command, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
if (logger.isTraceEnabled()) {
|
if (logger.isTraceEnabled()) {
|
||||||
logger.trace("Sending to '{}': {}", uri, Util.requestToLogString(request));
|
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);
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
request.send(new HttpResponseListener(responseContentFuture, null, config.bufferSize, this));
|
||||||
return null;
|
});
|
||||||
});
|
|
||||||
request.send(new HttpResponseListener(f, null, config.bufferSize));
|
|
||||||
} catch (IllegalArgumentException | URISyntaxException | MalformedURLException e) {
|
} catch (IllegalArgumentException | URISyntaxException | MalformedURLException e) {
|
||||||
logger.warn("Creating request for '{}' failed: {}", commandUrl, e.getMessage());
|
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) {
|
ChannelUID channelUID, HttpChannelConfig channelConfig) {
|
||||||
return factory.create(state -> updateState(channelUID, state), command -> postCommand(channelUID, command),
|
return factory.create(state -> updateState(channelUID, state), command -> postCommand(channelUID, command),
|
||||||
command -> sendHttpValue(commandUrl, channelConfig.escapedUrl, command),
|
command -> sendHttpValue(commandUrl, command),
|
||||||
valueTransformationProvider.getValueTransformation(channelConfig.stateTransformation),
|
new ChannelTransformation(channelConfig.stateTransformation),
|
||||||
valueTransformationProvider.getValueTransformation(channelConfig.commandTransformation), channelConfig);
|
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) {
|
HttpChannelConfig channelConfig, Function<String, State> toState) {
|
||||||
AbstractTransformingItemConverter.Factory factory = (state, command, value, stateTrans, commandTrans,
|
AbstractTransformingChannelHandler.Factory factory = (state, command, value, stateTrans, commandTrans,
|
||||||
config) -> new GenericItemConverter(toState, state, command, value, stateTrans, commandTrans, config);
|
config) -> new GenericChannelHandler(toState, state, command, value, stateTrans, commandTrans, config);
|
||||||
return createItemConverter(factory, commandUrl, channelUID, channelConfig);
|
return createChannelHandler(factory, commandUrl, channelUID, channelConfig);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -54,13 +54,14 @@ public class Util {
|
|||||||
* create an URI from a string, escaping all necessary characters
|
* create an URI from a string, escaping all necessary characters
|
||||||
*
|
*
|
||||||
* @param s the URI as unescaped string
|
* @param s the URI as unescaped string
|
||||||
* @return URI correspondign to the input string
|
* @return URI corresponding to the input string
|
||||||
* @throws MalformedURLException
|
* @throws MalformedURLException if parameter is not an URL
|
||||||
* @throws URISyntaxException
|
* @throws URISyntaxException if parameter could not be converted to an URI
|
||||||
*/
|
*/
|
||||||
public static URI uriFromString(String s) throws MalformedURLException, URISyntaxException {
|
public static URI uriFromString(String s) throws MalformedURLException, URISyntaxException {
|
||||||
URL url = new URL(s);
|
URL url = new URL(s);
|
||||||
return new URI(url.getProtocol(), url.getUserInfo(), IDN.toASCII(url.getHost()), url.getPort(), url.getPath(),
|
URI uri = new URI(url.getProtocol(), url.getUserInfo(), IDN.toASCII(url.getHost()), url.getPort(),
|
||||||
url.getQuery(), url.getRef());
|
url.getPath(), url.getQuery(), url.getRef());
|
||||||
|
return URI.create(uri.toASCIIString());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -23,5 +23,6 @@ import org.eclipse.jdt.annotation.NonNullByDefault;
|
|||||||
public enum HttpAuthMode {
|
public enum HttpAuthMode {
|
||||||
BASIC_PREEMPTIVE,
|
BASIC_PREEMPTIVE,
|
||||||
BASIC,
|
BASIC,
|
||||||
DIGEST
|
DIGEST,
|
||||||
|
TOKEN
|
||||||
}
|
}
|
||||||
|
@ -12,23 +12,9 @@
|
|||||||
*/
|
*/
|
||||||
package org.openhab.binding.http.internal.config;
|
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.NonNullByDefault;
|
||||||
import org.eclipse.jdt.annotation.Nullable;
|
import org.eclipse.jdt.annotation.Nullable;
|
||||||
import org.openhab.binding.http.internal.converter.ColorItemConverter;
|
import org.openhab.core.thing.binding.generic.ChannelValueConverterConfig;
|
||||||
import org.openhab.core.library.types.IncreaseDecreaseType;
|
|
||||||
import org.openhab.core.library.types.NextPreviousType;
|
|
||||||
import org.openhab.core.library.types.OnOffType;
|
|
||||||
import org.openhab.core.library.types.OpenClosedType;
|
|
||||||
import org.openhab.core.library.types.PlayPauseType;
|
|
||||||
import org.openhab.core.library.types.RewindFastforwardType;
|
|
||||||
import org.openhab.core.library.types.StopMoveType;
|
|
||||||
import org.openhab.core.library.types.UpDownType;
|
|
||||||
import org.openhab.core.types.Command;
|
|
||||||
import org.openhab.core.types.State;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The {@link HttpChannelConfig} class contains fields mapping channel configuration parameters.
|
* 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
|
* @author Jan N. Klug - Initial contribution
|
||||||
*/
|
*/
|
||||||
@NonNullByDefault
|
@NonNullByDefault
|
||||||
public class HttpChannelConfig {
|
public class HttpChannelConfig extends ChannelValueConverterConfig {
|
||||||
private final Map<String, State> stringStateMap = new HashMap<>();
|
|
||||||
private final Map<Command, @Nullable String> commandStringMap = new HashMap<>();
|
|
||||||
private boolean initialized = false;
|
|
||||||
|
|
||||||
public @Nullable String stateExtension;
|
public @Nullable String stateExtension;
|
||||||
public @Nullable String commandExtension;
|
public @Nullable String commandExtension;
|
||||||
public @Nullable String stateTransformation;
|
public @Nullable String stateTransformation;
|
||||||
public @Nullable String commandTransformation;
|
public @Nullable String commandTransformation;
|
||||||
public String stateContent = "";
|
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;
|
package org.openhab.binding.http.internal.config;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||||
import org.eclipse.jdt.annotation.Nullable;
|
import org.eclipse.jdt.annotation.Nullable;
|
||||||
|
import org.eclipse.jetty.http.HttpHeader;
|
||||||
import org.eclipse.jetty.http.HttpMethod;
|
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.
|
* The {@link HttpThingConfig} class contains fields mapping thing configuration parameters.
|
||||||
@ -25,6 +31,8 @@ import org.eclipse.jetty.http.HttpMethod;
|
|||||||
*/
|
*/
|
||||||
@NonNullByDefault
|
@NonNullByDefault
|
||||||
public class HttpThingConfig {
|
public class HttpThingConfig {
|
||||||
|
private final Logger logger = LoggerFactory.getLogger(HttpThingConfig.class);
|
||||||
|
|
||||||
public String baseURL = "";
|
public String baseURL = "";
|
||||||
public int refresh = 30;
|
public int refresh = 30;
|
||||||
public int timeout = 3000;
|
public int timeout = 3000;
|
||||||
@ -43,7 +51,26 @@ public class HttpThingConfig {
|
|||||||
public @Nullable String contentType = null;
|
public @Nullable String contentType = null;
|
||||||
|
|
||||||
public boolean ignoreSSLErrors = false;
|
public boolean ignoreSSLErrors = false;
|
||||||
|
public boolean strictErrorHandling = false;
|
||||||
|
|
||||||
// ArrayList is required as implementation because list may be modified later
|
// ArrayList is required as implementation because list may be modified later
|
||||||
public ArrayList<String> headers = new ArrayList<>();
|
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;
|
package org.openhab.binding.http.internal.http;
|
||||||
|
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.util.Objects;
|
|
||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.CompletableFuture;
|
||||||
import java.util.stream.Collectors;
|
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.client.util.BufferingResponseListener;
|
||||||
import org.eclipse.jetty.http.HttpField;
|
import org.eclipse.jetty.http.HttpField;
|
||||||
import org.eclipse.jetty.http.HttpStatus;
|
import org.eclipse.jetty.http.HttpStatus;
|
||||||
|
import org.openhab.core.thing.binding.generic.ChannelHandlerContent;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
@ -36,7 +36,8 @@ import org.slf4j.LoggerFactory;
|
|||||||
@NonNullByDefault
|
@NonNullByDefault
|
||||||
public class HttpResponseListener extends BufferingResponseListener {
|
public class HttpResponseListener extends BufferingResponseListener {
|
||||||
private final Logger logger = LoggerFactory.getLogger(HttpResponseListener.class);
|
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;
|
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 fallbackEncoding a fallback encoding for the content (UTF-8 if null)
|
||||||
* @param bufferSize the buffer size for the content in kB (default 2048 kB)
|
* @param bufferSize the buffer size for the content in kB (default 2048 kB)
|
||||||
*/
|
*/
|
||||||
public HttpResponseListener(CompletableFuture<@Nullable Content> future, @Nullable String fallbackEncoding,
|
public HttpResponseListener(CompletableFuture<@Nullable ChannelHandlerContent> future,
|
||||||
int bufferSize) {
|
@Nullable String fallbackEncoding, int bufferSize, HttpStatusListener httpStatusListener) {
|
||||||
super(bufferSize * 1024);
|
super(bufferSize * 1024);
|
||||||
this.future = future;
|
this.future = future;
|
||||||
this.fallbackEncoding = fallbackEncoding != null ? fallbackEncoding : StandardCharsets.UTF_8.name();
|
this.fallbackEncoding = fallbackEncoding != null ? fallbackEncoding : StandardCharsets.UTF_8.name();
|
||||||
|
this.httpStatusListener = httpStatusListener;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -60,31 +62,49 @@ public class HttpResponseListener extends BufferingResponseListener {
|
|||||||
logger.trace("Received from '{}': {}", result.getRequest().getURI(), responseToLogString(response));
|
logger.trace("Received from '{}': {}", result.getRequest().getURI(), responseToLogString(response));
|
||||||
}
|
}
|
||||||
Request request = result.getRequest();
|
Request request = result.getRequest();
|
||||||
if (result.isFailed()) {
|
if (response == null || (result.isFailed() && response.getStatus() != HttpStatus.UNAUTHORIZED_401)) {
|
||||||
logger.warn("Requesting '{}' (method='{}', content='{}') failed: {}", request.getURI(), request.getMethod(),
|
logger.debug("Requesting '{}' (method='{}', content='{}') failed: {}", request.getURI(),
|
||||||
request.getContent(), result.getFailure().toString());
|
request.getMethod(), request.getContent(), result.getFailure().getMessage());
|
||||||
future.complete(null);
|
future.complete(null);
|
||||||
} else if (HttpStatus.isSuccess(response.getStatus())) {
|
httpStatusListener.onHttpError(result.getFailure().getMessage());
|
||||||
String encoding = Objects.requireNonNullElse(getEncoding(), fallbackEncoding);
|
|
||||||
future.complete(new Content(getContent(), encoding, getMediaType()));
|
|
||||||
} else {
|
} else {
|
||||||
switch (response.getStatus()) {
|
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:
|
case HttpStatus.UNAUTHORIZED_401:
|
||||||
logger.debug("Requesting '{}' (method='{}', content='{}') failed: Authorization error",
|
logger.debug("Requesting '{}' (method='{}', content='{}') failed: Authorization error",
|
||||||
request.getURI(), request.getMethod(), request.getContent());
|
request.getURI(), request.getMethod(), request.getContent());
|
||||||
future.completeExceptionally(new HttpAuthException());
|
future.completeExceptionally(new HttpAuthException());
|
||||||
break;
|
break;
|
||||||
default:
|
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());
|
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) {
|
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(", "))
|
+ response.getHeaders().stream().map(HttpField::toString).collect(Collectors.joining(", "))
|
||||||
+ "}, Content = {" + getContentAsString() + "}";
|
+ "}, Content = {" + getContentAsString() + "}";
|
||||||
|
return logString;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -10,24 +10,27 @@
|
|||||||
*
|
*
|
||||||
* SPDX-License-Identifier: EPL-2.0
|
* 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.NonNullByDefault;
|
||||||
import org.eclipse.jdt.annotation.Nullable;
|
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
|
* @author Jan N. Klug - Initial contribution
|
||||||
*/
|
*/
|
||||||
@NonNullByDefault
|
@NonNullByDefault
|
||||||
public interface ValueTransformationProvider {
|
public interface HttpStatusListener {
|
||||||
|
/**
|
||||||
|
* report an error
|
||||||
|
*
|
||||||
|
* @param message optional error message
|
||||||
|
*/
|
||||||
|
void onHttpError(@Nullable String message);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
* report a successful request
|
||||||
* @param pattern A transformation pattern, starting with the transformation service
|
|
||||||
* * name, followed by a colon and the transformation itself.
|
|
||||||
* @return
|
|
||||||
*/
|
*/
|
||||||
ValueTransformation getValueTransformation(@Nullable String pattern);
|
void onHttpSuccess();
|
||||||
}
|
}
|
@ -24,10 +24,13 @@ import java.util.concurrent.TimeUnit;
|
|||||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||||
import org.eclipse.jdt.annotation.Nullable;
|
import org.eclipse.jdt.annotation.Nullable;
|
||||||
import org.eclipse.jetty.client.HttpClient;
|
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.AuthenticationStore;
|
||||||
import org.eclipse.jetty.client.api.Request;
|
import org.eclipse.jetty.client.api.Request;
|
||||||
import org.eclipse.jetty.client.util.StringContentProvider;
|
import org.eclipse.jetty.client.util.StringContentProvider;
|
||||||
import org.eclipse.jetty.http.HttpMethod;
|
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
|
* 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
|
@NonNullByDefault
|
||||||
public class RateLimitedHttpClient {
|
public class RateLimitedHttpClient {
|
||||||
private static final int MAX_QUEUE_SIZE = 1000; // maximum queue size
|
private static final int MAX_QUEUE_SIZE = 1000; // maximum queue size
|
||||||
|
private final Logger logger = LoggerFactory.getLogger(RateLimitedHttpClient.class);
|
||||||
|
|
||||||
private HttpClient httpClient;
|
private HttpClient httpClient;
|
||||||
private int delay = 0; // in ms
|
private int delay = 0; // in ms
|
||||||
private final ScheduledExecutorService scheduler;
|
private final ScheduledExecutorService scheduler;
|
||||||
private final LinkedBlockingQueue<RequestQueueEntry> requestQueue = new LinkedBlockingQueue<>(MAX_QUEUE_SIZE);
|
private final LinkedBlockingQueue<RequestQueueEntry> requestQueue = new LinkedBlockingQueue<>(MAX_QUEUE_SIZE);
|
||||||
|
private final LinkedBlockingQueue<RequestQueueEntry> priorityRequestQueue = new LinkedBlockingQueue<>(
|
||||||
|
MAX_QUEUE_SIZE);
|
||||||
|
|
||||||
private @Nullable ScheduledFuture<?> processJob;
|
private @Nullable ScheduledFuture<?> processJob;
|
||||||
|
|
||||||
@ -55,7 +62,7 @@ public class RateLimitedHttpClient {
|
|||||||
*/
|
*/
|
||||||
public void shutdown() {
|
public void shutdown() {
|
||||||
stopProcessJob();
|
stopProcessJob();
|
||||||
requestQueue.forEach(queueEntry -> queueEntry.future.completeExceptionally(new CancellationException()));
|
requestQueue.forEach(RequestQueueEntry::cancel);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -77,7 +84,7 @@ public class RateLimitedHttpClient {
|
|||||||
/**
|
/**
|
||||||
* Set the HTTP client
|
* Set the HTTP client
|
||||||
*
|
*
|
||||||
* @param httpClient secure or insecure Jetty http client
|
* @param httpClient secure or insecure {@link HttpClient}
|
||||||
*/
|
*/
|
||||||
public void setHttpClient(HttpClient httpClient) {
|
public void setHttpClient(HttpClient httpClient) {
|
||||||
this.httpClient = httpClient;
|
this.httpClient = httpClient;
|
||||||
@ -89,31 +96,70 @@ public class RateLimitedHttpClient {
|
|||||||
* @param finalUrl the request URL
|
* @param finalUrl the request URL
|
||||||
* @param method http request method GET/PUT/POST
|
* @param method http request method GET/PUT/POST
|
||||||
* @param content the content (if method 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
|
// if no delay is set, return a completed CompletableFuture
|
||||||
CompletableFuture<Request> future = new 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) {
|
if (delay == 0) {
|
||||||
queueEntry.completeFuture(httpClient);
|
queueEntry.completeFuture(httpClient);
|
||||||
} else {
|
} else {
|
||||||
if (!requestQueue.offer(queueEntry)) {
|
if (!queue.offer(queueEntry)) {
|
||||||
future.completeExceptionally(new RejectedExecutionException("Maximum queue size exceeded."));
|
future.completeExceptionally(new RejectedExecutionException("Maximum queue size exceeded."));
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
return future;
|
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() {
|
public AuthenticationStore getAuthenticationStore() {
|
||||||
return httpClient.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() {
|
private void stopProcessJob() {
|
||||||
ScheduledFuture<?> processJob = this.processJob;
|
ScheduledFuture<?> processJob = this.processJob;
|
||||||
if (processJob != null) {
|
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() {
|
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) {
|
if (queueEntry != null) {
|
||||||
queueEntry.completeFuture(httpClient);
|
queueEntry.completeFuture(httpClient);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static class RequestQueueEntry {
|
private static class RequestQueueEntry {
|
||||||
private URI finalUrl;
|
private final URI finalUrl;
|
||||||
private HttpMethod method;
|
private final HttpMethod method;
|
||||||
private String content;
|
private final String content;
|
||||||
private CompletableFuture<Request> future;
|
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.finalUrl = finalUrl;
|
||||||
this.method = method;
|
this.method = method;
|
||||||
this.content = content;
|
this.content = content;
|
||||||
|
this.contentType = contentType;
|
||||||
this.future = future;
|
this.future = future;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -149,10 +205,21 @@ public class RateLimitedHttpClient {
|
|||||||
*/
|
*/
|
||||||
public void completeFuture(HttpClient httpClient) {
|
public void completeFuture(HttpClient httpClient) {
|
||||||
Request request = httpClient.newRequest(finalUrl).method(method);
|
Request request = httpClient.newRequest(finalUrl).method(method);
|
||||||
if (method != HttpMethod.GET && !content.isEmpty()) {
|
if ((method == HttpMethod.POST || method == HttpMethod.PUT) && !content.isEmpty()) {
|
||||||
request.content(new StringContentProvider(content));
|
if (contentType == null) {
|
||||||
|
request.content(new StringContentProvider(content));
|
||||||
|
} else {
|
||||||
|
request.content(new StringContentProvider(content), contentType);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
future.complete(request);
|
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.URI;
|
||||||
import java.net.URISyntaxException;
|
import java.net.URISyntaxException;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.List;
|
import java.util.Map;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.concurrent.CancellationException;
|
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.NonNullByDefault;
|
||||||
import org.eclipse.jdt.annotation.Nullable;
|
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.eclipse.jetty.http.HttpMethod;
|
||||||
import org.openhab.binding.http.internal.Util;
|
import org.openhab.binding.http.internal.Util;
|
||||||
import org.openhab.binding.http.internal.config.HttpThingConfig;
|
import org.openhab.binding.http.internal.config.HttpThingConfig;
|
||||||
|
import org.openhab.core.thing.binding.generic.ChannelHandlerContent;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
@ -48,29 +47,34 @@ public class RefreshingUrlCache {
|
|||||||
private final Logger logger = LoggerFactory.getLogger(RefreshingUrlCache.class);
|
private final Logger logger = LoggerFactory.getLogger(RefreshingUrlCache.class);
|
||||||
|
|
||||||
private final String url;
|
private final String url;
|
||||||
private final boolean escapedUrl;
|
|
||||||
private final RateLimitedHttpClient httpClient;
|
private final RateLimitedHttpClient httpClient;
|
||||||
|
private final boolean strictErrorHandling;
|
||||||
private final int timeout;
|
private final int timeout;
|
||||||
private final int bufferSize;
|
private final int bufferSize;
|
||||||
private final @Nullable String fallbackEncoding;
|
private final @Nullable String fallbackEncoding;
|
||||||
private final Set<Consumer<Content>> consumers = ConcurrentHashMap.newKeySet();
|
private final Set<Consumer<@Nullable ChannelHandlerContent>> consumers = ConcurrentHashMap.newKeySet();
|
||||||
private final List<String> headers;
|
private final Map<String, String> headers;
|
||||||
private final HttpMethod httpMethod;
|
private final HttpMethod httpMethod;
|
||||||
private final String httpContent;
|
private final String httpContent;
|
||||||
|
private final @Nullable String httpContentType;
|
||||||
|
private final HttpStatusListener httpStatusListener;
|
||||||
|
|
||||||
private final ScheduledFuture<?> future;
|
private final ScheduledFuture<?> future;
|
||||||
private @Nullable Content lastContent;
|
private @Nullable ChannelHandlerContent lastContent;
|
||||||
|
|
||||||
public RefreshingUrlCache(ScheduledExecutorService executor, RateLimitedHttpClient httpClient, String url,
|
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.httpClient = httpClient;
|
||||||
this.url = url;
|
this.url = url;
|
||||||
this.escapedUrl = escapedUrl;
|
this.strictErrorHandling = thingConfig.strictErrorHandling;
|
||||||
this.timeout = thingConfig.timeout;
|
this.timeout = thingConfig.timeout;
|
||||||
this.bufferSize = thingConfig.bufferSize;
|
this.bufferSize = thingConfig.bufferSize;
|
||||||
this.headers = thingConfig.headers;
|
|
||||||
this.httpMethod = thingConfig.stateMethod;
|
this.httpMethod = thingConfig.stateMethod;
|
||||||
|
this.headers = thingConfig.getHeaders();
|
||||||
this.httpContent = httpContent;
|
this.httpContent = httpContent;
|
||||||
|
this.httpContentType = httpContentType;
|
||||||
|
this.httpStatusListener = httpStatusListener;
|
||||||
fallbackEncoding = thingConfig.encoding;
|
fallbackEncoding = thingConfig.encoding;
|
||||||
|
|
||||||
future = executor.scheduleWithFixedDelay(this::refresh, 1, thingConfig.refresh, TimeUnit.SECONDS);
|
future = executor.scheduleWithFixedDelay(this::refresh, 1, thingConfig.refresh, TimeUnit.SECONDS);
|
||||||
@ -89,37 +93,21 @@ public class RefreshingUrlCache {
|
|||||||
|
|
||||||
// format URL
|
// format URL
|
||||||
try {
|
try {
|
||||||
String url = String.format(this.url, new Date());
|
URI uri = Util.uriFromString(String.format(this.url, new Date()));
|
||||||
URI uri = escapedUrl ? new URI(url) : Util.uriFromString(url);
|
|
||||||
logger.trace("Requesting refresh (retry={}) from '{}' with timeout {}ms", isRetry, uri, timeout);
|
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);
|
request.timeout(timeout, TimeUnit.MILLISECONDS);
|
||||||
|
headers.forEach(request::header);
|
||||||
|
|
||||||
headers.forEach(header -> {
|
CompletableFuture<@Nullable ChannelHandlerContent> responseContentFuture = new CompletableFuture<>();
|
||||||
String[] keyValuePair = header.split("=", 2);
|
responseContentFuture.exceptionally(t -> {
|
||||||
if (keyValuePair.length == 2) {
|
if (t instanceof HttpAuthException) {
|
||||||
request.header(keyValuePair[0].trim(), keyValuePair[1].trim());
|
if (isRetry || !httpClient.reAuth(uri)) {
|
||||||
} else {
|
logger.debug("Authentication failed for '{}', retry={}", uri, isRetry);
|
||||||
logger.warn("Splitting header '{}' failed. No '=' was found. Ignoring", header);
|
httpStatusListener.onHttpError("Authentication failed");
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
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);
|
|
||||||
} else {
|
} else {
|
||||||
AuthenticationStore authStore = httpClient.getAuthenticationStore();
|
refresh(true);
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
@ -129,7 +117,8 @@ public class RefreshingUrlCache {
|
|||||||
logger.trace("Sending to '{}': {}", uri, Util.requestToLogString(request));
|
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 -> {
|
}).exceptionally(e -> {
|
||||||
if (e instanceof CancellationException) {
|
if (e instanceof CancellationException) {
|
||||||
logger.debug("Request to URL {} was cancelled by thing handler.", uri);
|
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);
|
logger.trace("Stopped refresh task for URL '{}'", url);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void addConsumer(Consumer<Content> consumer) {
|
public void addConsumer(Consumer<@Nullable ChannelHandlerContent> consumer) {
|
||||||
consumers.add(consumer);
|
consumers.add(consumer);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Optional<Content> get() {
|
public Optional<ChannelHandlerContent> get() {
|
||||||
final Content content = lastContent;
|
return Optional.ofNullable(lastContent);
|
||||||
if (content == null) {
|
|
||||||
return Optional.empty();
|
|
||||||
} else {
|
|
||||||
return Optional.of(content);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void processResult(@Nullable Content content) {
|
private void processResult(@Nullable ChannelHandlerContent content) {
|
||||||
if (content != null) {
|
if (content != null || strictErrorHandling) {
|
||||||
for (Consumer<Content> consumer : consumers) {
|
for (Consumer<@Nullable ChannelHandlerContent> consumer : consumers) {
|
||||||
try {
|
try {
|
||||||
consumer.accept(content);
|
consumer.accept(content);
|
||||||
} catch (IllegalArgumentException | IllegalStateException e) {
|
} 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>
|
<type>binding</type>
|
||||||
<name>HTTP Binding</name>
|
<name>HTTP Binding</name>
|
||||||
<description>This is the binding for retrieving and processing HTTP resources.</description>
|
<description>This is the binding for retrieving and processing HTTP resources.</description>
|
||||||
<connection>local</connection>
|
<connection>hybrid</connection>
|
||||||
|
|
||||||
</addon:addon>
|
</addon:addon>
|
||||||
|
@ -7,13 +7,11 @@
|
|||||||
<config-description uri="channel-type:http:channel-config">
|
<config-description uri="channel-type:http:channel-config">
|
||||||
<parameter name="stateTransformation" type="text">
|
<parameter name="stateTransformation" type="text">
|
||||||
<label>State Transformation</label>
|
<label>State Transformation</label>
|
||||||
<description>Transformation pattern used when receiving values. Chain multiple transformations with the mathematical
|
<description>Transformation pattern used when receiving values. Multiple transformation can be chained using "∩".</description>
|
||||||
intersection character "∩".</description>
|
|
||||||
</parameter>
|
</parameter>
|
||||||
<parameter name="commandTransformation" type="text">
|
<parameter name="commandTransformation" type="text">
|
||||||
<label>Command Transformation</label>
|
<label>Command Transformation</label>
|
||||||
<description>Transformation pattern used when sending values. Chain multiple transformations with the mathematical
|
<description>Transformation pattern used when sending values. Multiple transformation can be chained using "∩".</description>
|
||||||
intersection character "∩".</description>
|
|
||||||
</parameter>
|
</parameter>
|
||||||
<parameter name="stateExtension" type="text">
|
<parameter name="stateExtension" type="text">
|
||||||
<label>State URL Extension</label>
|
<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>
|
<description>This value is added to the base URL configured in the thing for sending values.</description>
|
||||||
<advanced>true</advanced>
|
<advanced>true</advanced>
|
||||||
</parameter>
|
</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">
|
<parameter name="stateContent" type="text">
|
||||||
<label>State Content</label>
|
<label>State Content</label>
|
||||||
<description>Content for state request (only used if method is POST/PUT)</description>
|
<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">
|
<config-description uri="channel-type:http:channel-config-color">
|
||||||
<parameter name="stateTransformation" type="text">
|
<parameter name="stateTransformation" type="text">
|
||||||
<label>State Transformation</label>
|
<label>State Transformation</label>
|
||||||
<description>Transformation pattern used when receiving values. Chain multiple transformations with the mathematical
|
<description>Transformation pattern used when receiving values.</description>
|
||||||
intersection character "∩".</description>
|
|
||||||
</parameter>
|
</parameter>
|
||||||
<parameter name="commandTransformation" type="text">
|
<parameter name="commandTransformation" type="text">
|
||||||
<label>Command Transformation</label>
|
<label>Command Transformation</label>
|
||||||
<description>Transformation pattern used when sending values. Chain multiple transformations with the mathematical
|
<description>Transformation pattern used when sending value. Multiple transformation can be chained using "∩".</description>
|
||||||
intersection character "∩".</description>
|
|
||||||
</parameter>
|
</parameter>
|
||||||
<parameter name="stateExtension" type="text">
|
<parameter name="stateExtension" type="text">
|
||||||
<label>State URL Extension</label>
|
<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>
|
<description>This value is added to the base URL configured in the thing for sending values.</description>
|
||||||
<advanced>true</advanced>
|
<advanced>true</advanced>
|
||||||
</parameter>
|
</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">
|
<parameter name="stateContent" type="text">
|
||||||
<label>State Content</label>
|
<label>State Content</label>
|
||||||
<description>Content for state request (only used if method is POST/PUT)</description>
|
<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">
|
<config-description uri="channel-type:http:channel-config-contact">
|
||||||
<parameter name="stateTransformation" type="text">
|
<parameter name="stateTransformation" type="text">
|
||||||
<label>State Transformation</label>
|
<label>State Transformation</label>
|
||||||
<description>Transformation pattern used when receiving values. Chain multiple transformations with the mathematical
|
<description>Transformation pattern used when receiving value. Multiple transformation can be chained using "∩".</description>
|
||||||
intersection character "∩".</description>
|
|
||||||
</parameter>
|
</parameter>
|
||||||
<parameter name="commandTransformation" type="text">
|
<parameter name="commandTransformation" type="text">
|
||||||
<label>Command Transformation</label>
|
<label>Command Transformation</label>
|
||||||
<description>Transformation pattern used when sending values. Chain multiple transformations with the mathematical
|
<description>Transformation pattern used when sending value. Multiple transformation can be chained using "∩".</description>
|
||||||
intersection character "∩".</description>
|
|
||||||
</parameter>
|
</parameter>
|
||||||
<parameter name="stateExtension" type="text">
|
<parameter name="stateExtension" type="text">
|
||||||
<label>State URL Extension</label>
|
<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>
|
<description>This value is added to the base URL configured in the thing for sending values.</description>
|
||||||
<advanced>true</advanced>
|
<advanced>true</advanced>
|
||||||
</parameter>
|
</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">
|
<parameter name="stateContent" type="text">
|
||||||
<label>State Content</label>
|
<label>State Content</label>
|
||||||
<description>Content for state request (only used if method is POST/PUT)</description>
|
<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">
|
<config-description uri="channel-type:http:channel-config-dimmer">
|
||||||
<parameter name="stateTransformation" type="text">
|
<parameter name="stateTransformation" type="text">
|
||||||
<label>State Transformation</label>
|
<label>State Transformation</label>
|
||||||
<description>Transformation pattern used when receiving values. Chain multiple transformations with the mathematical
|
<description>Transformation pattern used when receiving value. Multiple transformation can be chained using "∩".</description>
|
||||||
intersection character "∩".</description>
|
|
||||||
</parameter>
|
</parameter>
|
||||||
<parameter name="commandTransformation" type="text">
|
<parameter name="commandTransformation" type="text">
|
||||||
<label>Command Transformation</label>
|
<label>Command Transformation</label>
|
||||||
<description>Transformation pattern used when sending values. Chain multiple transformations with the mathematical
|
<description>Transformation pattern used when sending value. Multiple transformation can be chained using "∩".</description>
|
||||||
intersection character "∩".</description>
|
|
||||||
</parameter>
|
</parameter>
|
||||||
<parameter name="stateExtension" type="text">
|
<parameter name="stateExtension" type="text">
|
||||||
<label>State URL Extension</label>
|
<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>
|
<description>This value is added to the base URL configured in the thing for sending values.</description>
|
||||||
<advanced>true</advanced>
|
<advanced>true</advanced>
|
||||||
</parameter>
|
</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">
|
<parameter name="stateContent" type="text">
|
||||||
<label>State Content</label>
|
<label>State Content</label>
|
||||||
<description>Content for state request (only used if method is POST/PUT)</description>
|
<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>
|
<description>This value is added to the base URL configured in the thing for retrieving values.</description>
|
||||||
<advanced>true</advanced>
|
<advanced>true</advanced>
|
||||||
</parameter>
|
</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">
|
<parameter name="stateContent" type="text">
|
||||||
<label>State Content</label>
|
<label>State Content</label>
|
||||||
<description>Content for state request (only used if method is POST/PUT)</description>
|
<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">
|
<config-description uri="channel-type:http:channel-config-number">
|
||||||
<parameter name="stateTransformation" type="text">
|
<parameter name="stateTransformation" type="text">
|
||||||
<label>State Transformation</label>
|
<label>State Transformation</label>
|
||||||
<description>Transformation pattern used when receiving values. Chain multiple transformations with the mathematical
|
<description>Transformation pattern used when receiving value. Multiple transformation can be chained using "∩".</description>
|
||||||
intersection character "∩".</description>
|
|
||||||
</parameter>
|
</parameter>
|
||||||
<parameter name="commandTransformation" type="text">
|
<parameter name="commandTransformation" type="text">
|
||||||
<label>Command Transformation</label>
|
<label>Command Transformation</label>
|
||||||
<description>Transformation pattern used when sending values. Chain multiple transformations with the mathematical
|
<description>Transformation pattern used when sending value. Multiple transformation can be chained using "∩".</description>
|
||||||
intersection character "∩".</description>
|
|
||||||
</parameter>
|
</parameter>
|
||||||
<parameter name="stateExtension" type="text">
|
<parameter name="stateExtension" type="text">
|
||||||
<label>State URL Extension</label>
|
<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>
|
<description>This value is added to the base URL configured in the thing for sending values.</description>
|
||||||
<advanced>true</advanced>
|
<advanced>true</advanced>
|
||||||
</parameter>
|
</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">
|
<parameter name="stateContent" type="text">
|
||||||
<label>State Content</label>
|
<label>State Content</label>
|
||||||
<description>Content for state request (only used if method is POST/PUT)</description>
|
<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">
|
<config-description uri="channel-type:http:channel-config-player">
|
||||||
<parameter name="stateTransformation" type="text">
|
<parameter name="stateTransformation" type="text">
|
||||||
<label>State Transformation</label>
|
<label>State Transformation</label>
|
||||||
<description>Transformation pattern used when receiving values. Chain multiple transformations with the mathematical
|
<description>Transformation pattern used when receiving value. Multiple transformation can be chained using "∩".</description>
|
||||||
intersection character "∩".</description>
|
|
||||||
</parameter>
|
</parameter>
|
||||||
<parameter name="commandTransformation" type="text">
|
<parameter name="commandTransformation" type="text">
|
||||||
<label>Command Transformation</label>
|
<label>Command Transformation</label>
|
||||||
<description>Transformation pattern used when sending values. Chain multiple transformations with the mathematical
|
<description>Transformation pattern used when sending value. Multiple transformation can be chained using "∩".</description>
|
||||||
intersection character "∩".</description>
|
|
||||||
</parameter>
|
</parameter>
|
||||||
<parameter name="stateExtension" type="text">
|
<parameter name="stateExtension" type="text">
|
||||||
<label>State URL Extension</label>
|
<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>
|
<description>This value is added to the base URL configured in the thing for sending values.</description>
|
||||||
<advanced>true</advanced>
|
<advanced>true</advanced>
|
||||||
</parameter>
|
</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">
|
<parameter name="stateContent" type="text">
|
||||||
<label>State Content</label>
|
<label>State Content</label>
|
||||||
<description>Content for state request (only used if method is POST/PUT)</description>
|
<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">
|
<config-description uri="channel-type:http:channel-config-rollershutter">
|
||||||
<parameter name="stateTransformation" type="text">
|
<parameter name="stateTransformation" type="text">
|
||||||
<label>State Transformation</label>
|
<label>State Transformation</label>
|
||||||
<description>Transformation pattern used when receiving values. Chain multiple transformations with the mathematical
|
<description>Transformation pattern used when receiving value. Multiple transformation can be chained using "∩".</description>
|
||||||
intersection character "∩".</description>
|
|
||||||
</parameter>
|
</parameter>
|
||||||
<parameter name="commandTransformation" type="text">
|
<parameter name="commandTransformation" type="text">
|
||||||
<label>Command Transformation</label>
|
<label>Command Transformation</label>
|
||||||
<description>Transformation pattern used when sending values Chain multiple transformations with the mathematical
|
<description>Transformation pattern used when sending value. Multiple transformation can be chained using "∩".</description>
|
||||||
intersection character "∩"..</description>
|
|
||||||
</parameter>
|
</parameter>
|
||||||
<parameter name="stateExtension" type="text">
|
<parameter name="stateExtension" type="text">
|
||||||
<label>State URL Extension</label>
|
<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>
|
<description>This value is added to the base URL configured in the thing for sending values.</description>
|
||||||
<advanced>true</advanced>
|
<advanced>true</advanced>
|
||||||
</parameter>
|
</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">
|
<parameter name="stateContent" type="text">
|
||||||
<label>State Content</label>
|
<label>State Content</label>
|
||||||
<description>Content for state request (only used if method is POST/PUT)</description>
|
<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">
|
<config-description uri="channel-type:http:channel-config-switch">
|
||||||
<parameter name="stateTransformation" type="text">
|
<parameter name="stateTransformation" type="text">
|
||||||
<label>State Transformation</label>
|
<label>State Transformation</label>
|
||||||
<description>Transformation pattern used when receiving values. Chain multiple transformations with the mathematical
|
<description>Transformation pattern used when receiving value. Multiple transformation can be chained using "∩".</description>
|
||||||
intersection character "∩".</description>
|
|
||||||
</parameter>
|
</parameter>
|
||||||
<parameter name="commandTransformation" type="text">
|
<parameter name="commandTransformation" type="text">
|
||||||
<label>Command Transformation</label>
|
<label>Command Transformation</label>
|
||||||
<description>Transformation pattern used when sending values. Chain multiple transformations with the mathematical
|
<description>Transformation pattern used when sending value. Multiple transformation can be chained using "∩".</description>
|
||||||
intersection character "∩".</description>
|
|
||||||
</parameter>
|
</parameter>
|
||||||
<parameter name="stateExtension" type="text">
|
<parameter name="stateExtension" type="text">
|
||||||
<label>State URL Extension</label>
|
<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>
|
<description>This value is added to the base URL configured in the thing for sending values.</description>
|
||||||
<advanced>true</advanced>
|
<advanced>true</advanced>
|
||||||
</parameter>
|
</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">
|
<parameter name="stateContent" type="text">
|
||||||
<label>State Content</label>
|
<label>State Content</label>
|
||||||
<description>Content for state request (only used if method is POST/PUT)</description>
|
<description>Content for state request (only used if method is POST/PUT)</description>
|
||||||
|
@ -9,6 +9,19 @@
|
|||||||
<label>HTTP URL Thing</label>
|
<label>HTTP URL Thing</label>
|
||||||
<description>Represents a base URL and all associated requests.</description>
|
<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>
|
<config-description>
|
||||||
<parameter name="baseURL" type="text" required="true">
|
<parameter name="baseURL" type="text" required="true">
|
||||||
<label>Base URL</label>
|
<label>Base URL</label>
|
||||||
@ -44,7 +57,7 @@
|
|||||||
</parameter>
|
</parameter>
|
||||||
<parameter name="password" type="text">
|
<parameter name="password" type="text">
|
||||||
<label>Password</label>
|
<label>Password</label>
|
||||||
<description>Basic Authentication password</description>
|
<description>Authentication password or token</description>
|
||||||
<context>password</context>
|
<context>password</context>
|
||||||
<advanced>true</advanced>
|
<advanced>true</advanced>
|
||||||
</parameter>
|
</parameter>
|
||||||
@ -54,6 +67,7 @@
|
|||||||
<option value="BASIC">Basic Authentication</option>
|
<option value="BASIC">Basic Authentication</option>
|
||||||
<option value="BASIC_PREEMPTIVE">Preemptive Basic Authentication</option>
|
<option value="BASIC_PREEMPTIVE">Preemptive Basic Authentication</option>
|
||||||
<option value="DIGEST">Digest Authentication</option>
|
<option value="DIGEST">Digest Authentication</option>
|
||||||
|
<option value="TOKEN">Token/Bearer Authentication</option>
|
||||||
</options>
|
</options>
|
||||||
<default>BASIC</default>
|
<default>BASIC</default>
|
||||||
<limitToOptions>true</limitToOptions>
|
<limitToOptions>true</limitToOptions>
|
||||||
@ -112,9 +126,20 @@
|
|||||||
<default>false</default>
|
<default>false</default>
|
||||||
<advanced>true</advanced>
|
<advanced>true</advanced>
|
||||||
</parameter>
|
</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>
|
</config-description>
|
||||||
</thing-type>
|
</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">
|
<channel-type id="color">
|
||||||
<item-type>Color</item-type>
|
<item-type>Color</item-type>
|
||||||
<label>Color Channel</label>
|
<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