mirror of
https://github.com/openhab/openhab-addons.git
synced 2025-01-10 07:02:02 +01:00
[WebThing] Initial contribution (#9555)
Signed-off-by: Gregor Roth <gregor.roth@web.de>
This commit is contained in:
parent
9bfb2f4313
commit
d9ed461950
@ -295,6 +295,7 @@
|
||||
/bundles/org.openhab.binding.volvooncall/ @clinique
|
||||
/bundles/org.openhab.binding.weathercompany/ @mhilbush
|
||||
/bundles/org.openhab.binding.weatherunderground/ @lolodomo
|
||||
/bundles/org.openhab.binding.webthing/ @grro
|
||||
/bundles/org.openhab.binding.wemo/ @hmerk
|
||||
/bundles/org.openhab.binding.wifiled/ @rvt @xylo
|
||||
/bundles/org.openhab.binding.windcentrale/ @marcelrv
|
||||
|
13
bundles/org.openhab.binding.webthing/NOTICE
Normal file
13
bundles/org.openhab.binding.webthing/NOTICE
Normal file
@ -0,0 +1,13 @@
|
||||
This content is produced and maintained by the openHAB project.
|
||||
|
||||
* Project home: https://www.openhab.org
|
||||
|
||||
== Declared Project Licenses
|
||||
|
||||
This program and the accompanying materials are made available under the terms
|
||||
of the Eclipse Public License 2.0 which is available at
|
||||
https://www.eclipse.org/legal/epl-2.0/.
|
||||
|
||||
== Source Code
|
||||
|
||||
https://github.com/openhab/openhab-addons
|
106
bundles/org.openhab.binding.webthing/README.md
Normal file
106
bundles/org.openhab.binding.webthing/README.md
Normal file
@ -0,0 +1,106 @@
|
||||
# WebThing Binding
|
||||
|
||||
The WebThing binding supports an interface to smart home device supporting the Web Thing API.
|
||||
|
||||
The [Web Thing API](https://iot.mozilla.org/wot/) describes an open and generic standard to discover and link smart home devices
|
||||
like motion sensors, web-connected displays or awning controls. Devices implementing the Web Thing standard can be integrated
|
||||
into smart home systems such as openHAB to monitor and control them.
|
||||
These days, the Web Thing API is primarily used by makers to provide a common API to their physical devices.
|
||||
For instance, the Web Thing API has been used by makers to provide an open way to control [LEDs on a ESP8266 board](https://github.com/WebThingsIO/webthing-arduino)
|
||||
or to monitor [a PIR motion sensor on Raspberry Pi](https://pypi.org/project/pi-pir-webthing/).
|
||||
|
||||
## Supported Things
|
||||
|
||||
As a generic solution, the WebThing binding does not depend on specific devices. All devices implementing the Web Thing API should be accessible.
|
||||
|
||||
|
||||
## Discovery
|
||||
|
||||
Once the binding is activated all reachable **WebThing devices will be detected automatically** (via mDNS).
|
||||
|
||||
## Binding Configuration
|
||||
|
||||
No binding configuration required.
|
||||
|
||||
|
||||
## Thing Configuration
|
||||
|
||||
| Parameter | Description | Required |
|
||||
|----------|--------|-------------|
|
||||
| webThingURI | the URI of the WebThing | true |
|
||||
|
||||
Due to the discovery support, **no manual Thing configuration is required** in general. However, under certain circumstances textual
|
||||
Thing configuration may be preferred. In this case, the webThingURI has to be configured as shown in the webthing.things file below:
|
||||
|
||||
```
|
||||
Thing webthing:generic:motionsensor [ webThingURI="http://192.168.1.27:9496/" ]
|
||||
```
|
||||
|
||||
## Channels
|
||||
|
||||
The supported channels depend on the WebThing device that is connected. Each mappable **WebThing property will be mapped to a dedicated channel, automatically**. For instance, to support the *motion property* of a Motion-Sensor WebThing, a dedicated *motion channel* will be created, automatically.
|
||||
|
||||
| Thing | channel | type | description |
|
||||
|--------|----------|--------|------------------------------|
|
||||
| WebThing | Automatic | Automatic | All channels will be generated automatically based on the detected WebThing properties |
|
||||
|
||||
## Full Example
|
||||
|
||||
In the example below WebThings provided by the [Internet Monitor Service](https://pypi.org/project/internet-monitor-webthing/) will be connected.
|
||||
This service does not require specific hardware or devices. To connect the WebThings, the service has to be installed inside your local network.
|
||||
|
||||
|
||||
### Thing
|
||||
|
||||
After installing the WebThing binding you should find the WebThings of your network in the things section of your openHAB administration interface as shown below.
|
||||
|
||||
![discovery picture](docs/discovery.png)
|
||||
|
||||
Here, the WebThings provided by the *Internet Monitor Service*: the *Internet Connectivity* WebThing as well as the
|
||||
*Internet Speed Monitor* WebThing have been discovered. To add a WebThing as an openHAB Thing click the 'Add as Thing' button.
|
||||
|
||||
![discovery picture](docs/speedmonitor.png)
|
||||
|
||||
Alternatively, you may add the WebThing as a openHAB Thing by using a webthing.thing file that has to be located inside the things folder.
|
||||
|
||||
```
|
||||
Thing webthing:generic:speedmonitor [ webThingURI="http://192.168.1.27:9496/0" ]
|
||||
```
|
||||
|
||||
Please consider that the *Internet Monitor Service* in this example supports two WebThings. Both WebThings are bound on the
|
||||
same hostname and port. However, the WebThing URI path of the speed monitor ends with '/0'. In contrast,
|
||||
the connectivity WebThing URI path ends with '/1' in this example.
|
||||
|
||||
Due to the fact that the WebThing API is based on web technologies, you can validate the WebThing description by opening the WebThing uri in a browser.
|
||||
|
||||
![webthing picture](docs/webthing_description.png)
|
||||
|
||||
### Items
|
||||
|
||||
The *Internet Speed Monitor* WebThing used in this example supports properties such as *downloadspeed*, *uploadspeed* or *ping*.
|
||||
For each property of the WebThing a dedicated openHAB channel will be created, automatically. The channelUID such
|
||||
as *webthing:generic:speedmonitor:uploadspeed* is the combination of the thingUID *webthing:generic* and the
|
||||
WebThing property name *uploadspeed*.
|
||||
|
||||
![channels picture](docs/channels.png)
|
||||
|
||||
These channels may be linked via the channels tab of the graphical user interface or manually via a webthing.items file as shown below
|
||||
|
||||
```
|
||||
Number uploadSpeed "uploadspeed speed [%.0f]" {channel="webthing:generic:speedmonitor:uploadspeed"}
|
||||
Number downloadSpeed "download speed [%.0f]" {channel="webthing:generic:speedmonitor:downloadspeed"}
|
||||
|
||||
```
|
||||
|
||||
### Sitemap
|
||||
|
||||
To add the newly linked WebThing items to the sitemap you place a sitemap file such as the internetmonitor.sitemap file shown below
|
||||
|
||||
```
|
||||
sitemap internetmonitor label="Internet Speed Monitor" {
|
||||
Text item=uploadSpeed
|
||||
Text item=downloadSpeed
|
||||
}
|
||||
```
|
||||
|
||||
![sitemap picture](docs/sitemap.png)
|
BIN
bundles/org.openhab.binding.webthing/docs/channels.png
Normal file
BIN
bundles/org.openhab.binding.webthing/docs/channels.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 81 KiB |
BIN
bundles/org.openhab.binding.webthing/docs/discovery.png
Normal file
BIN
bundles/org.openhab.binding.webthing/docs/discovery.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 90 KiB |
BIN
bundles/org.openhab.binding.webthing/docs/sitemap.png
Normal file
BIN
bundles/org.openhab.binding.webthing/docs/sitemap.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 28 KiB |
BIN
bundles/org.openhab.binding.webthing/docs/speedmonitor.png
Normal file
BIN
bundles/org.openhab.binding.webthing/docs/speedmonitor.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 65 KiB |
Binary file not shown.
After Width: | Height: | Size: 120 KiB |
16
bundles/org.openhab.binding.webthing/pom.xml
Normal file
16
bundles/org.openhab.binding.webthing/pom.xml
Normal file
@ -0,0 +1,16 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
<groupId>org.openhab.addons.bundles</groupId>
|
||||
<artifactId>org.openhab.addons.reactor.bundles</artifactId>
|
||||
<version>3.1.0-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>org.openhab.binding.webthing</artifactId>
|
||||
|
||||
<name>openHAB Add-ons :: Bundles :: WebThing Binding</name>
|
||||
</project>
|
@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<features name="org.openhab.binding.webthing-${project.version}" xmlns="http://karaf.apache.org/xmlns/features/v1.4.0">
|
||||
<repository>mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features</repository>
|
||||
|
||||
<feature name="openhab-binding-webthing" description="WebThing Binding" version="${project.version}">
|
||||
<feature>openhab-runtime-base</feature>
|
||||
<feature>openhab-transport-mdns</feature>
|
||||
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.webthing/${project.version}</bundle>
|
||||
</feature>
|
||||
</features>
|
@ -0,0 +1,58 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2021 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.webthing.internal;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.core.thing.ChannelUID;
|
||||
import org.openhab.core.types.Command;
|
||||
import org.openhab.core.types.State;
|
||||
|
||||
/**
|
||||
* The {@link ChannelHandler} class is a simplified abstraction of an openHAB Channel implementing
|
||||
* methods to observe a channel as well to update an Item associated to a channel
|
||||
*
|
||||
* @author Gregor Roth - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public interface ChannelHandler {
|
||||
|
||||
/**
|
||||
* register a listener to observer the channel regarding item change events
|
||||
*
|
||||
* @param channelUID the channel identifier
|
||||
* @param listener the listener to be notified
|
||||
*/
|
||||
void observeChannel(ChannelUID channelUID, ItemChangedListener listener);
|
||||
|
||||
/**
|
||||
* updates an Item state of a dedicated channel
|
||||
*
|
||||
* @param channelUID the channel identifier
|
||||
* @param command the state update command
|
||||
*/
|
||||
void updateItemState(ChannelUID channelUID, Command command);
|
||||
|
||||
/**
|
||||
* Listener that will be notified, if a Item state is changed
|
||||
*/
|
||||
interface ItemChangedListener {
|
||||
|
||||
/**
|
||||
* item change callback method
|
||||
*
|
||||
* @param channelUID the channel identifier
|
||||
* @param stateCommand the item change command
|
||||
*/
|
||||
void onItemStateChanged(ChannelUID channelUID, State stateCommand);
|
||||
}
|
||||
}
|
@ -0,0 +1,38 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2021 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.webthing.internal;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.core.thing.ThingTypeUID;
|
||||
|
||||
/**
|
||||
* The {@link WebThingBindingConstants} class defines common constants, which are
|
||||
* used across the whole binding.
|
||||
*
|
||||
* @author Gregor Roth - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class WebThingBindingConstants {
|
||||
|
||||
public static final String BINDING_ID = "webthing";
|
||||
|
||||
public static final ThingTypeUID THING_TYPE_UID = new ThingTypeUID(BINDING_ID, "generic");
|
||||
|
||||
public static final Collection<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Collections
|
||||
.singleton(WebThingBindingConstants.THING_TYPE_UID);
|
||||
|
||||
public static final String MDNS_SERVICE_TYPE = "_webthing._tcp.local.";
|
||||
}
|
@ -0,0 +1,31 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2021 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.webthing.internal;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* The {@link WebThingConfiguration} class contains fields mapping thing configuration parameters.
|
||||
*
|
||||
* @author Gregor Roth - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class WebThingConfiguration {
|
||||
|
||||
/**
|
||||
* The webThing uri. This URI will be detected within the discovery process
|
||||
*/
|
||||
@Nullable
|
||||
public String webThingURI = null;
|
||||
}
|
@ -0,0 +1,298 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2021 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.webthing.internal;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.eclipse.jetty.client.HttpClient;
|
||||
import org.eclipse.jetty.websocket.client.WebSocketClient;
|
||||
import org.openhab.binding.webthing.internal.channel.Channels;
|
||||
import org.openhab.binding.webthing.internal.client.*;
|
||||
import org.openhab.binding.webthing.internal.link.ChannelToPropertyLink;
|
||||
import org.openhab.binding.webthing.internal.link.PropertyToChannelLink;
|
||||
import org.openhab.binding.webthing.internal.link.UnknownPropertyException;
|
||||
import org.openhab.core.thing.*;
|
||||
import org.openhab.core.thing.binding.BaseThingHandler;
|
||||
import org.openhab.core.types.Command;
|
||||
import org.openhab.core.types.RefreshType;
|
||||
import org.openhab.core.types.State;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* The {@link WebThingHandler} is responsible for handling commands, which are
|
||||
* sent to one of the channels.
|
||||
*
|
||||
* @author Gregor Roth - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class WebThingHandler extends BaseThingHandler implements ChannelHandler {
|
||||
private static final Duration RECONNECT_PERIOD = Duration.ofHours(23);
|
||||
private static final Duration HEALTH_CHECK_PERIOD = Duration.ofSeconds(70);
|
||||
private static final ItemChangedListener EMPTY_ITEM_CHANGED_LISTENER = (channelUID, stateCommand) -> {
|
||||
};
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(WebThingHandler.class);
|
||||
private final HttpClient httpClient;
|
||||
private final WebSocketClient webSocketClient;
|
||||
private final AtomicBoolean isActivated = new AtomicBoolean(true);
|
||||
private final Map<ChannelUID, ItemChangedListener> itemChangedListenerMap = new ConcurrentHashMap<>();
|
||||
private final AtomicReference<Optional<ConsumedThing>> webThingConnectionRef = new AtomicReference<>(
|
||||
Optional.empty());
|
||||
private final AtomicReference<Instant> lastReconnect = new AtomicReference<>(Instant.now());
|
||||
private final AtomicReference<Optional<ScheduledFuture<?>>> watchdogHandle = new AtomicReference<>(
|
||||
Optional.empty());
|
||||
private @Nullable URI webThingURI = null;
|
||||
|
||||
public WebThingHandler(Thing thing, HttpClient httpClient, WebSocketClient webSocketClient) {
|
||||
super(thing);
|
||||
this.httpClient = httpClient;
|
||||
this.webSocketClient = webSocketClient;
|
||||
}
|
||||
|
||||
private boolean isOnline() {
|
||||
return getThing().getStatus() == ThingStatus.ONLINE;
|
||||
}
|
||||
|
||||
private boolean isDisconnected() {
|
||||
return (getThing().getStatus() == ThingStatus.OFFLINE) || (getThing().getStatus() == ThingStatus.UNKNOWN);
|
||||
}
|
||||
|
||||
private boolean isAlive() {
|
||||
return webThingConnectionRef.get().map(ConsumedThing::isAlive).orElse(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initialize() {
|
||||
updateStatus(ThingStatus.UNKNOWN);
|
||||
isActivated.set(true); // set with true, even though the connect may fail. In this case retries will be
|
||||
// triggered
|
||||
|
||||
// perform connect in background
|
||||
scheduler.execute(() -> {
|
||||
// WebThing URI present?
|
||||
var uri = toUri(getConfigAs(WebThingConfiguration.class).webThingURI);
|
||||
if (uri != null) {
|
||||
logger.debug("try to connect WebThing {}", uri);
|
||||
var connected = tryReconnect(uri);
|
||||
if (connected) {
|
||||
logger.debug("WebThing {} connected", getWebThingLabel());
|
||||
}
|
||||
} else {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
|
||||
"webThing uri has not been set");
|
||||
logger.warn("could not initialize WebThing. URI is not set or invalid. {}", this.webThingURI);
|
||||
}
|
||||
});
|
||||
|
||||
// starting watchdog that checks the healthiness of the WebThing connection, periodically
|
||||
watchdogHandle
|
||||
.getAndSet(Optional.of(scheduler.scheduleWithFixedDelay(this::checkWebThingConnection,
|
||||
HEALTH_CHECK_PERIOD.getSeconds(), HEALTH_CHECK_PERIOD.getSeconds(), TimeUnit.SECONDS)))
|
||||
.ifPresent(future -> future.cancel(true));
|
||||
}
|
||||
|
||||
private @Nullable URI toUri(@Nullable String uri) {
|
||||
try {
|
||||
if (uri != null) {
|
||||
return URI.create(uri);
|
||||
}
|
||||
} catch (IllegalArgumentException illegalURIException) {
|
||||
return null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose() {
|
||||
try {
|
||||
isActivated.set(false); // set to false to avoid reconnecting
|
||||
|
||||
// terminate WebThing connection as well as the alive watchdog
|
||||
webThingConnectionRef.getAndSet(Optional.empty()).ifPresent(ConsumedThing::close);
|
||||
watchdogHandle.getAndSet(Optional.empty()).ifPresent(future -> future.cancel(true));
|
||||
} finally {
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
private boolean tryReconnect(@Nullable URI uri) {
|
||||
if (isActivated.get()) { // will try reconnect only, if activated
|
||||
try {
|
||||
// create the client-side WebThing representation
|
||||
if (uri != null) {
|
||||
var webThing = ConsumedThingFactory.instance().create(webSocketClient, httpClient, uri, scheduler,
|
||||
this::onError);
|
||||
this.webThingConnectionRef.getAndSet(Optional.of(webThing)).ifPresent(ConsumedThing::close);
|
||||
|
||||
// update the Thing structure based on the WebThing description
|
||||
thingStructureChanged(webThing);
|
||||
|
||||
// link the Thing's channels with the WebThing properties to forward properties/item updates
|
||||
establishWebThingChannelLinks(webThing);
|
||||
|
||||
lastReconnect.set(Instant.now());
|
||||
updateStatus(ThingStatus.ONLINE);
|
||||
return true;
|
||||
}
|
||||
} catch (IOException e) {
|
||||
var msg = e.getMessage();
|
||||
if (msg == null) {
|
||||
msg = "";
|
||||
}
|
||||
onError(msg);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public void onError(String reason) {
|
||||
var wasConnectedBefore = isOnline();
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, reason);
|
||||
|
||||
// close the WebThing connection. If the handler is still active, the WebThing connection
|
||||
// will be re-established within the periodically watchdog task
|
||||
webThingConnectionRef.getAndSet(Optional.empty()).ifPresent(ConsumedThing::close);
|
||||
|
||||
if (wasConnectedBefore) { // to reduce log messages, just log in case of connection state changed
|
||||
logger.debug("WebThing {} disconnected {}. Try reconnect (each {} sec)", getWebThingLabel(), reason,
|
||||
HEALTH_CHECK_PERIOD.getSeconds());
|
||||
} else {
|
||||
logger.debug("WebThing {} is offline {}. Try reconnect (each {} sec)", getWebThingLabel(), reason,
|
||||
HEALTH_CHECK_PERIOD.getSeconds());
|
||||
}
|
||||
}
|
||||
|
||||
private String getWebThingLabel() {
|
||||
if (getThing().getLabel() == null) {
|
||||
return "" + webThingURI;
|
||||
} else {
|
||||
return "'" + getThing().getLabel() + "' (" + webThingURI + ")";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* updates the thing structure. Refer https://www.openhab.org/docs/developer/bindings/#updating-the-thing-structure
|
||||
*
|
||||
* @param webThing the WebThing that is used for the new structure
|
||||
*/
|
||||
private void thingStructureChanged(ConsumedThing webThing) {
|
||||
var thingBuilder = editThing().withLabel(webThing.getThingDescription().title);
|
||||
|
||||
// create a channel for each WebThing property
|
||||
for (var entry : webThing.getThingDescription().properties.entrySet()) {
|
||||
var channel = Channels.createChannel(thing.getUID(), entry.getKey(), entry.getValue());
|
||||
// add channel (and remove a previous one, if exist)
|
||||
thingBuilder.withoutChannel(channel.getUID()).withChannel(channel);
|
||||
}
|
||||
var thing = thingBuilder.build();
|
||||
|
||||
// and update the thing
|
||||
updateThing(thing);
|
||||
}
|
||||
|
||||
/**
|
||||
* connects each WebThing property with a corresponding openHAB channel. After this changes will be synchronized
|
||||
* between a WebThing property and the openHAB channel
|
||||
*
|
||||
* @param webThing the WebThing to be connected
|
||||
* @throws IOException if the channels can not be connected
|
||||
*/
|
||||
private void establishWebThingChannelLinks(ConsumedThing webThing) throws IOException {
|
||||
// remove all registered listeners
|
||||
itemChangedListenerMap.clear();
|
||||
|
||||
// create new links (listeners will be registered, implicitly)
|
||||
for (var namePropertyPair : webThing.getThingDescription().properties.entrySet()) {
|
||||
try {
|
||||
// determine the name of the associated channel
|
||||
var channelUID = Channels.createChannelUID(getThing().getUID(), namePropertyPair.getKey());
|
||||
|
||||
// will try to establish a link, if channel is present
|
||||
var channel = getThing().getChannel(channelUID);
|
||||
if (channel != null) {
|
||||
// establish downstream link
|
||||
PropertyToChannelLink.establish(webThing, namePropertyPair.getKey(), this, channel);
|
||||
|
||||
// establish upstream link
|
||||
if (!namePropertyPair.getValue().readOnly) {
|
||||
ChannelToPropertyLink.establish(this, channel, webThing, namePropertyPair.getKey());
|
||||
}
|
||||
}
|
||||
} catch (UnknownPropertyException upe) {
|
||||
logger.warn("WebThing {} property {} could not be linked with a channel", getWebThingLabel(),
|
||||
namePropertyPair.getKey(), upe);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleCommand(ChannelUID channelUID, Command command) {
|
||||
if (command instanceof State) {
|
||||
itemChangedListenerMap.getOrDefault(channelUID, EMPTY_ITEM_CHANGED_LISTENER).onItemStateChanged(channelUID,
|
||||
(State) command);
|
||||
} else if (command instanceof RefreshType) {
|
||||
tryReconnect(webThingURI);
|
||||
}
|
||||
}
|
||||
|
||||
/////////////
|
||||
// ChannelHandler methods
|
||||
@Override
|
||||
public void observeChannel(ChannelUID channelUID, ItemChangedListener listener) {
|
||||
itemChangedListenerMap.put(channelUID, listener);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateItemState(ChannelUID channelUID, Command command) {
|
||||
if (isActivated.get()) {
|
||||
postCommand(channelUID, command);
|
||||
}
|
||||
}
|
||||
//
|
||||
/////////////
|
||||
|
||||
private void checkWebThingConnection() {
|
||||
// try reconnect, if necessary
|
||||
if (isDisconnected() || (isOnline() && !isAlive())) {
|
||||
logger.debug("try reconnecting WebThing {}", getWebThingLabel());
|
||||
if (tryReconnect(webThingURI)) {
|
||||
logger.debug("WebThing {} reconnected", getWebThingLabel());
|
||||
}
|
||||
|
||||
} else {
|
||||
// force reconnecting periodically, to fix erroneous states that occurs for unknown reasons
|
||||
var elapsedSinceLastReconnect = Duration.between(lastReconnect.get(), Instant.now());
|
||||
if (isOnline() && (elapsedSinceLastReconnect.getSeconds() > RECONNECT_PERIOD.getSeconds())) {
|
||||
if (tryReconnect(webThingURI)) {
|
||||
logger.debug("WebThing {} reconnected. Initiated by periodic reconnect", getWebThingLabel());
|
||||
} else {
|
||||
logger.debug("could not reconnect WebThing {} (periodic reconnect failed). Next trial in {} sec",
|
||||
getWebThingLabel(), HEALTH_CHECK_PERIOD.getSeconds());
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,58 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2021 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.webthing.internal;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.eclipse.jetty.client.HttpClient;
|
||||
import org.eclipse.jetty.websocket.client.WebSocketClient;
|
||||
import org.openhab.core.io.net.http.HttpClientFactory;
|
||||
import org.openhab.core.io.net.http.WebSocketFactory;
|
||||
import org.openhab.core.thing.Thing;
|
||||
import org.openhab.core.thing.ThingTypeUID;
|
||||
import org.openhab.core.thing.binding.BaseThingHandlerFactory;
|
||||
import org.openhab.core.thing.binding.ThingHandler;
|
||||
import org.openhab.core.thing.binding.ThingHandlerFactory;
|
||||
import org.osgi.service.component.annotations.Activate;
|
||||
import org.osgi.service.component.annotations.Component;
|
||||
import org.osgi.service.component.annotations.Reference;
|
||||
|
||||
/**
|
||||
* The {@link WebThingHandlerFactory} is responsible for creating things and thing
|
||||
* handlers.
|
||||
*
|
||||
* @author Gregor Roth - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
@Component(configurationPid = "binding.webthing", service = ThingHandlerFactory.class)
|
||||
public class WebThingHandlerFactory extends BaseThingHandlerFactory {
|
||||
private final HttpClient httpClient;
|
||||
private final WebSocketClient webSocketClient;
|
||||
|
||||
@Activate
|
||||
public WebThingHandlerFactory(@Reference HttpClientFactory httpClientFactory,
|
||||
@Reference WebSocketFactory webSocketFactory) {
|
||||
this.httpClient = httpClientFactory.getCommonHttpClient();
|
||||
this.webSocketClient = webSocketFactory.getCommonWebSocketClient();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsThingType(ThingTypeUID thingTypeUID) {
|
||||
return WebThingBindingConstants.SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected @Nullable ThingHandler createHandler(Thing thing) {
|
||||
return new WebThingHandler(thing, httpClient, webSocketClient);
|
||||
}
|
||||
}
|
@ -0,0 +1,74 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2021 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.webthing.internal.channel;
|
||||
|
||||
import static org.openhab.binding.webthing.internal.WebThingBindingConstants.BINDING_ID;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.binding.webthing.internal.client.dto.Property;
|
||||
import org.openhab.binding.webthing.internal.link.TypeMapping;
|
||||
import org.openhab.core.thing.Channel;
|
||||
import org.openhab.core.thing.ChannelUID;
|
||||
import org.openhab.core.thing.ThingUID;
|
||||
import org.openhab.core.thing.binding.builder.ChannelBuilder;
|
||||
import org.openhab.core.thing.type.ChannelTypeUID;
|
||||
|
||||
/**
|
||||
* The {@link Channels} class is an utility class to create Channel based on the property characteristics as
|
||||
* well as ChannelUID identifier
|
||||
*
|
||||
* @author Gregor Roth - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class Channels {
|
||||
|
||||
/**
|
||||
* create a ChannelUIFD identifier for a given property name
|
||||
*
|
||||
* @param thingUID the thing uid of the associated WebThing
|
||||
* @param propertyName the property name
|
||||
* @return the ChannelUID identifier
|
||||
*/
|
||||
public static ChannelUID createChannelUID(ThingUID thingUID, String propertyName) {
|
||||
return new ChannelUID(thingUID.toString() + ":" + propertyName);
|
||||
}
|
||||
|
||||
/**
|
||||
* create a Channel base on a given WebThing property
|
||||
*
|
||||
* @param thingUID the thing uid of the associated WebThing
|
||||
* @param propertyName the property name
|
||||
* @param property the WebThing property
|
||||
* @return the Channel according to the properties characteristics
|
||||
*/
|
||||
public static Channel createChannel(ThingUID thingUID, String propertyName, Property property) {
|
||||
var itemType = TypeMapping.toItemType(property);
|
||||
var channelUID = createChannelUID(thingUID, propertyName);
|
||||
var channelBuilder = ChannelBuilder.create(channelUID, itemType.getType());
|
||||
|
||||
// Currently, few predefined, generic channel types such as number, string or color are defined
|
||||
// inside the thing-types.xml file. A better solution would be to create the channel types
|
||||
// dynamically based on the WebThing description to make most of the meta data of a WebThing.
|
||||
// The goal of the WebThing meta data is to enable semantic interoperability for connected things
|
||||
channelBuilder.withType(new ChannelTypeUID(BINDING_ID, itemType.getType()));
|
||||
channelBuilder.withDescription(property.description);
|
||||
channelBuilder.withLabel(property.title);
|
||||
var defaultTag = itemType.getTag();
|
||||
if (defaultTag != null) {
|
||||
channelBuilder.withDefaultTags(Set.of(defaultTag));
|
||||
}
|
||||
return channelBuilder.build();
|
||||
}
|
||||
}
|
@ -0,0 +1,70 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2021 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.webthing.internal.client;
|
||||
|
||||
import java.util.function.BiConsumer;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.binding.webthing.internal.client.dto.WebThingDescription;
|
||||
|
||||
/**
|
||||
* A WebThing represents the client-side proxy of a remote devices implementing the Web Thing API according to
|
||||
* https://iot.mozilla.org/wot/
|
||||
* The API design is oriented on https://www.w3.org/TR/wot-scripting-api/#the-consumedthing-interface
|
||||
*
|
||||
* @author Gregor Roth - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public interface ConsumedThing {
|
||||
|
||||
/**
|
||||
* @return the description (meta data) of the WebThing
|
||||
*/
|
||||
WebThingDescription getThingDescription();
|
||||
|
||||
/**
|
||||
* Makes a request for Property value change notifications
|
||||
*
|
||||
* @param propertyName the property to be observed
|
||||
* @param listener the listener to call on changes
|
||||
*/
|
||||
void observeProperty(String propertyName, BiConsumer<String, Object> listener);
|
||||
|
||||
/**
|
||||
* Writes a single Property.
|
||||
*
|
||||
* @param propertyName the propertyName
|
||||
* @return the current propertyValue
|
||||
* @throws PropertyAccessException if the property can not be read
|
||||
*/
|
||||
Object readProperty(String propertyName) throws PropertyAccessException;
|
||||
|
||||
/**
|
||||
* Writes a single Property.
|
||||
*
|
||||
* @param propertyName the propertyName
|
||||
* @param newValue the new propertyValue
|
||||
* @throws PropertyAccessException if the property can not be written
|
||||
*/
|
||||
void writeProperty(String propertyName, Object newValue) throws PropertyAccessException;
|
||||
|
||||
/**
|
||||
* @return true, if connection is alive
|
||||
*/
|
||||
boolean isAlive();
|
||||
|
||||
/**
|
||||
* closes the connection
|
||||
*/
|
||||
void close();
|
||||
}
|
@ -0,0 +1,50 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2021 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.webthing.internal.client;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jetty.client.HttpClient;
|
||||
import org.eclipse.jetty.websocket.client.WebSocketClient;
|
||||
|
||||
/**
|
||||
* Factory to create new instances of the WebThing client-side proxy
|
||||
*
|
||||
* @author Gregor Roth - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public interface ConsumedThingFactory {
|
||||
|
||||
/**
|
||||
* @param webSocketClient the webSocketClient to use
|
||||
* @param httpClient the http client to use
|
||||
* @param webThingURI the identifier of a WebThing resource
|
||||
* @param executor executor
|
||||
* @param errorHandler the error handler
|
||||
* @return the newly created WebThing
|
||||
* @throws IOException if the WebThing can not be connected
|
||||
*/
|
||||
ConsumedThing create(WebSocketClient webSocketClient, HttpClient httpClient, URI webThingURI,
|
||||
ScheduledExecutorService executor, Consumer<String> errorHandler) throws IOException;
|
||||
|
||||
/**
|
||||
* @return the default instance of the factory
|
||||
*/
|
||||
static ConsumedThingFactory instance() {
|
||||
return ConsumedThingImpl::new;
|
||||
}
|
||||
}
|
@ -0,0 +1,262 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2021 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.webthing.internal.client;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.time.Duration;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.function.BiConsumer;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.eclipse.jetty.client.HttpClient;
|
||||
import org.eclipse.jetty.client.util.StringContentProvider;
|
||||
import org.eclipse.jetty.websocket.client.WebSocketClient;
|
||||
import org.openhab.binding.webthing.internal.client.dto.Property;
|
||||
import org.openhab.binding.webthing.internal.client.dto.WebThingDescription;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
|
||||
/**
|
||||
* The implementation of the client-side Webthing representation. This is based on HTTP. Bindings to alternative
|
||||
* application protocols such as CoAP may be defined in the future (which may be implemented by a another class)
|
||||
*
|
||||
* @author Gregor Roth - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class ConsumedThingImpl implements ConsumedThing {
|
||||
private static final Duration DEFAULT_PING_PERIOD = Duration.ofSeconds(80);
|
||||
private final Logger logger = LoggerFactory.getLogger(ConsumedThingImpl.class);
|
||||
private final URI webThingURI;
|
||||
private final Gson gson = new Gson();
|
||||
private final HttpClient httpClient;
|
||||
private final Consumer<String> errorHandler;
|
||||
private final WebThingDescription description;
|
||||
private final WebSocketConnection websocketDownstream;
|
||||
private final AtomicBoolean isOpen = new AtomicBoolean(true);
|
||||
|
||||
/**
|
||||
* constructor
|
||||
*
|
||||
* @param webSocketClient the web socket client to use
|
||||
* @param httpClient the http client to use
|
||||
* @param webThingURI the identifier of a WebThing resource
|
||||
* @param executor executor to use
|
||||
* @param errorHandler the error handler
|
||||
* @throws IOException it the WebThing can not be connected
|
||||
*/
|
||||
ConsumedThingImpl(WebSocketClient webSocketClient, HttpClient httpClient, URI webThingURI,
|
||||
ScheduledExecutorService executor, Consumer<String> errorHandler) throws IOException {
|
||||
this(httpClient, webThingURI, executor, errorHandler, WebSocketConnectionFactory.instance(webSocketClient));
|
||||
}
|
||||
|
||||
/**
|
||||
* constructor
|
||||
*
|
||||
* @param httpClient the http client to use
|
||||
* @param webthingUrl the identifier of a WebThing resource
|
||||
* @param executor executor to use
|
||||
* @param errorHandler the error handler
|
||||
* @param webSocketConnectionFactory the Websocket connectino fctory to be used
|
||||
* @throws IOException if the WebThing can not be connected
|
||||
*/
|
||||
ConsumedThingImpl(HttpClient httpClient, URI webthingUrl, ScheduledExecutorService executor,
|
||||
Consumer<String> errorHandler, WebSocketConnectionFactory webSocketConnectionFactory) throws IOException {
|
||||
this(httpClient, webthingUrl, executor, errorHandler, webSocketConnectionFactory, DEFAULT_PING_PERIOD);
|
||||
}
|
||||
|
||||
/**
|
||||
* constructor
|
||||
*
|
||||
* @param httpClient the http client to use
|
||||
* @param webthingUrl the identifier of a WebThing resource
|
||||
* @param executor executor to use
|
||||
* @param errorHandler the error handler
|
||||
* @param webSocketConnectionFactory the Websocket connectino fctory to be used
|
||||
* @param pingPeriod the ping period tothe the healthiness of the connection
|
||||
* @throws IOException if the WebThing can not be connected
|
||||
*/
|
||||
ConsumedThingImpl(HttpClient httpClient, URI webthingUrl, ScheduledExecutorService executor,
|
||||
Consumer<String> errorHandler, WebSocketConnectionFactory webSocketConnectionFactory, Duration pingPeriod)
|
||||
throws IOException {
|
||||
this.webThingURI = webthingUrl;
|
||||
this.httpClient = httpClient;
|
||||
this.errorHandler = errorHandler;
|
||||
this.description = new DescriptionLoader(httpClient).loadWebthingDescription(webThingURI,
|
||||
Duration.ofSeconds(20));
|
||||
|
||||
// opens a websocket downstream to be notified if a property value will be changed
|
||||
var optionalEventStreamUri = this.description.getEventStreamUri();
|
||||
if (optionalEventStreamUri.isPresent()) {
|
||||
this.websocketDownstream = webSocketConnectionFactory.create(optionalEventStreamUri.get(), executor,
|
||||
this::onError, pingPeriod);
|
||||
} else {
|
||||
throw new IOException("WebThing " + webThingURI + " does not support websocket uri. WebThing description: "
|
||||
+ this.description);
|
||||
}
|
||||
}
|
||||
|
||||
private Optional<URI> getPropertyUri(String propertyName) {
|
||||
var optionalProperty = description.getProperty(propertyName);
|
||||
if (optionalProperty.isPresent()) {
|
||||
var propertyDescription = optionalProperty.get();
|
||||
for (var link : propertyDescription.links) {
|
||||
if ((link.rel != null) && (link.href != null) && link.rel.equals("property")) {
|
||||
return Optional.of(webThingURI.resolve(link.href));
|
||||
}
|
||||
}
|
||||
}
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isAlive() {
|
||||
return isOpen.get() && this.websocketDownstream.isAlive();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
isOpen.set(false);
|
||||
this.websocketDownstream.close();
|
||||
}
|
||||
|
||||
void onError(String reason) {
|
||||
logger.debug("WebThing {} error occurred. {}", webThingURI, reason);
|
||||
if (isOpen.get()) {
|
||||
errorHandler.accept(reason);
|
||||
}
|
||||
close();
|
||||
}
|
||||
|
||||
@Override
|
||||
public WebThingDescription getThingDescription() {
|
||||
return this.description;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void observeProperty(String propertyName, BiConsumer<String, Object> listener) {
|
||||
this.websocketDownstream.observeProperty(propertyName, listener);
|
||||
|
||||
// it may take a long time before the observed property value will be changed. For this reason
|
||||
// read and notify the current property value (as starting point)
|
||||
try {
|
||||
var value = readProperty(propertyName);
|
||||
listener.accept(propertyName, value);
|
||||
} catch (PropertyAccessException pae) {
|
||||
logger.warn("could not read WebThing {} property {}", webThingURI, propertyName, pae);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object readProperty(String propertyName) throws PropertyAccessException {
|
||||
var optionalPropertyUri = getPropertyUri(propertyName);
|
||||
if (optionalPropertyUri.isPresent()) {
|
||||
var propertyUri = optionalPropertyUri.get();
|
||||
try {
|
||||
var response = httpClient.newRequest(propertyUri).timeout(30, TimeUnit.SECONDS)
|
||||
.accept("application/json").send();
|
||||
if (response.getStatus() < 200 || response.getStatus() >= 300) {
|
||||
onError("WebThing " + webThingURI + " disconnected");
|
||||
throw new PropertyAccessException("could not read " + propertyName + " (" + propertyUri + ")");
|
||||
}
|
||||
var body = response.getContentAsString();
|
||||
var properties = gson.fromJson(body, Map.class);
|
||||
if (properties == null) {
|
||||
onError("WebThing " + webThingURI + " erroneous");
|
||||
throw new PropertyAccessException("could not read " + propertyName + " (" + propertyUri
|
||||
+ "). Response does not include any property (" + propertyUri + "): " + body);
|
||||
} else {
|
||||
var value = properties.get(propertyName);
|
||||
if (value != null) {
|
||||
return value;
|
||||
} else {
|
||||
onError("WebThing " + webThingURI + " erroneous");
|
||||
throw new PropertyAccessException("could not read " + propertyName + " (" + propertyUri
|
||||
+ "). Response does not include " + propertyName + "(" + propertyUri + "): " + body);
|
||||
}
|
||||
}
|
||||
} catch (ExecutionException | TimeoutException | InterruptedException e) {
|
||||
onError("WebThing resource " + webThingURI + " disconnected");
|
||||
throw new PropertyAccessException("could not read " + propertyName + " (" + propertyUri + ").", e);
|
||||
}
|
||||
} else {
|
||||
onError("WebThing " + webThingURI + " does not support " + propertyName);
|
||||
throw new PropertyAccessException("WebThing " + webThingURI + " does not support " + propertyName);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeProperty(String propertyName, Object newValue) throws PropertyAccessException {
|
||||
var optionalPropertyUri = getPropertyUri(propertyName);
|
||||
if (optionalPropertyUri.isPresent()) {
|
||||
var propertyUri = optionalPropertyUri.get();
|
||||
var optionalProperty = description.getProperty(propertyName);
|
||||
if (optionalProperty.isPresent()) {
|
||||
try {
|
||||
if (optionalProperty.get().readOnly) {
|
||||
throw new PropertyAccessException("could not write " + propertyName + " (" + propertyUri
|
||||
+ ") with " + newValue + ". Property is readOnly");
|
||||
} else {
|
||||
logger.debug("updating {} with {}", propertyName, newValue);
|
||||
Map<String, Object> payload = Map.of(propertyName, newValue);
|
||||
var json = gson.toJson(payload);
|
||||
var response = httpClient.newRequest(propertyUri).method("PUT")
|
||||
.content(new StringContentProvider(json), "application/json")
|
||||
.timeout(30, TimeUnit.SECONDS).send();
|
||||
if (response.getStatus() < 200 || response.getStatus() >= 300) {
|
||||
onError("WebThing " + webThingURI + "could not write " + propertyName + " (" + propertyUri
|
||||
+ ") with " + newValue);
|
||||
throw new PropertyAccessException(
|
||||
"could not write " + propertyName + " (" + propertyUri + ") with " + newValue);
|
||||
}
|
||||
}
|
||||
} catch (ExecutionException | TimeoutException | InterruptedException e) {
|
||||
onError("WebThing resource " + webThingURI + " disconnected");
|
||||
throw new PropertyAccessException(
|
||||
"could not write " + propertyName + " (" + propertyUri + ") with " + newValue, e);
|
||||
}
|
||||
} else {
|
||||
throw new PropertyAccessException("could not write " + propertyName + " (" + propertyUri + ") with "
|
||||
+ newValue + " WebTing does not support a property named " + propertyName);
|
||||
}
|
||||
} else {
|
||||
onError("WebThing " + webThingURI + " does not support " + propertyName);
|
||||
throw new PropertyAccessException("WebThing " + webThingURI + " does not support " + propertyName);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the property description
|
||||
*
|
||||
* @param propertyName the propertyName
|
||||
* @return the description (meta data) of the property
|
||||
*/
|
||||
public @Nullable Property getPropertyDescription(String propertyName) {
|
||||
return description.properties.get(propertyName);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "WebThing " + description.title + " (" + webThingURI + ")";
|
||||
}
|
||||
}
|
@ -0,0 +1,97 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2021 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.webthing.internal.client;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.time.Duration;
|
||||
import java.util.Locale;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jetty.client.HttpClient;
|
||||
import org.openhab.binding.webthing.internal.client.dto.WebThingDescription;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.JsonSyntaxException;
|
||||
|
||||
/**
|
||||
* Utility class to load the WebThing description (meta data). Refer https://iot.mozilla.org/wot/#web-thing-description
|
||||
*
|
||||
* @author Gregor Roth - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class DescriptionLoader {
|
||||
private final Logger logger = LoggerFactory.getLogger(DescriptionLoader.class);
|
||||
private final Gson gson = new Gson();
|
||||
private final HttpClient httpClient;
|
||||
|
||||
/**
|
||||
* constructor
|
||||
*
|
||||
* @param httpClient the http client to use
|
||||
*/
|
||||
public DescriptionLoader(HttpClient httpClient) {
|
||||
this.httpClient = httpClient;
|
||||
}
|
||||
|
||||
/**
|
||||
* loads the WebThing meta data
|
||||
*
|
||||
* @param webthingURI the WebThing URI
|
||||
* @param timeout the timeout
|
||||
* @return the Webthing description
|
||||
* @throws IOException if the WebThing can not be connected
|
||||
*/
|
||||
public WebThingDescription loadWebthingDescription(URI webthingURI, Duration timeout) throws IOException {
|
||||
try {
|
||||
var response = httpClient.newRequest(webthingURI).timeout(30, TimeUnit.SECONDS).accept("application/json")
|
||||
.send();
|
||||
if (response.getStatus() < 200 || response.getStatus() >= 300) {
|
||||
throw new IOException(
|
||||
"could not read resource description " + webthingURI + ". Got " + response.getStatus());
|
||||
}
|
||||
var body = response.getContentAsString();
|
||||
var description = gson.fromJson(body, WebThingDescription.class);
|
||||
if ((description != null) && (description.properties != null) && (description.properties.size() > 0)) {
|
||||
if ((description.contextKeyword == null) || description.contextKeyword.trim().length() == 0) {
|
||||
description.contextKeyword = "https://webthings.io/schemas";
|
||||
}
|
||||
var schema = description.contextKeyword.replaceFirst("/$", "").toLowerCase(Locale.US).trim();
|
||||
|
||||
// currently, the old and new location of the WebThings schema are supported only.
|
||||
// In the future, other schemas such as http://iotschema.org/docs/full.html may be supported
|
||||
if (schema.equals("https://webthings.io/schemas") || schema.equals("https://iot.mozilla.org/schemas")) {
|
||||
return description;
|
||||
}
|
||||
logger.debug(
|
||||
"WebThing {} detected with unsupported schema {} (Supported schemas are https://webthings.io/schemas and https://iot.mozilla.org/schemas)",
|
||||
webthingURI, description.contextKeyword);
|
||||
throw new IOException("unsupported schema (@context parameter) " + description.contextKeyword
|
||||
+ " (Supported schemas are https://webthings.io/schemas and https://iot.mozilla.org/schemas)");
|
||||
} else {
|
||||
throw new IOException("description does not include properties");
|
||||
}
|
||||
} catch (ExecutionException | TimeoutException e) {
|
||||
throw new IOException("error occurred by calling WebThing", e);
|
||||
} catch (JsonSyntaxException se) {
|
||||
throw new IOException("resource seems not to be a WebThing. Typo?");
|
||||
} catch (InterruptedException ie) {
|
||||
throw new IOException("resource seems not to be reachable");
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,44 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2021 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.webthing.internal.client;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
/**
|
||||
* The {@link PropertyAccessException} indicates a WebThing property can not be accessed
|
||||
*
|
||||
* @author Gregor Roth - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class PropertyAccessException extends Exception {
|
||||
private static final long serialVersionUID = 5177277585758195790L;
|
||||
|
||||
/**
|
||||
* contructor
|
||||
*
|
||||
* @param message the error message
|
||||
*/
|
||||
PropertyAccessException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
/**
|
||||
* contructor
|
||||
*
|
||||
* @param message the error message
|
||||
* @param cause the error cause
|
||||
*/
|
||||
PropertyAccessException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
}
|
@ -0,0 +1,45 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2021 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.webthing.internal.client;
|
||||
|
||||
import java.util.function.BiConsumer;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
/**
|
||||
* The WebsocketConnection represents an open WebSocket connection on the Web Thing. It provides a realtime mechanism
|
||||
* to be notified of events as soon as they happen. Refer https://iot.mozilla.org/wot/#web-thing-websocket-api
|
||||
*
|
||||
* @author Gregor Roth - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
interface WebSocketConnection {
|
||||
|
||||
/**
|
||||
* Makes a request for Property value change notifications
|
||||
*
|
||||
* @param propertyName the property to be observed
|
||||
* @param listener the listener to call on changes
|
||||
*/
|
||||
void observeProperty(String propertyName, BiConsumer<String, Object> listener);
|
||||
|
||||
/**
|
||||
* closes the WebSocket connection
|
||||
*/
|
||||
void close();
|
||||
|
||||
/**
|
||||
* @return true, if connection is alive
|
||||
*/
|
||||
boolean isAlive();
|
||||
}
|
@ -0,0 +1,56 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2021 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.webthing.internal.client;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.time.Duration;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jetty.websocket.client.WebSocketClient;
|
||||
|
||||
/**
|
||||
* Factory to create new instances of a WebSocket connection
|
||||
*
|
||||
* @author Gregor Roth - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
interface WebSocketConnectionFactory {
|
||||
|
||||
/**
|
||||
* create (and opens) a new WebSocket connection
|
||||
*
|
||||
* @param webSocketURI the websocket uri
|
||||
* @param executor the executor to use
|
||||
* @param errorHandler the error handler
|
||||
* @param pingPeriod the ping period to check the healthiness of the connection
|
||||
* @return the newly opened WebSocket connection
|
||||
* @throws IOException if the web socket connection can not be established
|
||||
*/
|
||||
WebSocketConnection create(URI webSocketURI, ScheduledExecutorService executor, Consumer<String> errorHandler,
|
||||
Duration pingPeriod) throws IOException;
|
||||
|
||||
/**
|
||||
* @param webSocketClient the web socket client to use
|
||||
* @return the default instance of the factory
|
||||
*/
|
||||
static WebSocketConnectionFactory instance(WebSocketClient webSocketClient) {
|
||||
return (webSocketURI, executor, errorHandler, pingPeriod) -> {
|
||||
var webSocketConnection = new WebSocketConnectionImpl(executor, errorHandler, pingPeriod);
|
||||
webSocketClient.connect(webSocketConnection, webSocketURI);
|
||||
return webSocketConnection;
|
||||
};
|
||||
}
|
||||
}
|
@ -0,0 +1,185 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2021 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.webthing.internal.client;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.function.BiConsumer;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNull;
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.eclipse.jetty.websocket.api.Session;
|
||||
import org.eclipse.jetty.websocket.api.WebSocketListener;
|
||||
import org.eclipse.jetty.websocket.api.WebSocketPingPongListener;
|
||||
import org.openhab.binding.webthing.internal.client.dto.PropertyStatusMessage;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.JsonSyntaxException;
|
||||
|
||||
/**
|
||||
* The WebsocketConnection implementation
|
||||
*
|
||||
* @author Gregor Roth - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class WebSocketConnectionImpl implements WebSocketConnection, WebSocketListener, WebSocketPingPongListener {
|
||||
private static final BiConsumer<String, Object> EMPTY_PROPERTY_CHANGED_LISTENER = (String propertyName,
|
||||
Object value) -> {
|
||||
};
|
||||
private final Logger logger = LoggerFactory.getLogger(WebSocketConnectionImpl.class);
|
||||
private final Gson gson = new Gson();
|
||||
private final Duration pingPeriod;
|
||||
private final Consumer<String> errorHandler;
|
||||
private final ScheduledFuture<?> watchDogHandle;
|
||||
private final ScheduledFuture<?> pingHandle;
|
||||
private final Map<String, BiConsumer<String, Object>> propertyChangedListeners = new HashMap<>();
|
||||
private final AtomicReference<Instant> lastTimeReceived = new AtomicReference<>(Instant.now());
|
||||
private final AtomicReference<Optional<Session>> sessionRef = new AtomicReference<>(Optional.empty());
|
||||
|
||||
/**
|
||||
* constructor
|
||||
*
|
||||
* @param executor the executor to use
|
||||
* @param errorHandler the errorHandler
|
||||
* @param pingPeriod the period pings should be sent
|
||||
*/
|
||||
WebSocketConnectionImpl(ScheduledExecutorService executor, Consumer<String> errorHandler, Duration pingPeriod) {
|
||||
this.errorHandler = errorHandler;
|
||||
this.pingPeriod = pingPeriod;
|
||||
|
||||
// send a ping message are x seconds to validate if the connection is not broken
|
||||
this.pingHandle = executor.scheduleWithFixedDelay(this::sendPing, pingPeriod.dividedBy(2).toMillis(),
|
||||
pingPeriod.toMillis(), TimeUnit.MILLISECONDS);
|
||||
|
||||
// checks if a message (regular message or pong message) has been received recently. If not, connection is
|
||||
// seen as broken
|
||||
this.watchDogHandle = executor.scheduleWithFixedDelay(this::checkConnection, pingPeriod.toMillis(),
|
||||
pingPeriod.toMillis(), TimeUnit.MILLISECONDS);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
sessionRef.getAndSet(Optional.empty()).ifPresent(Session::close);
|
||||
watchDogHandle.cancel(true);
|
||||
pingHandle.cancel(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void observeProperty(@NonNull String propertyName, @NonNull BiConsumer<String, Object> listener) {
|
||||
propertyChangedListeners.put(propertyName, listener);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onWebSocketConnect(@Nullable Session session) {
|
||||
sessionRef.set(Optional.ofNullable(session)); // save websocket session to be able to send ping
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onWebSocketPing(@Nullable ByteBuffer payload) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onWebSocketPong(@Nullable ByteBuffer payload) {
|
||||
lastTimeReceived.set(Instant.now());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onWebSocketBinary(byte @Nullable [] payload, int offset, int len) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onWebSocketText(@Nullable String message) {
|
||||
try {
|
||||
if (message != null) {
|
||||
var propertyStatus = gson.fromJson(message, PropertyStatusMessage.class);
|
||||
if ((propertyStatus != null) && (propertyStatus.messageType != null)
|
||||
&& (propertyStatus.messageType.equals("propertyStatus"))) {
|
||||
for (var propertyEntry : propertyStatus.data.entrySet()) {
|
||||
var listener = propertyChangedListeners.getOrDefault(propertyEntry.getKey(),
|
||||
EMPTY_PROPERTY_CHANGED_LISTENER);
|
||||
try {
|
||||
listener.accept(propertyEntry.getKey(), propertyEntry.getValue());
|
||||
} catch (RuntimeException re) {
|
||||
logger.warn("calling property change listener {} failed. {}", listener, re.getMessage());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
logger.debug("Ignoring received message of unknown type: {}", message);
|
||||
}
|
||||
}
|
||||
} catch (JsonSyntaxException se) {
|
||||
logger.warn("received invalid message: {}", message);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onWebSocketClose(int statusCode, @Nullable String reason) {
|
||||
onWebSocketError(new IOException("websocket closed by peer. " + Optional.ofNullable(reason).orElse("")));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onWebSocketError(@Nullable Throwable cause) {
|
||||
var reason = "";
|
||||
if (cause != null) {
|
||||
reason = cause.getMessage();
|
||||
}
|
||||
onError(reason);
|
||||
}
|
||||
|
||||
private void onError(@Nullable String message) {
|
||||
if (message == null) {
|
||||
message = "";
|
||||
}
|
||||
errorHandler.accept(message);
|
||||
}
|
||||
|
||||
private void sendPing() {
|
||||
var optionalSession = sessionRef.get();
|
||||
if (optionalSession.isPresent()) {
|
||||
try {
|
||||
optionalSession.get().getRemote().sendPing(ByteBuffer.wrap(Instant.now().toString().getBytes()));
|
||||
} catch (IOException e) {
|
||||
onError("could not send ping " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isAlive() {
|
||||
var elapsedSinceLastReceived = Duration.between(lastTimeReceived.get(), Instant.now());
|
||||
var thresholdOverdued = pingPeriod.multipliedBy(3);
|
||||
var isOverdued = elapsedSinceLastReceived.toMillis() > thresholdOverdued.toMillis();
|
||||
return sessionRef.get().isPresent() && !isOverdued;
|
||||
}
|
||||
|
||||
private void checkConnection() {
|
||||
// check if connection is alive (message has been received recently)
|
||||
if (!isAlive()) {
|
||||
onError("connection seems to be broken (last message received at " + lastTimeReceived.get() + ", "
|
||||
+ Duration.between(lastTimeReceived.get(), Instant.now()).getSeconds() + " sec ago)");
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,25 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2021 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.webthing.internal.client.dto;
|
||||
|
||||
/**
|
||||
* The Web Thing Description Link object. Refer https://iot.mozilla.org/wot/#link-object
|
||||
*
|
||||
* @author Gregor Roth - Initial contribution
|
||||
*/
|
||||
public class Link {
|
||||
|
||||
public String rel = null;
|
||||
|
||||
public String href = null;
|
||||
}
|
@ -0,0 +1,40 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2021 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.webthing.internal.client.dto;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
|
||||
/**
|
||||
* The Web Thing Description Property object. Refer https://iot.mozilla.org/wot/#property-object
|
||||
*
|
||||
* @author Gregor Roth - Initial contribution
|
||||
*/
|
||||
public class Property {
|
||||
|
||||
public String title = "";
|
||||
|
||||
@SerializedName("@type")
|
||||
public String typeKeyword = "";
|
||||
|
||||
public String type = "string";
|
||||
|
||||
public String unit = null;
|
||||
|
||||
public boolean readOnly = false;
|
||||
|
||||
public String description = "";
|
||||
|
||||
public List<Link> links = List.of();
|
||||
}
|
@ -0,0 +1,32 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2021 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.webthing.internal.client.dto;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Web Thing WebSocket API property status message. Refer https://iot.mozilla.org/wot/#propertystatus-message
|
||||
*
|
||||
* @author Gregor Roth - Initial contribution
|
||||
*/
|
||||
public class PropertyStatusMessage {
|
||||
|
||||
public String messageType = "<undefined>";
|
||||
|
||||
public Map<String, Object> data = Map.of();
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "PropertyStatusMessage{" + "messageType='" + messageType + '\'' + ", data=" + data + '}';
|
||||
}
|
||||
}
|
@ -0,0 +1,67 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2021 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.webthing.internal.client.dto;
|
||||
|
||||
import java.net.URI;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
|
||||
/**
|
||||
* The Web Thing Description. Refer https://iot.mozilla.org/wot/#web-thing-description
|
||||
*
|
||||
* @author Gregor Roth - Initial contribution
|
||||
*/
|
||||
public class WebThingDescription {
|
||||
|
||||
public String id = null;
|
||||
|
||||
public String title = "";
|
||||
|
||||
@SerializedName("@context")
|
||||
public String contextKeyword = "";
|
||||
|
||||
public Map<String, Property> properties = Map.of();
|
||||
|
||||
public List<Link> links = List.of();
|
||||
|
||||
/**
|
||||
* convenience method to read properties
|
||||
*
|
||||
* @param propertyName the property name to read
|
||||
* @return the property value
|
||||
*/
|
||||
public Optional<Property> getProperty(String propertyName) {
|
||||
return Optional.ofNullable(properties.get(propertyName));
|
||||
}
|
||||
|
||||
/**
|
||||
* convenience method to read the event stream uri
|
||||
*
|
||||
* @return the optional event stream uri
|
||||
*/
|
||||
public Optional<URI> getEventStreamUri() {
|
||||
for (var link : this.links) {
|
||||
var href = link.href;
|
||||
if ((href != null) && href.startsWith("ws")) {
|
||||
var rel = Optional.ofNullable(link.rel).orElse("<undefined>");
|
||||
if (rel.equals("alternate")) {
|
||||
return Optional.of(URI.create(href));
|
||||
}
|
||||
}
|
||||
}
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
@ -0,0 +1,297 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2021 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.webthing.internal.discovery;
|
||||
|
||||
import static org.openhab.binding.webthing.internal.WebThingBindingConstants.MDNS_SERVICE_TYPE;
|
||||
import static org.openhab.binding.webthing.internal.WebThingBindingConstants.THING_TYPE_UID;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.*;
|
||||
|
||||
import javax.jmdns.ServiceEvent;
|
||||
import javax.jmdns.ServiceInfo;
|
||||
import javax.jmdns.ServiceListener;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.webthing.internal.client.DescriptionLoader;
|
||||
import org.openhab.core.config.discovery.AbstractDiscoveryService;
|
||||
import org.openhab.core.config.discovery.DiscoveryResult;
|
||||
import org.openhab.core.config.discovery.DiscoveryResultBuilder;
|
||||
import org.openhab.core.config.discovery.DiscoveryService;
|
||||
import org.openhab.core.io.net.http.HttpClientFactory;
|
||||
import org.openhab.core.io.transport.mdns.MDNSClient;
|
||||
import org.openhab.core.scheduler.Scheduler;
|
||||
import org.openhab.core.thing.ThingTypeUID;
|
||||
import org.openhab.core.thing.ThingUID;
|
||||
import org.osgi.service.component.annotations.Activate;
|
||||
import org.osgi.service.component.annotations.Component;
|
||||
import org.osgi.service.component.annotations.Deactivate;
|
||||
import org.osgi.service.component.annotations.Reference;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* WebThing discovery service based on mDNS. Refer https://iot.mozilla.org/wot/#web-thing-discovery
|
||||
*
|
||||
* @author Gregor Roth - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
@Component(service = DiscoveryService.class, configurationPid = "webthingdiscovery.mdns")
|
||||
public class WebthingDiscoveryService extends AbstractDiscoveryService implements ServiceListener {
|
||||
private static final Duration FOREGROUND_SCAN_TIMEOUT = Duration.ofMillis(200);
|
||||
public static final String ID = "id";
|
||||
public static final String SCHEMAS = "schemas";
|
||||
public static final String WEB_THING_URI = "webThingURI";
|
||||
private final Logger logger = LoggerFactory.getLogger(WebthingDiscoveryService.class);
|
||||
private final DescriptionLoader descriptionLoader;
|
||||
private final MDNSClient mdnsClient;
|
||||
private final List<Future<Set<DiscoveryResult>>> runningDiscoveryTasks = new CopyOnWriteArrayList<>();
|
||||
|
||||
/**
|
||||
* constructor
|
||||
*
|
||||
* @param configProperties the config props
|
||||
* @param mdnsClient the underlying mDNS client
|
||||
*/
|
||||
@Activate
|
||||
public WebthingDiscoveryService(@Nullable Map<String, Object> configProperties, @Reference MDNSClient mdnsClient,
|
||||
@Reference Scheduler executor, @Reference HttpClientFactory httpClientFactory) {
|
||||
super(30);
|
||||
this.mdnsClient = mdnsClient;
|
||||
this.descriptionLoader = new DescriptionLoader(httpClientFactory.getCommonHttpClient());
|
||||
super.activate(configProperties);
|
||||
if (isBackgroundDiscoveryEnabled()) {
|
||||
mdnsClient.addServiceListener(MDNS_SERVICE_TYPE, this);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<ThingTypeUID> getSupportedThingTypes() {
|
||||
return Set.of(THING_TYPE_UID);
|
||||
}
|
||||
|
||||
@Deactivate
|
||||
@Override
|
||||
protected void deactivate() {
|
||||
super.deactivate();
|
||||
mdnsClient.removeServiceListener(MDNS_SERVICE_TYPE, this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void serviceAdded(@NonNullByDefault({}) ServiceEvent serviceEvent) {
|
||||
considerService(serviceEvent);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void serviceResolved(@NonNullByDefault({}) ServiceEvent serviceEvent) {
|
||||
considerService(serviceEvent);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void serviceRemoved(@NonNullByDefault({}) ServiceEvent serviceEvent) {
|
||||
for (var discoveryResult : discoverWebThing(serviceEvent.getInfo())) {
|
||||
thingRemoved(discoveryResult.getThingUID());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void startBackgroundDiscovery() {
|
||||
mdnsClient.addServiceListener(MDNS_SERVICE_TYPE, this);
|
||||
startScan(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void stopBackgroundDiscovery() {
|
||||
mdnsClient.removeServiceListener(MDNS_SERVICE_TYPE, this);
|
||||
}
|
||||
|
||||
private void startScan(boolean isBackground) {
|
||||
scheduler.submit(() -> scan(isBackground));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void startScan() {
|
||||
startScan(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected synchronized void stopScan() {
|
||||
removeOlderResults(Instant.now().minus(Duration.ofMinutes(10)).toEpochMilli());
|
||||
|
||||
// stop running discovery tasks
|
||||
for (var future : runningDiscoveryTasks) {
|
||||
future.cancel(true);
|
||||
runningDiscoveryTasks.remove(future);
|
||||
}
|
||||
super.stopScan();
|
||||
}
|
||||
|
||||
/**
|
||||
* scans the network via mDNS
|
||||
*
|
||||
* @param isBackground true, if is background task
|
||||
*/
|
||||
private void scan(boolean isBackground) {
|
||||
var serviceInfos = isBackground ? mdnsClient.list(MDNS_SERVICE_TYPE)
|
||||
: mdnsClient.list(MDNS_SERVICE_TYPE, FOREGROUND_SCAN_TIMEOUT);
|
||||
logger.debug("got {} mDNS entries", serviceInfos.length);
|
||||
|
||||
// create discovery task for each detected service and process these in parallel to increase total
|
||||
// discovery speed
|
||||
for (var serviceInfo : serviceInfos) {
|
||||
var future = scheduler.submit(new DiscoveryTask(serviceInfo));
|
||||
runningDiscoveryTasks.add(future);
|
||||
}
|
||||
|
||||
// wait until all tasks are completed
|
||||
for (var future : runningDiscoveryTasks) {
|
||||
try {
|
||||
future.get(5, TimeUnit.MINUTES);
|
||||
} catch (InterruptedException | ExecutionException | TimeoutException e) {
|
||||
logger.warn("discovering task {} terminated", future);
|
||||
}
|
||||
runningDiscoveryTasks.remove(future);
|
||||
}
|
||||
}
|
||||
|
||||
private class DiscoveryTask implements Callable<Set<DiscoveryResult>> {
|
||||
private final ServiceInfo serviceInfo;
|
||||
|
||||
DiscoveryTask(ServiceInfo serviceInfo) {
|
||||
this.serviceInfo = serviceInfo;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<DiscoveryResult> call() {
|
||||
var results = new HashSet<DiscoveryResult>();
|
||||
for (var discoveryResult : discoverWebThing(serviceInfo)) {
|
||||
results.add(discoveryResult);
|
||||
thingDiscovered(discoveryResult);
|
||||
logger.debug("WebThing '{}' (uri: {}, id: {}, schemas: {}) discovered", discoveryResult.getLabel(),
|
||||
discoveryResult.getProperties().get(WEB_THING_URI), discoveryResult.getProperties().get(ID),
|
||||
discoveryResult.getProperties().get(SCHEMAS));
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "DiscoveryTask{" + "serviceInfo=" + serviceInfo + '}';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* convert the serviceInfo result of the mDNS scan to discovery results
|
||||
*
|
||||
* @param serviceInfo the service info
|
||||
* @return the associated discovery result
|
||||
*/
|
||||
private Set<DiscoveryResult> discoverWebThing(ServiceInfo serviceInfo) {
|
||||
var discoveryResults = new HashSet<DiscoveryResult>();
|
||||
|
||||
if (serviceInfo.getHostAddresses().length > 0) {
|
||||
var host = serviceInfo.getHostAddresses()[0];
|
||||
var port = serviceInfo.getPort();
|
||||
var path = "/";
|
||||
if (Collections.list(serviceInfo.getPropertyNames()).contains("path")) {
|
||||
path = serviceInfo.getPropertyString("path");
|
||||
if (!path.endsWith("/")) {
|
||||
path = path + "/";
|
||||
}
|
||||
}
|
||||
|
||||
// There are two kinds of WebThing endpoints: Endpoints supporting a single WebThing as well as
|
||||
// endpoints supporting multiple WebThings.
|
||||
//
|
||||
// In the routine below the enpoint will be checked for single WebThings first, than for multiple
|
||||
// WebThings if a ingle WebTHing has not been found.
|
||||
// Furthermore, first it will be tried to connect the endpoint using https. If this fails, as fallback
|
||||
// plain http is used.
|
||||
|
||||
// check single WebThing path via https (e.g. https://192.168.0.23:8433/)
|
||||
var optionalDiscoveryResult = discoverWebThing(toURI(host, port, path, true));
|
||||
if (optionalDiscoveryResult.isPresent()) {
|
||||
discoveryResults.add(optionalDiscoveryResult.get());
|
||||
} else {
|
||||
// check single WebThing path via plain http (e.g. http://192.168.0.23:8433/)
|
||||
optionalDiscoveryResult = discoverWebThing(toURI(host, port, path, false));
|
||||
if (optionalDiscoveryResult.isPresent()) {
|
||||
discoveryResults.add(optionalDiscoveryResult.get());
|
||||
} else {
|
||||
// check multiple WebThing path via https (e.g. https://192.168.0.23:8433/0,
|
||||
// https://192.168.0.23:8433/1,...)
|
||||
outer: for (int i = 0; i < 50; i++) { // search 50 entries at maximum
|
||||
optionalDiscoveryResult = discoverWebThing(toURI(host, port, path + i + "/", true));
|
||||
if (optionalDiscoveryResult.isPresent()) {
|
||||
discoveryResults.add(optionalDiscoveryResult.get());
|
||||
} else if (i == 0) {
|
||||
// check multiple WebThing path via plain http (e.g. http://192.168.0.23:8433/0,
|
||||
// http://192.168.0.23:8433/1,...)
|
||||
for (int j = 0; j < 50; j++) { // search 50 entries at maximum
|
||||
optionalDiscoveryResult = discoverWebThing(toURI(host, port, path + j + "/", false));
|
||||
if (optionalDiscoveryResult.isPresent()) {
|
||||
discoveryResults.add(optionalDiscoveryResult.get());
|
||||
} else {
|
||||
break outer;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return discoveryResults;
|
||||
}
|
||||
|
||||
private Optional<DiscoveryResult> discoverWebThing(URI uri) {
|
||||
try {
|
||||
var description = descriptionLoader.loadWebthingDescription(uri, Duration.ofSeconds(5));
|
||||
|
||||
var id = uri.getHost().replaceAll("\\W", "_") + "_" + uri.getPort();
|
||||
if (uri.getPath().length() > 1) {
|
||||
id = id + "_" + uri.getPath().replaceAll("\\W", "");
|
||||
}
|
||||
|
||||
var thingUID = new ThingUID(THING_TYPE_UID, id);
|
||||
Map<String, Object> properties = new HashMap<>(2);
|
||||
properties.put(ID, id);
|
||||
properties.put(SCHEMAS, description.contextKeyword);
|
||||
return Optional.of(DiscoveryResultBuilder.create(thingUID).withThingType(THING_TYPE_UID)
|
||||
.withProperty(WEB_THING_URI, uri).withLabel(description.title).withProperties(properties)
|
||||
.withRepresentationProperty(ID).build());
|
||||
} catch (IOException ioe) {
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
|
||||
private URI toURI(String host, int port, String path, boolean isHttps) {
|
||||
return isHttps ? URI.create("https://" + host + ":" + port + path)
|
||||
: URI.create("http://" + host + ":" + port + path);
|
||||
}
|
||||
|
||||
private void considerService(ServiceEvent serviceEvent) {
|
||||
if (isBackgroundDiscoveryEnabled()) {
|
||||
for (var discoveryResult : discoverWebThing(serviceEvent.getInfo())) {
|
||||
thingDiscovered(discoveryResult);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,84 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2021 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.webthing.internal.link;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.binding.webthing.internal.ChannelHandler;
|
||||
import org.openhab.binding.webthing.internal.WebThingHandler;
|
||||
import org.openhab.binding.webthing.internal.client.ConsumedThing;
|
||||
import org.openhab.binding.webthing.internal.client.PropertyAccessException;
|
||||
import org.openhab.core.thing.Channel;
|
||||
import org.openhab.core.thing.ChannelUID;
|
||||
import org.openhab.core.types.State;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* The {@link ChannelToPropertyLink} represents an upstream link from a Channel to a WebThing property.
|
||||
* This link is used to update a the value of a property
|
||||
*
|
||||
* @author Gregor Roth - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class ChannelToPropertyLink implements WebThingHandler.ItemChangedListener {
|
||||
private final Logger logger = LoggerFactory.getLogger(ChannelToPropertyLink.class);
|
||||
private final String propertyName;
|
||||
private final String propertyType;
|
||||
private final ConsumedThing webThing;
|
||||
private final TypeConverter typeConverter;
|
||||
|
||||
/**
|
||||
* establish a upstream link from a Channel to a WebThing property
|
||||
*
|
||||
* @param channelHandler the channel handler that provides registering an ItemChangedListener
|
||||
* @param channel the channel to be linked
|
||||
* @param webthing the WebThing to be linked
|
||||
* @param propertyName the property name
|
||||
* @throws UnknownPropertyException if the a WebThing property should be link that does not exist
|
||||
*/
|
||||
public static void establish(ChannelHandler channelHandler, Channel channel, ConsumedThing webthing,
|
||||
String propertyName) throws UnknownPropertyException {
|
||||
new ChannelToPropertyLink(channelHandler, channel, webthing, propertyName);
|
||||
}
|
||||
|
||||
private ChannelToPropertyLink(ChannelHandler channelHandler, Channel channel, ConsumedThing webThing,
|
||||
String propertyName) throws UnknownPropertyException {
|
||||
this.webThing = webThing;
|
||||
var optionalProperty = webThing.getThingDescription().getProperty(propertyName);
|
||||
if (optionalProperty.isPresent()) {
|
||||
this.propertyType = optionalProperty.get().type;
|
||||
var acceptedType = channel.getAcceptedItemType();
|
||||
if (acceptedType == null) {
|
||||
this.typeConverter = TypeConverters.create("String", propertyType);
|
||||
} else {
|
||||
this.typeConverter = TypeConverters.create(acceptedType, propertyType);
|
||||
}
|
||||
this.propertyName = propertyName;
|
||||
channelHandler.observeChannel(channel.getUID(), this);
|
||||
} else {
|
||||
throw new UnknownPropertyException("property " + propertyName + " does not exits");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onItemStateChanged(ChannelUID channelUID, State stateCommand) {
|
||||
try {
|
||||
var propertyValue = typeConverter.toPropertyValue(stateCommand);
|
||||
webThing.writeProperty(propertyName, typeConverter.toPropertyValue((State) stateCommand));
|
||||
logger.debug("property {} updated with {} ({}) ", propertyName, propertyValue, this.propertyType);
|
||||
} catch (PropertyAccessException pae) {
|
||||
logger.warn("could not write WebThing property {} with new channel value. {}", propertyName,
|
||||
pae.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,76 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2021 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.webthing.internal.link;
|
||||
|
||||
import java.util.function.BiConsumer;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.binding.webthing.internal.ChannelHandler;
|
||||
import org.openhab.binding.webthing.internal.client.ConsumedThing;
|
||||
import org.openhab.core.thing.Channel;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* The {@link PropertyToChannelLink} represents a downstream link from a WebThing property to a Channel.
|
||||
*
|
||||
* @author Gregor Roth - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class PropertyToChannelLink implements BiConsumer<String, Object> {
|
||||
private final Logger logger = LoggerFactory.getLogger(PropertyToChannelLink.class);
|
||||
private final ChannelHandler channelHandler;
|
||||
private final Channel channel;
|
||||
private final TypeConverter typeConverter;
|
||||
|
||||
/**
|
||||
* establish downstream link from a WebTHing property to a Channel
|
||||
*
|
||||
* @param webThing the WebThing to be linked
|
||||
* @param propertyName the property name
|
||||
* @param channelHandler the channel handler that provides updating the Item state of a channel
|
||||
* @param channel the channel to be linked
|
||||
* @throws UnknownPropertyException if the a WebThing property should be link that does not exist
|
||||
*/
|
||||
public static void establish(ConsumedThing webThing, String propertyName, ChannelHandler channelHandler,
|
||||
Channel channel) throws UnknownPropertyException {
|
||||
new PropertyToChannelLink(webThing, propertyName, channelHandler, channel);
|
||||
}
|
||||
|
||||
private PropertyToChannelLink(ConsumedThing webThing, String propertyName, ChannelHandler channelHandler,
|
||||
Channel channel) throws UnknownPropertyException {
|
||||
this.channel = channel;
|
||||
var optionalProperty = webThing.getThingDescription().getProperty(propertyName);
|
||||
if (optionalProperty.isPresent()) {
|
||||
var propertyType = optionalProperty.get().type;
|
||||
var acceptedType = channel.getAcceptedItemType();
|
||||
if (acceptedType == null) {
|
||||
this.typeConverter = TypeConverters.create("String", propertyType);
|
||||
} else {
|
||||
this.typeConverter = TypeConverters.create(acceptedType, propertyType);
|
||||
}
|
||||
this.channelHandler = channelHandler;
|
||||
webThing.observeProperty(propertyName, this);
|
||||
} else {
|
||||
throw new UnknownPropertyException("property " + propertyName + " does not exits");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void accept(String propertyName, Object value) {
|
||||
var stateCommand = typeConverter.toStateCommand(value);
|
||||
channelHandler.updateItemState(channel.getUID(), stateCommand);
|
||||
logger.debug("channel {} updated with {} ({})", channel.getUID().getAsString(), value,
|
||||
channel.getAcceptedItemType());
|
||||
}
|
||||
}
|
@ -0,0 +1,42 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2021 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.webthing.internal.link;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.core.types.Command;
|
||||
import org.openhab.core.types.State;
|
||||
|
||||
/**
|
||||
* The {@link TypeConverter} class map Item state <-> Property value
|
||||
*
|
||||
* @author Gregor Roth - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
interface TypeConverter {
|
||||
|
||||
/**
|
||||
* * maps a Property value to an Item state command
|
||||
*
|
||||
* @param propertyValue the Property value
|
||||
* @return the Item state command
|
||||
*/
|
||||
Command toStateCommand(Object propertyValue);
|
||||
|
||||
/**
|
||||
* maps an Item state to a Property value
|
||||
*
|
||||
* @param state the Item state
|
||||
* @return the Property value
|
||||
*/
|
||||
Object toPropertyValue(State state);
|
||||
}
|
@ -0,0 +1,179 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2021 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.webthing.internal.link;
|
||||
|
||||
import java.awt.*;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Collection;
|
||||
import java.util.Locale;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.core.library.types.*;
|
||||
import org.openhab.core.types.Command;
|
||||
import org.openhab.core.types.State;
|
||||
|
||||
/**
|
||||
* Helper class to create a TypeConverter
|
||||
*
|
||||
* @author Gregor Roth - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
class TypeConverters {
|
||||
|
||||
/**
|
||||
* create a TypeConverter for a given Item type and property type
|
||||
*
|
||||
* @param itemType the item type
|
||||
* @param propertyType the property type
|
||||
* @return the type converter
|
||||
*/
|
||||
static TypeConverter create(String itemType, String propertyType) {
|
||||
switch (itemType.toLowerCase(Locale.ENGLISH)) {
|
||||
case "switch":
|
||||
return new SwitchTypeConverter();
|
||||
case "dimmer":
|
||||
return new DimmerTypeConverter();
|
||||
case "contact":
|
||||
return new ContactTypeConverter();
|
||||
case "color":
|
||||
return new ColorTypeConverter();
|
||||
case "number":
|
||||
if (propertyType.toLowerCase(Locale.ENGLISH).equals("integer")) {
|
||||
return new IntegerTypeConverter();
|
||||
} else {
|
||||
return new NumberTypeConverter();
|
||||
}
|
||||
default:
|
||||
return new StringTypeConverter();
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean toBoolean(Object propertyValue) {
|
||||
return Boolean.parseBoolean(propertyValue.toString());
|
||||
}
|
||||
|
||||
private static BigDecimal toDecimal(Object propertyValue) {
|
||||
return new BigDecimal(propertyValue.toString());
|
||||
}
|
||||
|
||||
private static final class ColorTypeConverter implements TypeConverter {
|
||||
|
||||
@Override
|
||||
public Command toStateCommand(Object propertyValue) {
|
||||
var value = propertyValue.toString();
|
||||
if (!value.contains("#")) {
|
||||
value = "#" + value;
|
||||
}
|
||||
Color rgb = Color.decode(value);
|
||||
return HSBType.fromRGB(rgb.getRed(), rgb.getGreen(), rgb.getBlue());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object toPropertyValue(State state) {
|
||||
var hsb = ((HSBType) state);
|
||||
|
||||
// Get HSB values
|
||||
Float hue = hsb.getHue().floatValue();
|
||||
Float saturation = hsb.getSaturation().floatValue();
|
||||
Float brightness = hsb.getBrightness().floatValue();
|
||||
|
||||
// Convert HSB to RGB and then to HTML hex
|
||||
Color rgb = Color.getHSBColor(hue / 360, saturation / 100, brightness / 100);
|
||||
return String.format("#%02x%02x%02x", rgb.getRed(), rgb.getGreen(), rgb.getBlue());
|
||||
}
|
||||
}
|
||||
|
||||
private static final class SwitchTypeConverter implements TypeConverter {
|
||||
|
||||
@Override
|
||||
public Command toStateCommand(Object propertyValue) {
|
||||
return toBoolean(propertyValue) ? OnOffType.ON : OnOffType.OFF;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object toPropertyValue(State state) {
|
||||
return state == OnOffType.ON;
|
||||
}
|
||||
}
|
||||
|
||||
private static final class ContactTypeConverter implements TypeConverter {
|
||||
|
||||
@Override
|
||||
public Command toStateCommand(Object propertyValue) {
|
||||
return toBoolean(propertyValue) ? OpenClosedType.OPEN : OpenClosedType.CLOSED;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object toPropertyValue(State state) {
|
||||
return state == OpenClosedType.OPEN;
|
||||
}
|
||||
}
|
||||
|
||||
private static final class DimmerTypeConverter implements TypeConverter {
|
||||
|
||||
@Override
|
||||
public Command toStateCommand(Object propertyValue) {
|
||||
return new PercentType(toDecimal(propertyValue));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object toPropertyValue(State state) {
|
||||
return ((DecimalType) state).toBigDecimal().intValue();
|
||||
}
|
||||
}
|
||||
|
||||
private static final class NumberTypeConverter implements TypeConverter {
|
||||
|
||||
@Override
|
||||
public Command toStateCommand(Object propertyValue) {
|
||||
return new DecimalType(toDecimal(propertyValue));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object toPropertyValue(State state) {
|
||||
return ((DecimalType) state).doubleValue();
|
||||
}
|
||||
}
|
||||
|
||||
private static final class IntegerTypeConverter implements TypeConverter {
|
||||
|
||||
@Override
|
||||
public Command toStateCommand(Object propertyValue) {
|
||||
return new DecimalType(toDecimal(propertyValue));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object toPropertyValue(State state) {
|
||||
return ((DecimalType) state).intValue();
|
||||
}
|
||||
}
|
||||
|
||||
private static final class StringTypeConverter implements TypeConverter {
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
public Command toStateCommand(Object propertyValue) {
|
||||
String textValue = propertyValue.toString();
|
||||
if (propertyValue instanceof Collection) {
|
||||
textValue = ((Collection<Object>) propertyValue).stream()
|
||||
.reduce("", (entry1, entry2) -> entry1.toString() + "\n" + entry2.toString()).toString();
|
||||
}
|
||||
return StringType.valueOf(textValue);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object toPropertyValue(State state) {
|
||||
return state.toString();
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,143 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2021 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.webthing.internal.link;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.webthing.internal.client.dto.Property;
|
||||
|
||||
/**
|
||||
* The {@link TypeMapping} class defines the mapping of Item types <-> WebThing Property types.
|
||||
*
|
||||
* Please consider that changes of 'Item types <-> WebThing Property types' mapping will break the
|
||||
* compatibility to former releases
|
||||
*
|
||||
* @author Gregor Roth - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class TypeMapping {
|
||||
|
||||
/**
|
||||
* maps a property type to an item type
|
||||
*
|
||||
* @param propertyMetadata the property meta data
|
||||
* @return the associated item type
|
||||
*/
|
||||
public static ItemType toItemType(Property propertyMetadata) {
|
||||
String type = "string";
|
||||
@Nullable
|
||||
String tag = null;
|
||||
|
||||
switch (propertyMetadata.typeKeyword) {
|
||||
case "AlarmProperty":
|
||||
case "BooleanProperty":
|
||||
case "LeakProperty":
|
||||
case "LockedProperty":
|
||||
case "MotionProperty":
|
||||
case "OnOffProperty":
|
||||
case "PushedProperty":
|
||||
type = "switch";
|
||||
tag = "Switchable";
|
||||
break;
|
||||
case "CurrentProperty":
|
||||
case "FrequencyProperty":
|
||||
case "InstantaneousPowerProperty":
|
||||
case "VoltageProperty":
|
||||
type = "number";
|
||||
break;
|
||||
case "HeatingCoolingProperty":
|
||||
case "ImageProperty":
|
||||
case "VideoProperty":
|
||||
type = "string";
|
||||
break;
|
||||
case "BrightnessProperty":
|
||||
case "HumidityProperty":
|
||||
type = "dimmer";
|
||||
break;
|
||||
case "ColorModeProperty":
|
||||
type = "string";
|
||||
tag = "lighting";
|
||||
break;
|
||||
case "ColorProperty":
|
||||
type = "color";
|
||||
tag = "Lighting";
|
||||
break;
|
||||
case "ColorTemperatureProperty":
|
||||
type = "dimmer";
|
||||
tag = "Lighting";
|
||||
break;
|
||||
case "OpenProperty":
|
||||
type = "contact";
|
||||
tag = "ContactSensor";
|
||||
break;
|
||||
case "TargetTemperatureProperty":
|
||||
type = "number";
|
||||
tag = "TargetTemperature";
|
||||
break;
|
||||
case "TemperatureProperty":
|
||||
type = "number";
|
||||
tag = "CurrentTemperature";
|
||||
break;
|
||||
case "ThermostatModeProperty":
|
||||
break;
|
||||
case "LevelProperty":
|
||||
if ((propertyMetadata.unit != null)
|
||||
&& propertyMetadata.unit.toLowerCase(Locale.ENGLISH).equals("percent")) {
|
||||
type = "dimmer";
|
||||
} else {
|
||||
type = "number";
|
||||
}
|
||||
break;
|
||||
default:
|
||||
switch (propertyMetadata.type.toLowerCase(Locale.ENGLISH)) {
|
||||
case "boolean":
|
||||
type = "switch";
|
||||
tag = "Switchable";
|
||||
break;
|
||||
case "integer":
|
||||
case "number":
|
||||
type = "number";
|
||||
break;
|
||||
default:
|
||||
type = "string";
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
return new ItemType(type, tag);
|
||||
}
|
||||
|
||||
/**
|
||||
* The item type description
|
||||
*/
|
||||
public static class ItemType {
|
||||
private final String type;
|
||||
private final @Nullable String tag;
|
||||
|
||||
ItemType(String type, @Nullable String tag) {
|
||||
this.type = type;
|
||||
this.tag = tag;
|
||||
}
|
||||
|
||||
public String getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public @Nullable String getTag() {
|
||||
return tag;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,34 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2021 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.webthing.internal.link;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
/**
|
||||
* The {@link UnknownPropertyException} indicates addressing a WebThing property that does not exist
|
||||
*
|
||||
* @author Gregor Roth - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class UnknownPropertyException extends Exception {
|
||||
private static final long serialVersionUID = -5302763943749264616L;
|
||||
|
||||
/**
|
||||
* contructor
|
||||
*
|
||||
* @param message the error message
|
||||
*/
|
||||
UnknownPropertyException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<binding:binding id="webthing" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:binding="https://openhab.org/schemas/binding/v1.0.0"
|
||||
xsi:schemaLocation="https://openhab.org/schemas/binding/v1.0.0 https://openhab.org/schemas/binding-1.0.0.xsd">
|
||||
|
||||
<name>WebThing Binding</name>
|
||||
<description>The WebThing binding supports an interface to remote devices implementing the Web Thing API.</description>
|
||||
</binding:binding>
|
@ -0,0 +1,60 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<thing:thing-descriptions bindingId="webthing"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
|
||||
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
|
||||
|
||||
<thing-type id="generic">
|
||||
<label>WebThing</label>
|
||||
<description>The WebThing to be connected</description>
|
||||
|
||||
<config-description>
|
||||
<parameter name="webThingURI" type="text" required="true">
|
||||
<context>url</context>
|
||||
<label>URI</label>
|
||||
<description>The URI of the WebThing to be connected. E.g. the URI of a web-connected MotionSensor or a URI of a
|
||||
web-connected Display</description>
|
||||
</parameter>
|
||||
</config-description>
|
||||
|
||||
</thing-type>
|
||||
|
||||
|
||||
<channel-type id="number">
|
||||
<item-type>Number</item-type>
|
||||
<label>Webthing Binding Channel</label>
|
||||
<description>Number channel for Webthing Binding</description>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="string">
|
||||
<item-type>String</item-type>
|
||||
<label>Webthing Binding Channel</label>
|
||||
<description>String channel for Webthing Binding</description>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="contact">
|
||||
<item-type>Contact</item-type>
|
||||
<label>Webthing Binding Channel</label>
|
||||
<description>Contact channel for Webthing Binding</description>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="switch">
|
||||
<item-type>Switch</item-type>
|
||||
<label>Webthing Binding Channel</label>
|
||||
<description>Switch channel for Webthing Binding</description>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="color">
|
||||
<item-type>Color</item-type>
|
||||
<label>Webthing Binding Channel</label>
|
||||
<description>Color channel for Webthing Binding</description>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="dimmer">
|
||||
<item-type>Dimmer</item-type>
|
||||
<label>Webthing Binding Channel</label>
|
||||
<description>Dimmer channel for Webthing Binding</description>
|
||||
</channel-type>
|
||||
|
||||
|
||||
</thing:thing-descriptions>
|
@ -0,0 +1,60 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2021 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.webthing.internal.client;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import java.net.URI;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Paths;
|
||||
import java.time.Duration;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
/**
|
||||
*
|
||||
*
|
||||
* @author Gregor Roth - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class DescriptionTest {
|
||||
|
||||
@Test
|
||||
public void testDescriptionEventStreamUri() throws Exception {
|
||||
var httpClient = mock(org.eclipse.jetty.client.HttpClient.class);
|
||||
var request = Mocks.mockRequest(null, load("/awning_response.json"));
|
||||
when(httpClient.newRequest(URI.create("http://example.org:8090"))).thenReturn(request);
|
||||
|
||||
var loader = new DescriptionLoader(httpClient);
|
||||
var description = loader.loadWebthingDescription(URI.create("http://example.org:8090"), Duration.ofSeconds(2));
|
||||
assertEquals("ws://192.168.4.12:9040/0", description.getEventStreamUri().get().toString());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDescriptionEventStreamUriServerlaAlternateParts() throws Exception {
|
||||
var httpClient = mock(org.eclipse.jetty.client.HttpClient.class);
|
||||
var request = Mocks.mockRequest(null, load("/virtual-things_response.json"));
|
||||
when(httpClient.newRequest(URI.create("http://example.org:8090"))).thenReturn(request);
|
||||
|
||||
var loader = new DescriptionLoader(httpClient);
|
||||
var description = loader.loadWebthingDescription(URI.create("http://example.org:8090"), Duration.ofSeconds(2));
|
||||
assertEquals("ws://webthings/things/virtual-things-7", description.getEventStreamUri().get().toString());
|
||||
}
|
||||
|
||||
public static String load(String name) throws Exception {
|
||||
return new String(Files.readAllBytes(Paths.get(WebthingTest.class.getResource(name).toURI())));
|
||||
}
|
||||
}
|
@ -0,0 +1,78 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2021 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.webthing.internal.client;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.argThat;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.concurrent.*;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.eclipse.jetty.client.api.ContentProvider;
|
||||
import org.eclipse.jetty.client.api.ContentResponse;
|
||||
import org.eclipse.jetty.client.api.Request;
|
||||
|
||||
/**
|
||||
* Mock helper
|
||||
*
|
||||
* @author Gregor Roth - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class Mocks {
|
||||
|
||||
public static Request mockRequest(@Nullable String requestContent, String responseContent) throws Exception {
|
||||
return mockRequest(requestContent, responseContent, 200, 200);
|
||||
}
|
||||
|
||||
public static Request mockRequest(@Nullable String requestContent, String responseContent, int getResponse,
|
||||
int postResponse) throws Exception {
|
||||
var request = mock(Request.class);
|
||||
|
||||
// GET request -> request.timeout(30, TimeUnit.SECONDS).send();
|
||||
var getRequest = mock(Request.class);
|
||||
var getContentResponse = mock(ContentResponse.class);
|
||||
when(getContentResponse.getStatus()).thenReturn(getResponse);
|
||||
when(getContentResponse.getContentAsString()).thenReturn(responseContent);
|
||||
when(getRequest.send()).thenReturn(getContentResponse);
|
||||
when(getRequest.accept("application/json")).thenReturn(getRequest);
|
||||
when(request.timeout(30, TimeUnit.SECONDS)).thenReturn(getRequest);
|
||||
|
||||
// POST request -> request.method("PUT").content(new StringContentProvider(json)).timeout(30,
|
||||
// TimeUnit.SECONDS).send();
|
||||
if (requestContent != null) {
|
||||
var postRequest = mock(Request.class);
|
||||
when(postRequest.content(argThat((ContentProvider content) -> bufToString(content).equals(requestContent)),
|
||||
eq("application/json"))).thenReturn(postRequest);
|
||||
when(postRequest.timeout(30, TimeUnit.SECONDS)).thenReturn(postRequest);
|
||||
|
||||
var postContentResponse = mock(ContentResponse.class);
|
||||
when(postContentResponse.getStatus()).thenReturn(postResponse);
|
||||
when(postRequest.send()).thenReturn(postContentResponse);
|
||||
when(request.method("PUT")).thenReturn(postRequest);
|
||||
}
|
||||
return request;
|
||||
}
|
||||
|
||||
private static String bufToString(Iterable<ByteBuffer> data) {
|
||||
var result = "";
|
||||
for (var byteBuffer : data) {
|
||||
result += StandardCharsets.UTF_8.decode(byteBuffer).toString();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
@ -0,0 +1,501 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2021 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.webthing.internal.client;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.URI;
|
||||
import java.net.http.WebSocket;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Paths;
|
||||
import java.time.Duration;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.function.BiConsumer;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNull;
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.eclipse.jetty.client.HttpClient;
|
||||
import org.eclipse.jetty.websocket.api.BatchMode;
|
||||
import org.eclipse.jetty.websocket.api.CloseStatus;
|
||||
import org.eclipse.jetty.websocket.api.RemoteEndpoint;
|
||||
import org.eclipse.jetty.websocket.api.Session;
|
||||
import org.eclipse.jetty.websocket.api.SuspendToken;
|
||||
import org.eclipse.jetty.websocket.api.UpgradeRequest;
|
||||
import org.eclipse.jetty.websocket.api.UpgradeResponse;
|
||||
import org.eclipse.jetty.websocket.api.WebSocketListener;
|
||||
import org.eclipse.jetty.websocket.api.WebSocketPingPongListener;
|
||||
import org.eclipse.jetty.websocket.api.WebSocketPolicy;
|
||||
import org.eclipse.jetty.websocket.api.WriteCallback;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.openhab.binding.webthing.internal.client.dto.PropertyStatusMessage;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
|
||||
/**
|
||||
*
|
||||
*
|
||||
* @author Gregor Roth - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class WebthingTest {
|
||||
private static final Gson GSON = new Gson();
|
||||
|
||||
@Test
|
||||
public void testWebthingDescription() throws Exception {
|
||||
var httpClient = mock(org.eclipse.jetty.client.HttpClient.class);
|
||||
var request = Mocks.mockRequest(null, load("/windsensor_response.json"));
|
||||
when(httpClient.newRequest(URI.create("http://example.org:8090"))).thenReturn(request);
|
||||
|
||||
var request2 = Mocks.mockRequest(null, load("/windsensor_property.json"));
|
||||
when(httpClient.newRequest(URI.create("http://example.org:8090/properties/windspeed"))).thenReturn(request2);
|
||||
|
||||
var webthing = createTestWebthing("http://example.org:8090", httpClient);
|
||||
var metadata = webthing.getThingDescription();
|
||||
assertEquals("Wind", metadata.title);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testWebthingDescriptionUnsetSchema() throws Exception {
|
||||
var httpClient = mock(org.eclipse.jetty.client.HttpClient.class);
|
||||
var request = Mocks.mockRequest(null, load("/unsetschema_response.json"));
|
||||
when(httpClient.newRequest(URI.create("http://example.org:8090"))).thenReturn(request);
|
||||
|
||||
var request2 = Mocks.mockRequest(null, load("/windsensor_property.json"));
|
||||
when(httpClient.newRequest(URI.create("http://example.org:8090/properties/windspeed"))).thenReturn(request2);
|
||||
|
||||
var webthing = createTestWebthing("http://example.org:8090", httpClient);
|
||||
var metadata = webthing.getThingDescription();
|
||||
assertEquals("Wind", metadata.title);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testWebthingDescriptionUNsupportedSchema() throws Exception {
|
||||
var httpClient = mock(org.eclipse.jetty.client.HttpClient.class);
|
||||
var request = Mocks.mockRequest(null, load("/unknownschema_response.json"));
|
||||
when(httpClient.newRequest(URI.create("http://example.org:8090"))).thenReturn(request);
|
||||
|
||||
try {
|
||||
createTestWebthing("http://example.org:8090", httpClient);
|
||||
fail();
|
||||
} catch (IOException e) {
|
||||
assertEquals(
|
||||
"unsupported schema (@context parameter) https://www.w3.org/2019/wot/td/v1 (Supported schemas are https://webthings.io/schemas and https://iot.mozilla.org/schemas)",
|
||||
e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testReadReadOnlyProperty() throws Exception {
|
||||
var httpClient = mock(org.eclipse.jetty.client.HttpClient.class);
|
||||
var request = Mocks.mockRequest(null, load("/windsensor_response.json"));
|
||||
when(httpClient.newRequest(URI.create("http://example.org:8090"))).thenReturn(request);
|
||||
|
||||
var request2 = Mocks.mockRequest(null, load("/windsensor_property.json"));
|
||||
when(httpClient.newRequest(URI.create("http://example.org:8090/properties/windspeed"))).thenReturn(request2);
|
||||
|
||||
var webthing = createTestWebthing("http://example.org:8090", httpClient);
|
||||
|
||||
assertEquals(34.0, webthing.readProperty("windspeed"));
|
||||
try {
|
||||
webthing.writeProperty("windspeed", 23.0);
|
||||
fail();
|
||||
} catch (PropertyAccessException e) {
|
||||
assertEquals(
|
||||
"could not write windspeed (http://example.org:8090/properties/windspeed) with 23.0. Property is readOnly",
|
||||
e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testReadPropertyTest() throws Exception {
|
||||
var httpClient = mock(org.eclipse.jetty.client.HttpClient.class);
|
||||
var request = Mocks.mockRequest(null, load("/awning_response.json"));
|
||||
when(httpClient.newRequest(URI.create("http://example.org:8090/0"))).thenReturn(request);
|
||||
|
||||
var request2 = Mocks.mockRequest(null, load("/awning_property.json"));
|
||||
when(httpClient.newRequest(URI.create("http://example.org:8090/0/properties/target_position")))
|
||||
.thenReturn(request2);
|
||||
|
||||
var webthing = createTestWebthing("http://example.org:8090/0", httpClient);
|
||||
|
||||
assertEquals(85.0, webthing.readProperty("target_position"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testWriteProperty() throws Exception {
|
||||
var httpClient = mock(org.eclipse.jetty.client.HttpClient.class);
|
||||
var request = Mocks.mockRequest(null, load("/awning_response.json"));
|
||||
when(httpClient.newRequest(URI.create("http://example.org:8090/0"))).thenReturn(request);
|
||||
|
||||
var request2 = Mocks.mockRequest("{\"target_position\":10}", load("/awning_property.json"));
|
||||
when(httpClient.newRequest(URI.create("http://example.org:8090/0/properties/target_position")))
|
||||
.thenReturn(request2);
|
||||
|
||||
var webthing = createTestWebthing("http://example.org:8090/0", httpClient);
|
||||
webthing.writeProperty("target_position", 10);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testWritePropertyError() throws Exception {
|
||||
var httpClient = mock(org.eclipse.jetty.client.HttpClient.class);
|
||||
var request = Mocks.mockRequest(null, load("/awning_response.json"));
|
||||
when(httpClient.newRequest(URI.create("http://example.org:8090/0"))).thenReturn(request);
|
||||
|
||||
var request2 = Mocks.mockRequest("{\"target_position\":10}", load("/awning_property.json"), 200, 400);
|
||||
when(httpClient.newRequest(URI.create("http://example.org:8090/0/properties/target_position")))
|
||||
.thenReturn(request2);
|
||||
|
||||
var webthing = createTestWebthing("http://example.org:8090/0", httpClient);
|
||||
try {
|
||||
webthing.writeProperty("target_position", 10);
|
||||
fail();
|
||||
} catch (PropertyAccessException e) {
|
||||
assertEquals(
|
||||
"could not write target_position (http://example.org:8090/0/properties/target_position) with 10",
|
||||
e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testReadPropertyError() throws Exception {
|
||||
var httpClient = mock(org.eclipse.jetty.client.HttpClient.class);
|
||||
var request = Mocks.mockRequest(null, load("/windsensor_response.json"));
|
||||
when(httpClient.newRequest(URI.create("http://example.org:8090"))).thenReturn(request);
|
||||
|
||||
var request2 = Mocks.mockRequest(null, load("/windsensor_response.json"), 500, 200);
|
||||
when(httpClient.newRequest(URI.create("http://example.org:8090/properties/windspeed"))).thenReturn(request2);
|
||||
|
||||
var webthing = createTestWebthing("http://example.org:8090", httpClient);
|
||||
try {
|
||||
webthing.readProperty("windspeed");
|
||||
fail();
|
||||
} catch (PropertyAccessException e) {
|
||||
assertEquals("could not read windspeed (http://example.org:8090/properties/windspeed)", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testWebSocket() throws Exception {
|
||||
var httpClient = mock(org.eclipse.jetty.client.HttpClient.class);
|
||||
var request = Mocks.mockRequest(null, load("/awning_response.json"));
|
||||
when(httpClient.newRequest(URI.create("http://example.org:8090/0"))).thenReturn(request);
|
||||
|
||||
var request2 = Mocks.mockRequest(null, load("/awning_property.json"));
|
||||
when(httpClient.newRequest(URI.create("http://example.org:8090/0/properties/target_position")))
|
||||
.thenReturn(request2);
|
||||
|
||||
var errorHandler = new ErrorHandler();
|
||||
var webSocketFactory = new TestWebsocketConnectionFactory();
|
||||
var webthing = createTestWebthing("http://example.org:8090/0", httpClient, errorHandler, webSocketFactory);
|
||||
|
||||
var propertyChangedListenerImpl = new PropertyChangedListenerImpl();
|
||||
webthing.observeProperty("target_position", propertyChangedListenerImpl);
|
||||
|
||||
var webSocketServerSide = webSocketFactory.webSocketRef.get();
|
||||
var message = new PropertyStatusMessage();
|
||||
message.messageType = "propertyStatus";
|
||||
message.data = Map.of("target_position", 33);
|
||||
webSocketServerSide.sendToClient(message);
|
||||
|
||||
while (propertyChangedListenerImpl.valueRef.get() == null) {
|
||||
try {
|
||||
Thread.sleep(100);
|
||||
} catch (InterruptedException ignore) {
|
||||
}
|
||||
}
|
||||
assertEquals(33.0, propertyChangedListenerImpl.valueRef.get());
|
||||
|
||||
webSocketServerSide.sendCloseToClient();
|
||||
assertEquals("websocket closed by peer. ", errorHandler.errorRef.get());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testWebSocketReceiveTimout() throws Exception {
|
||||
var httpClient = mock(org.eclipse.jetty.client.HttpClient.class);
|
||||
var request = Mocks.mockRequest(null, load("/awning_response.json"));
|
||||
when(httpClient.newRequest(URI.create("http://example.org:8090/0"))).thenReturn(request);
|
||||
|
||||
var request2 = Mocks.mockRequest(null, load("/awning_property.json"));
|
||||
when(httpClient.newRequest(URI.create("http://example.org:8090/0/properties/target_position")))
|
||||
.thenReturn(request2);
|
||||
|
||||
var errorHandler = new ErrorHandler();
|
||||
var webSocketFactory = new TestWebsocketConnectionFactory();
|
||||
var pingPeriod = Duration.ofMillis(300);
|
||||
var webthing = createTestWebthing("http://example.org:8090/0", httpClient, errorHandler, webSocketFactory,
|
||||
pingPeriod);
|
||||
|
||||
var propertyChangedListenerImpl = new PropertyChangedListenerImpl();
|
||||
webthing.observeProperty("target_position", propertyChangedListenerImpl);
|
||||
webSocketFactory.webSocketRef.get().ignorePing.set(true);
|
||||
|
||||
try {
|
||||
Thread.sleep(pingPeriod.dividedBy(2).toMillis());
|
||||
} catch (InterruptedException ignore) {
|
||||
}
|
||||
assertNull(errorHandler.errorRef.get());
|
||||
|
||||
try {
|
||||
Thread.sleep(pingPeriod.multipliedBy(4).toMillis());
|
||||
} catch (InterruptedException ignore) {
|
||||
}
|
||||
assertTrue(errorHandler.errorRef.get().startsWith("connection seems to be broken (last message received at"));
|
||||
}
|
||||
|
||||
public static String load(String name) throws Exception {
|
||||
return new String(Files.readAllBytes(Paths.get(WebthingTest.class.getResource(name).toURI())));
|
||||
}
|
||||
|
||||
public static ConsumedThingImpl createTestWebthing(String uri, HttpClient httpClient) throws IOException {
|
||||
return createTestWebthing(uri, httpClient, (String) -> {
|
||||
}, new TestWebsocketConnectionFactory());
|
||||
}
|
||||
|
||||
public static ConsumedThingImpl createTestWebthing(String uri, HttpClient httpClient, Consumer<String> errorHandler,
|
||||
WebSocketConnectionFactory websocketConnectionFactory, Duration pingPeriod) throws IOException {
|
||||
return new ConsumedThingImpl(httpClient, URI.create(uri), Executors.newSingleThreadScheduledExecutor(),
|
||||
errorHandler, websocketConnectionFactory, pingPeriod);
|
||||
}
|
||||
|
||||
public static ConsumedThingImpl createTestWebthing(String uri, HttpClient httpClient, Consumer<String> errorHandler,
|
||||
WebSocketConnectionFactory websocketConnectionFactory) throws IOException {
|
||||
return createTestWebthing(uri, httpClient, errorHandler, websocketConnectionFactory, Duration.ofSeconds(100));
|
||||
}
|
||||
|
||||
public static class TestWebsocketConnectionFactory implements WebSocketConnectionFactory {
|
||||
public final AtomicReference<WebSocketImpl> webSocketRef = new AtomicReference<>();
|
||||
|
||||
@Override
|
||||
public WebSocketConnection create(@NonNull URI webSocketURI, @NonNull ScheduledExecutorService executor,
|
||||
@NonNull Consumer<String> errorHandler, @NonNull Duration pingPeriod) {
|
||||
var webSocketConnection = new WebSocketConnectionImpl(executor, errorHandler, pingPeriod);
|
||||
var webSocket = new WebSocketImpl(webSocketConnection);
|
||||
webSocketRef.set(webSocket);
|
||||
webSocketConnection.onWebSocketConnect(webSocket);
|
||||
return webSocketConnection;
|
||||
}
|
||||
}
|
||||
|
||||
public static final class WebSocketImpl implements Session {
|
||||
private final WebSocketListener listener;
|
||||
private final WebSocketPingPongListener pongListener;
|
||||
public AtomicBoolean ignorePing = new AtomicBoolean(false);
|
||||
|
||||
WebSocketImpl(WebSocketConnectionImpl connection) {
|
||||
this.listener = connection;
|
||||
this.pongListener = connection;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close(@Nullable CloseStatus closeStatus) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close(int statusCode, @Nullable String reason) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void disconnect() throws IOException {
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getIdleTimeout() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public InetSocketAddress getLocalAddress() {
|
||||
return InetSocketAddress.createUnresolved("test", 23);
|
||||
}
|
||||
|
||||
@Override
|
||||
public WebSocketPolicy getPolicy() {
|
||||
return WebSocketPolicy.newClientPolicy();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getProtocolVersion() {
|
||||
return "1";
|
||||
}
|
||||
|
||||
@Override
|
||||
public RemoteEndpoint getRemote() {
|
||||
return new RemoteEndpoint() {
|
||||
@Override
|
||||
public void sendBytes(@Nullable ByteBuffer data) throws IOException {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Future sendBytesByFuture(@Nullable ByteBuffer data) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendBytes(@Nullable ByteBuffer data, @Nullable WriteCallback callback) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendPartialBytes(@Nullable ByteBuffer fragment, boolean isLast) throws IOException {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendPartialString(@Nullable String fragment, boolean isLast) throws IOException {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendPing(@Nullable ByteBuffer applicationData) throws IOException {
|
||||
if (!ignorePing.get()) {
|
||||
pongListener.onWebSocketPong(applicationData);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendPong(@Nullable ByteBuffer applicationData) throws IOException {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendString(@Nullable String text) throws IOException {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Future sendStringByFuture(@Nullable String text) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendString(@Nullable String text, @Nullable WriteCallback callback) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public BatchMode getBatchMode() {
|
||||
return BatchMode.AUTO;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setBatchMode(@Nullable BatchMode mode) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public InetSocketAddress getInetSocketAddress() {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void flush() throws IOException {
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getMaxOutgoingFrames() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setMaxOutgoingFrames(int maxOutgoingFrames) {
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public InetSocketAddress getRemoteAddress() {
|
||||
return InetSocketAddress.createUnresolved("test", 12);
|
||||
}
|
||||
|
||||
@Override
|
||||
public UpgradeRequest getUpgradeRequest() {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public UpgradeResponse getUpgradeResponse() {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isOpen() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isSecure() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setIdleTimeout(long ms) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public SuspendToken suspend() {
|
||||
return new SuspendToken() {
|
||||
|
||||
@Override
|
||||
public void resume() {
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public void sendToClient(PropertyStatusMessage message) {
|
||||
var data = GSON.toJson(message);
|
||||
listener.onWebSocketText(data);
|
||||
}
|
||||
|
||||
public void sendCloseToClient() {
|
||||
listener.onWebSocketClose(200, "");
|
||||
}
|
||||
|
||||
public CompletableFuture<WebSocket> sendPing(String message) {
|
||||
if (!ignorePing.get()) {
|
||||
var bytes = message.getBytes(StandardCharsets.UTF_8);
|
||||
listener.onWebSocketBinary(bytes, 0, bytes.length);
|
||||
}
|
||||
return CompletableFuture.completedFuture(null);
|
||||
}
|
||||
}
|
||||
|
||||
private static final class PropertyChangedListenerImpl implements BiConsumer<String, Object> {
|
||||
public final AtomicReference<String> propertyNameRef = new AtomicReference<>();
|
||||
public final AtomicReference<Object> valueRef = new AtomicReference<>();
|
||||
|
||||
@Override
|
||||
public void accept(String propertyName, Object value) {
|
||||
propertyNameRef.set(propertyName);
|
||||
valueRef.set(value);
|
||||
}
|
||||
}
|
||||
|
||||
public static class ErrorHandler implements Consumer<String> {
|
||||
public final AtomicReference<String> errorRef = new AtomicReference<>();
|
||||
|
||||
@Override
|
||||
public void accept(String error) {
|
||||
errorRef.set(error);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,58 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2021 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.webthing.internal.link;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.junit.jupiter.api.Assumptions.*;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.openhab.core.library.types.DecimalType;
|
||||
import org.openhab.core.library.types.StringType;
|
||||
import org.openhab.core.types.State;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author Gregor Roth - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class TypeConverterTest {
|
||||
|
||||
@Test
|
||||
public void testStringType() throws Exception {
|
||||
var typeConverter = TypeConverters.create("String", "String");
|
||||
var state = typeConverter.toStateCommand("motion");
|
||||
assumeTrue(state instanceof StringType);
|
||||
assertEquals("motion", typeConverter.toPropertyValue((State) state));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNumberType() throws Exception {
|
||||
var typeConverter = TypeConverters.create("Number", "Number");
|
||||
var state = typeConverter.toStateCommand(45.6);
|
||||
assumeTrue(state instanceof DecimalType);
|
||||
assertEquals(45.6, typeConverter.toPropertyValue((State) state));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNumberIntegerType() throws Exception {
|
||||
var typeConverter = TypeConverters.create("Number", "Integer");
|
||||
var state = typeConverter.toStateCommand(45);
|
||||
assumeTrue(state instanceof DecimalType);
|
||||
assertEquals(45, typeConverter.toPropertyValue((State) state));
|
||||
|
||||
state = typeConverter.toStateCommand(45.2);
|
||||
assumeTrue(state instanceof DecimalType);
|
||||
assertEquals(45, typeConverter.toPropertyValue((State) state));
|
||||
}
|
||||
}
|
@ -0,0 +1,206 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2021 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.webthing.internal.link;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
import java.net.URI;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.openhab.binding.webthing.internal.ChannelHandler;
|
||||
import org.openhab.binding.webthing.internal.channel.Channels;
|
||||
import org.openhab.binding.webthing.internal.client.Mocks;
|
||||
import org.openhab.binding.webthing.internal.client.WebthingTest;
|
||||
import org.openhab.binding.webthing.internal.client.dto.PropertyStatusMessage;
|
||||
import org.openhab.core.library.types.*;
|
||||
import org.openhab.core.thing.*;
|
||||
import org.openhab.core.types.Command;
|
||||
import org.openhab.core.types.State;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
|
||||
/**
|
||||
* Mapping test.
|
||||
*
|
||||
* Please consider that changes of 'ItemType<->PropertyType mapping' validated by this test
|
||||
* will break the compatibility to former releases.
|
||||
*
|
||||
* @author Gregor Roth - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class WebthingChannelLinkTest {
|
||||
private final Gson gson = new Gson();
|
||||
|
||||
@Test
|
||||
public void testChannelToProperty() throws Exception {
|
||||
var httpClient = mock(org.eclipse.jetty.client.HttpClient.class);
|
||||
var request = Mocks.mockRequest(null, load("/awning_response.json"));
|
||||
when(httpClient.newRequest(URI.create("http://example.org:8090/0"))).thenReturn(request);
|
||||
|
||||
var request2 = Mocks.mockRequest("{\"target_position\":10}", load("/awning_property.json"));
|
||||
when(httpClient.newRequest(URI.create("http://example.org:8090/0/properties/target_position")))
|
||||
.thenReturn(request2);
|
||||
|
||||
var thingUID = new ThingUID("webthing", "anwing");
|
||||
var channelUID = Channels.createChannelUID(thingUID, "target_position");
|
||||
|
||||
var webthing = WebthingTest.createTestWebthing("http://example.org:8090/0", httpClient);
|
||||
var channel = Channels.createChannel(thingUID, "target_position",
|
||||
Objects.requireNonNull(webthing.getPropertyDescription("target_position")));
|
||||
|
||||
var testWebthingThingHandler = new TestWebthingThingHandler();
|
||||
ChannelToPropertyLink.establish(testWebthingThingHandler, channel, webthing, "target_position");
|
||||
|
||||
testWebthingThingHandler.listeners.get(channelUID).onItemStateChanged(channelUID, new DecimalType(10));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testChannelToPropertyServerError() throws Exception {
|
||||
var httpClient = mock(org.eclipse.jetty.client.HttpClient.class);
|
||||
var request = Mocks.mockRequest(null, load("/awning_response.json"));
|
||||
when(httpClient.newRequest(URI.create("http://example.org:8090/0"))).thenReturn(request);
|
||||
|
||||
var request2 = Mocks.mockRequest("{\"target_position\":130}", load("/awning_property.json"), 200, 500);
|
||||
when(httpClient.newRequest(URI.create("http://example.org:8090/0/properties/target_position")))
|
||||
.thenReturn(request2);
|
||||
|
||||
var thingUID = new ThingUID("webthing", "anwing");
|
||||
var channelUID = Channels.createChannelUID(thingUID, "target_position");
|
||||
|
||||
var webthing = WebthingTest.createTestWebthing("http://example.org:8090/0", httpClient);
|
||||
var channel = Channels.createChannel(thingUID, "target_position",
|
||||
Objects.requireNonNull(webthing.getPropertyDescription("target_position")));
|
||||
|
||||
var testWebthingThingHandler = new TestWebthingThingHandler();
|
||||
ChannelToPropertyLink.establish(testWebthingThingHandler, channel, webthing, "target_position");
|
||||
|
||||
testWebthingThingHandler.listeners.get(channelUID).onItemStateChanged(channelUID, new DecimalType(130));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPropertyToChannel() throws Exception {
|
||||
var httpClient = mock(org.eclipse.jetty.client.HttpClient.class);
|
||||
var request = Mocks.mockRequest(null, load("/awning_response.json"));
|
||||
when(httpClient.newRequest(URI.create("http://example.org:8090/0"))).thenReturn(request);
|
||||
|
||||
var request2 = Mocks.mockRequest("{\"target_position\":10}", load("/awning_property.json"));
|
||||
when(httpClient.newRequest(URI.create("http://example.org:8090/0/properties/target_position")))
|
||||
.thenReturn(request2);
|
||||
|
||||
var thingUID = new ThingUID("webthing", "anwing");
|
||||
var channelUID = Channels.createChannelUID(thingUID, "target_position");
|
||||
|
||||
var errorHandler = new WebthingTest.ErrorHandler();
|
||||
var websocketConnectionFactory = new WebthingTest.TestWebsocketConnectionFactory();
|
||||
var webthing = WebthingTest.createTestWebthing("http://example.org:8090/0", httpClient, errorHandler,
|
||||
websocketConnectionFactory);
|
||||
var channel = Channels.createChannel(thingUID, "target_position",
|
||||
Objects.requireNonNull(webthing.getPropertyDescription("target_position")));
|
||||
|
||||
var testWebthingThingHandler = new TestWebthingThingHandler();
|
||||
PropertyToChannelLink.establish(webthing, "target_position", testWebthingThingHandler, channel);
|
||||
|
||||
var message = new PropertyStatusMessage();
|
||||
message.messageType = "propertyStatus";
|
||||
message.data = Map.of("target_position", 77);
|
||||
websocketConnectionFactory.webSocketRef.get().sendToClient(message);
|
||||
|
||||
assertEquals(new DecimalType(77), testWebthingThingHandler.itemState.get(channelUID));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDataTypeMapping() throws Exception {
|
||||
performDataTypeMappingTest("level_prop", 56.5, new DecimalType(56.5), 3.5, new DecimalType(3.5));
|
||||
performDataTypeMappingTest("level_unit_prop", 10, new PercentType(10), 90, new PercentType(90));
|
||||
performDataTypeMappingTest("thermo_prop", "off", new StringType("off"), "auto", new StringType("auto"));
|
||||
performDataTypeMappingTest("temp_prop", 18.6, new DecimalType(18.6), 23.2, new DecimalType(23.2));
|
||||
performDataTypeMappingTest("targettemp_prop", 18.6, new DecimalType(18.6), 23.2, new DecimalType(23.2));
|
||||
performDataTypeMappingTest("open_prop", true, OpenClosedType.OPEN, false, OpenClosedType.CLOSED);
|
||||
performDataTypeMappingTest("colortemp_prop", 10, new PercentType(10), 60, new PercentType(60));
|
||||
performDataTypeMappingTest("color_prop", "#f2fe00", new HSBType("62,100,99"), "#ff0000",
|
||||
new HSBType("0.0,100.0,100.0"));
|
||||
performDataTypeMappingTest("colormode_prop", "color", new StringType("color"), "temperature",
|
||||
new StringType("temperature"));
|
||||
performDataTypeMappingTest("brightness_prop", 33, new PercentType(33), 65, new PercentType(65));
|
||||
performDataTypeMappingTest("voltage_prop", 4.5, new DecimalType(4.5), 30.2, new DecimalType(30.2));
|
||||
performDataTypeMappingTest("heating_prop", "off", new StringType("off"), "cooling", new StringType("cooling"));
|
||||
performDataTypeMappingTest("onoff_prop", true, OnOffType.ON, false, OnOffType.OFF);
|
||||
performDataTypeMappingTest("string_prop", "initial", new StringType("initial"), "updated",
|
||||
new StringType("updated"));
|
||||
performDataTypeMappingTest("number_prop", 80.5, new DecimalType(80.5), 60.9, new DecimalType(60.9));
|
||||
performDataTypeMappingTest("integer_prop", 11, new DecimalType(11), 77, new DecimalType(77));
|
||||
performDataTypeMappingTest("boolean_prop", true, OnOffType.ON, false, OnOffType.OFF);
|
||||
}
|
||||
|
||||
private void performDataTypeMappingTest(String propertyName, Object initialValue, State initialState,
|
||||
Object updatedValue, State updatedState) throws Exception {
|
||||
var httpClient = mock(org.eclipse.jetty.client.HttpClient.class);
|
||||
var request = Mocks.mockRequest(null, load("/datatypes_test_response.json"));
|
||||
when(httpClient.newRequest(URI.create("http://example.org:8090/"))).thenReturn(request);
|
||||
|
||||
var request2 = Mocks.mockRequest(gson.toJson(Map.of(propertyName, updatedValue)),
|
||||
gson.toJson(Map.of(propertyName, initialValue)));
|
||||
when(httpClient.newRequest(URI.create("http://example.org:8090/properties/" + propertyName)))
|
||||
.thenReturn(request2);
|
||||
|
||||
var thingUID = new ThingUID("webthing", "test");
|
||||
var channelUID = Channels.createChannelUID(thingUID, propertyName);
|
||||
|
||||
var errorHandler = new WebthingTest.ErrorHandler();
|
||||
var websocketConnectionFactory = new WebthingTest.TestWebsocketConnectionFactory();
|
||||
var webthing = WebthingTest.createTestWebthing("http://example.org:8090/", httpClient, errorHandler,
|
||||
websocketConnectionFactory);
|
||||
var channel = Channels.createChannel(thingUID, propertyName,
|
||||
Objects.requireNonNull(webthing.getPropertyDescription(propertyName)));
|
||||
|
||||
var testWebthingThingHandler = new TestWebthingThingHandler();
|
||||
|
||||
PropertyToChannelLink.establish(webthing, propertyName, testWebthingThingHandler, channel);
|
||||
|
||||
var message = new PropertyStatusMessage();
|
||||
message.messageType = "propertyStatus";
|
||||
message.data = Map.of(propertyName, initialValue);
|
||||
websocketConnectionFactory.webSocketRef.get().sendToClient(message);
|
||||
|
||||
assertEquals(initialState, testWebthingThingHandler.itemState.get(channelUID));
|
||||
|
||||
ChannelToPropertyLink.establish(testWebthingThingHandler, channel, webthing, propertyName);
|
||||
testWebthingThingHandler.listeners.get(channelUID).onItemStateChanged(channelUID, updatedState);
|
||||
}
|
||||
|
||||
public static String load(String name) throws Exception {
|
||||
return new String(Files.readAllBytes(Paths.get(WebthingTest.class.getResource(name).toURI())));
|
||||
}
|
||||
|
||||
private static class TestWebthingThingHandler implements ChannelHandler {
|
||||
public final Map<ChannelUID, ItemChangedListener> listeners = new ConcurrentHashMap<>();
|
||||
public final Map<ChannelUID, Command> itemState = new ConcurrentHashMap<>();
|
||||
|
||||
@Override
|
||||
public void observeChannel(ChannelUID channelUID, ItemChangedListener listener) {
|
||||
listeners.put(channelUID, listener);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateItemState(ChannelUID channelUID, Command command) {
|
||||
itemState.put(channelUID, command);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,394 @@
|
||||
[
|
||||
{
|
||||
"id":"urn:dev:ops:anwing-TB6612FNG",
|
||||
"title":"Awning lane1",
|
||||
"@context":"https://iot.mozilla.org/schemas",
|
||||
"properties":{
|
||||
"target_position":{
|
||||
"@type":"LevelProperty",
|
||||
"title":"awning lane1 target position",
|
||||
"type":"integer",
|
||||
"minimum":0,
|
||||
"maximum":100,
|
||||
"description":"awning lane1 target position",
|
||||
"links":[
|
||||
{
|
||||
"rel":"property",
|
||||
"href":"/0/properties/target_position"
|
||||
}
|
||||
]
|
||||
},
|
||||
"current_position":{
|
||||
"@type":"LevelProperty",
|
||||
"title":"awning lane1 current position",
|
||||
"type":"integer",
|
||||
"minimum":0,
|
||||
"maximum":100,
|
||||
"readOnly":true,
|
||||
"description":"awning lane1 current position",
|
||||
"links":[
|
||||
{
|
||||
"rel":"property",
|
||||
"href":"/0/properties/current_position"
|
||||
}
|
||||
]
|
||||
},
|
||||
"retracting":{
|
||||
"@type":"BooleanProperty",
|
||||
"title":"lane1 is retracting",
|
||||
"type":"boolean",
|
||||
"readOnly":true,
|
||||
"description":"lane1 is retracting",
|
||||
"links":[
|
||||
{
|
||||
"rel":"property",
|
||||
"href":"/0/properties/retracting"
|
||||
}
|
||||
]
|
||||
},
|
||||
"extending":{
|
||||
"@type":"BooleanProperty",
|
||||
"title":"lane1 is extending",
|
||||
"type":"boolean",
|
||||
"readOnly":true,
|
||||
"description":"lane1 is extending",
|
||||
"links":[
|
||||
{
|
||||
"rel":"property",
|
||||
"href":"/0/properties/extending"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"actions":{
|
||||
|
||||
},
|
||||
"events":{
|
||||
|
||||
},
|
||||
"links":[
|
||||
{
|
||||
"rel":"properties",
|
||||
"href":"/0/properties"
|
||||
},
|
||||
{
|
||||
"rel":"actions",
|
||||
"href":"/0/actions"
|
||||
},
|
||||
{
|
||||
"rel":"events",
|
||||
"href":"/0/events"
|
||||
},
|
||||
{
|
||||
"rel":"alternate",
|
||||
"href":"ws://192.168.4.12:9040/0"
|
||||
}
|
||||
],
|
||||
"description":"A web connected patio awnings controller on Raspberry Pi",
|
||||
"@type":[
|
||||
"MultiLevelSensor"
|
||||
],
|
||||
"href":"/0",
|
||||
"base":"http://192.168.4.12:9040/0",
|
||||
"securityDefinitions":{
|
||||
"nosec_sc":{
|
||||
"scheme":"nosec"
|
||||
}
|
||||
},
|
||||
"security":"nosec_sc"
|
||||
},
|
||||
{
|
||||
"id":"urn:dev:ops:anwing-TB6612FNG",
|
||||
"title":"Awning lane2",
|
||||
"@context":"https://iot.mozilla.org/schemas",
|
||||
"properties":{
|
||||
"target_position":{
|
||||
"@type":"LevelProperty",
|
||||
"title":"awning lane2 target position",
|
||||
"type":"integer",
|
||||
"minimum":0,
|
||||
"maximum":100,
|
||||
"description":"awning lane2 target position",
|
||||
"links":[
|
||||
{
|
||||
"rel":"property",
|
||||
"href":"/1/properties/target_position"
|
||||
}
|
||||
]
|
||||
},
|
||||
"current_position":{
|
||||
"@type":"LevelProperty",
|
||||
"title":"awning lane2 current position",
|
||||
"type":"integer",
|
||||
"minimum":0,
|
||||
"maximum":100,
|
||||
"readOnly":true,
|
||||
"description":"awning lane2 current position",
|
||||
"links":[
|
||||
{
|
||||
"rel":"property",
|
||||
"href":"/1/properties/current_position"
|
||||
}
|
||||
]
|
||||
},
|
||||
"retracting":{
|
||||
"@type":"BooleanProperty",
|
||||
"title":"lane2 is retracting",
|
||||
"type":"boolean",
|
||||
"readOnly":true,
|
||||
"description":"lane2 is retracting",
|
||||
"links":[
|
||||
{
|
||||
"rel":"property",
|
||||
"href":"/1/properties/retracting"
|
||||
}
|
||||
]
|
||||
},
|
||||
"extending":{
|
||||
"@type":"BooleanProperty",
|
||||
"title":"lane2 is extending",
|
||||
"type":"boolean",
|
||||
"readOnly":true,
|
||||
"description":"lane2 is extending",
|
||||
"links":[
|
||||
{
|
||||
"rel":"property",
|
||||
"href":"/1/properties/extending"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"actions":{
|
||||
|
||||
},
|
||||
"events":{
|
||||
|
||||
},
|
||||
"links":[
|
||||
{
|
||||
"rel":"properties",
|
||||
"href":"/1/properties"
|
||||
},
|
||||
{
|
||||
"rel":"actions",
|
||||
"href":"/1/actions"
|
||||
},
|
||||
{
|
||||
"rel":"events",
|
||||
"href":"/1/events"
|
||||
},
|
||||
{
|
||||
"rel":"alternate",
|
||||
"href":"ws://192.168.4.12:9040/1"
|
||||
}
|
||||
],
|
||||
"description":"A web connected patio awnings controller on Raspberry Pi",
|
||||
"@type":[
|
||||
"MultiLevelSensor"
|
||||
],
|
||||
"href":"/1",
|
||||
"base":"http://192.168.4.12:9040/1",
|
||||
"securityDefinitions":{
|
||||
"nosec_sc":{
|
||||
"scheme":"nosec"
|
||||
}
|
||||
},
|
||||
"security":"nosec_sc"
|
||||
},
|
||||
{
|
||||
"id":"urn:dev:ops:anwing-TB6612FNG",
|
||||
"title":"Awning lane3",
|
||||
"@context":"https://iot.mozilla.org/schemas",
|
||||
"properties":{
|
||||
"target_position":{
|
||||
"@type":"LevelProperty",
|
||||
"title":"awning lane3 target position",
|
||||
"type":"integer",
|
||||
"minimum":0,
|
||||
"maximum":100,
|
||||
"description":"awning lane3 target position",
|
||||
"links":[
|
||||
{
|
||||
"rel":"property",
|
||||
"href":"/2/properties/target_position"
|
||||
}
|
||||
]
|
||||
},
|
||||
"current_position":{
|
||||
"@type":"LevelProperty",
|
||||
"title":"awning lane3 current position",
|
||||
"type":"integer",
|
||||
"minimum":0,
|
||||
"maximum":100,
|
||||
"readOnly":true,
|
||||
"description":"awning lane3 current position",
|
||||
"links":[
|
||||
{
|
||||
"rel":"property",
|
||||
"href":"/2/properties/current_position"
|
||||
}
|
||||
]
|
||||
},
|
||||
"retracting":{
|
||||
"@type":"BooleanProperty",
|
||||
"title":"lane3 is retracting",
|
||||
"type":"boolean",
|
||||
"readOnly":true,
|
||||
"description":"lane3 is retracting",
|
||||
"links":[
|
||||
{
|
||||
"rel":"property",
|
||||
"href":"/2/properties/retracting"
|
||||
}
|
||||
]
|
||||
},
|
||||
"extending":{
|
||||
"@type":"BooleanProperty",
|
||||
"title":"lane3 is extending",
|
||||
"type":"boolean",
|
||||
"readOnly":true,
|
||||
"description":"lane3 is extending",
|
||||
"links":[
|
||||
{
|
||||
"rel":"property",
|
||||
"href":"/2/properties/extending"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"actions":{
|
||||
|
||||
},
|
||||
"events":{
|
||||
|
||||
},
|
||||
"links":[
|
||||
{
|
||||
"rel":"properties",
|
||||
"href":"/2/properties"
|
||||
},
|
||||
{
|
||||
"rel":"actions",
|
||||
"href":"/2/actions"
|
||||
},
|
||||
{
|
||||
"rel":"events",
|
||||
"href":"/2/events"
|
||||
},
|
||||
{
|
||||
"rel":"alternate",
|
||||
"href":"ws://192.168.4.12:9040/2"
|
||||
}
|
||||
],
|
||||
"description":"A web connected patio awnings controller on Raspberry Pi",
|
||||
"@type":[
|
||||
"MultiLevelSensor"
|
||||
],
|
||||
"href":"/2",
|
||||
"base":"http://192.168.4.12:9040/2",
|
||||
"securityDefinitions":{
|
||||
"nosec_sc":{
|
||||
"scheme":"nosec"
|
||||
}
|
||||
},
|
||||
"security":"nosec_sc"
|
||||
},
|
||||
{
|
||||
"id":"urn:dev:ops:anwing-TB6612FNG",
|
||||
"title":"Awning lane4",
|
||||
"@context":"https://iot.mozilla.org/schemas",
|
||||
"properties":{
|
||||
"target_position":{
|
||||
"@type":"LevelProperty",
|
||||
"title":"awning lane4 target position",
|
||||
"type":"integer",
|
||||
"minimum":0,
|
||||
"maximum":100,
|
||||
"description":"awning lane4 target position",
|
||||
"links":[
|
||||
{
|
||||
"rel":"property",
|
||||
"href":"/3/properties/target_position"
|
||||
}
|
||||
]
|
||||
},
|
||||
"current_position":{
|
||||
"@type":"LevelProperty",
|
||||
"title":"awning lane4 current position",
|
||||
"type":"integer",
|
||||
"minimum":0,
|
||||
"maximum":100,
|
||||
"readOnly":true,
|
||||
"description":"awning lane4 current position",
|
||||
"links":[
|
||||
{
|
||||
"rel":"property",
|
||||
"href":"/3/properties/current_position"
|
||||
}
|
||||
]
|
||||
},
|
||||
"retracting":{
|
||||
"@type":"BooleanProperty",
|
||||
"title":"lane4 is retracting",
|
||||
"type":"boolean",
|
||||
"readOnly":true,
|
||||
"description":"lane4 is retracting",
|
||||
"links":[
|
||||
{
|
||||
"rel":"property",
|
||||
"href":"/3/properties/retracting"
|
||||
}
|
||||
]
|
||||
},
|
||||
"extending":{
|
||||
"@type":"BooleanProperty",
|
||||
"title":"lane4 is extending",
|
||||
"type":"boolean",
|
||||
"readOnly":true,
|
||||
"description":"lane4 is extending",
|
||||
"links":[
|
||||
{
|
||||
"rel":"property",
|
||||
"href":"/3/properties/extending"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"actions":{
|
||||
|
||||
},
|
||||
"events":{
|
||||
|
||||
},
|
||||
"links":[
|
||||
{
|
||||
"rel":"properties",
|
||||
"href":"/3/properties"
|
||||
},
|
||||
{
|
||||
"rel":"actions",
|
||||
"href":"/3/actions"
|
||||
},
|
||||
{
|
||||
"rel":"events",
|
||||
"href":"/3/events"
|
||||
},
|
||||
{
|
||||
"rel":"alternate",
|
||||
"href":"ws://192.168.4.12:9040/3"
|
||||
}
|
||||
],
|
||||
"description":"A web connected patio awnings controller on Raspberry Pi",
|
||||
"@type":[
|
||||
"MultiLevelSensor"
|
||||
],
|
||||
"href":"/3",
|
||||
"base":"http://192.168.4.12:9040/3",
|
||||
"securityDefinitions":{
|
||||
"nosec_sc":{
|
||||
"scheme":"nosec"
|
||||
}
|
||||
},
|
||||
"security":"nosec_sc"
|
||||
}
|
||||
]
|
@ -0,0 +1 @@
|
||||
{"target_position":85}
|
@ -0,0 +1,98 @@
|
||||
{
|
||||
"id":"urn:dev:ops:anwing-TB6612FNG",
|
||||
"title":"Awning lane1",
|
||||
"@context":"https://webthings.io/schemas",
|
||||
"properties":{
|
||||
"target_position":{
|
||||
"@type":"LevelProperty",
|
||||
"title":"awning lane1 target position",
|
||||
"type":"integer",
|
||||
"minimum":0,
|
||||
"maximum":100,
|
||||
"description":"awning lane1 target position",
|
||||
"links":[
|
||||
{
|
||||
"rel":"property",
|
||||
"href":"/0/properties/target_position"
|
||||
}
|
||||
]
|
||||
},
|
||||
"current_position":{
|
||||
"@type":"LevelProperty",
|
||||
"title":"awning lane1 current position",
|
||||
"type":"integer",
|
||||
"minimum":0,
|
||||
"maximum":100,
|
||||
"readOnly":true,
|
||||
"description":"awning lane1 current position",
|
||||
"links":[
|
||||
{
|
||||
"rel":"property",
|
||||
"href":"/0/properties/current_position"
|
||||
}
|
||||
]
|
||||
},
|
||||
"retracting":{
|
||||
"@type":"BooleanProperty",
|
||||
"title":"lane1 is retracting",
|
||||
"type":"boolean",
|
||||
"readOnly":true,
|
||||
"description":"lane1 is retracting",
|
||||
"links":[
|
||||
{
|
||||
"rel":"property",
|
||||
"href":"/0/properties/retracting"
|
||||
}
|
||||
]
|
||||
},
|
||||
"extending":{
|
||||
"@type":"BooleanProperty",
|
||||
"title":"lane1 is extending",
|
||||
"type":"boolean",
|
||||
"readOnly":true,
|
||||
"description":"lane1 is extending",
|
||||
"links":[
|
||||
{
|
||||
"rel":"property",
|
||||
"href":"/0/properties/extending"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"actions":{
|
||||
|
||||
},
|
||||
"events":{
|
||||
|
||||
},
|
||||
"links":[
|
||||
{
|
||||
"rel":"properties",
|
||||
"href":"/0/properties"
|
||||
},
|
||||
{
|
||||
"rel":"actions",
|
||||
"href":"/0/actions"
|
||||
},
|
||||
{
|
||||
"rel":"events",
|
||||
"href":"/0/events"
|
||||
},
|
||||
{
|
||||
"rel":"alternate",
|
||||
"href":"ws://192.168.4.12:9040/0"
|
||||
}
|
||||
],
|
||||
"description":"A web connected patio awnings controller on Raspberry Pi",
|
||||
"@type":[
|
||||
"MultiLevelSensor"
|
||||
],
|
||||
"href":"/0",
|
||||
"base":"http://192.168.4.12:9040/0",
|
||||
"securityDefinitions":{
|
||||
"nosec_sc":{
|
||||
"scheme":"nosec"
|
||||
}
|
||||
},
|
||||
"security":"nosec_sc"
|
||||
}
|
@ -0,0 +1,209 @@
|
||||
{
|
||||
"id":"urn:dev:ops:test-1",
|
||||
"title":"Test Device",
|
||||
"@context":"https://iot.mozilla.org/schemas",
|
||||
"properties":{
|
||||
"number_prop":{
|
||||
"type":"number",
|
||||
"links":[
|
||||
{
|
||||
"rel":"property",
|
||||
"href":"/properties/number_prop"
|
||||
}
|
||||
]
|
||||
},
|
||||
"integer_prop":{
|
||||
"type":"integer",
|
||||
"links":[
|
||||
{
|
||||
"rel":"property",
|
||||
"href":"/properties/integer_prop"
|
||||
}
|
||||
]
|
||||
},
|
||||
"string_prop":{
|
||||
"type":"string",
|
||||
"links":[
|
||||
{
|
||||
"rel":"property",
|
||||
"href":"/properties/string_prop"
|
||||
}
|
||||
]
|
||||
},
|
||||
"boolean_prop":{
|
||||
"type":"boolean",
|
||||
"links":[
|
||||
{
|
||||
"rel":"property",
|
||||
"href":"/properties/boolean_prop"
|
||||
}
|
||||
]
|
||||
},
|
||||
"onoff_prop":{
|
||||
"@type": "OnOffProperty",
|
||||
"type":"boolean",
|
||||
"links":[
|
||||
{
|
||||
"rel":"property",
|
||||
"href":"/properties/onoff_prop"
|
||||
}
|
||||
]
|
||||
},
|
||||
"heating_prop":{
|
||||
"@type": "HeatingCoolingProperty",
|
||||
"type":"string",
|
||||
"links":[
|
||||
{
|
||||
"rel":"property",
|
||||
"href":"/properties/heating_prop"
|
||||
}
|
||||
]
|
||||
},
|
||||
"voltage_prop":{
|
||||
"@type": "VoltageProperty",
|
||||
"type":"number",
|
||||
"links":[
|
||||
{
|
||||
"rel":"property",
|
||||
"href":"/properties/voltage_prop"
|
||||
}
|
||||
]
|
||||
},
|
||||
"brightness_prop":{
|
||||
"@type": "BrightnessProperty",
|
||||
"type":"number",
|
||||
"links":[
|
||||
{
|
||||
"rel":"property",
|
||||
"href":"/properties/brightness_prop"
|
||||
}
|
||||
]
|
||||
},
|
||||
"colormode_prop":{
|
||||
"@type": "ColorModeProperty",
|
||||
"type":"string",
|
||||
"links":[
|
||||
{
|
||||
"rel":"property",
|
||||
"href":"/properties/colormode_prop"
|
||||
}
|
||||
]
|
||||
},
|
||||
"colortemp_prop":{
|
||||
"@type": "ColorTemperatureProperty",
|
||||
"type":"integer",
|
||||
"links":[
|
||||
{
|
||||
"rel":"property",
|
||||
"href":"/properties/colortemp_prop"
|
||||
}
|
||||
]
|
||||
},
|
||||
"color_prop":{
|
||||
"@type": "ColorProperty",
|
||||
"type":"string",
|
||||
"links":[
|
||||
{
|
||||
"rel":"property",
|
||||
"href":"/properties/color_prop"
|
||||
}
|
||||
]
|
||||
},
|
||||
"open_prop":{
|
||||
"@type": "OpenProperty",
|
||||
"type":"boolean",
|
||||
"links":[
|
||||
{
|
||||
"rel":"property",
|
||||
"href":"/properties/open_prop"
|
||||
}
|
||||
]
|
||||
},
|
||||
"targettemp_prop":{
|
||||
"@type": "TargetTemperatureProperty",
|
||||
"type":"number",
|
||||
"links":[
|
||||
{
|
||||
"rel":"property",
|
||||
"href":"/properties/targettemp_prop"
|
||||
}
|
||||
]
|
||||
},
|
||||
"temp_prop":{
|
||||
"@type": "TemperatureProperty",
|
||||
"type":"number",
|
||||
"links":[
|
||||
{
|
||||
"rel":"property",
|
||||
"href":"/properties/temp_prop"
|
||||
}
|
||||
]
|
||||
},
|
||||
"thermo_prop":{
|
||||
"@type": "ThermostatModeProperty",
|
||||
"type":"string",
|
||||
"links":[
|
||||
{
|
||||
"rel":"property",
|
||||
"href":"/properties/thermo_prop"
|
||||
}
|
||||
]
|
||||
},
|
||||
"level_unit_prop":{
|
||||
"@type": "LevelProperty",
|
||||
"type":"number",
|
||||
"unit": "percent",
|
||||
"links":[
|
||||
{
|
||||
"rel":"property",
|
||||
"href":"/properties/level_unit_prop"
|
||||
}
|
||||
]
|
||||
},
|
||||
"level_prop":{
|
||||
"@type": "LevelProperty",
|
||||
"type":"number",
|
||||
"links":[
|
||||
{
|
||||
"rel":"property",
|
||||
"href":"/properties/level_prop"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"actions":{
|
||||
|
||||
},
|
||||
"events":{
|
||||
|
||||
},
|
||||
"links":[
|
||||
{
|
||||
"rel":"properties",
|
||||
"href":"/properties"
|
||||
},
|
||||
{
|
||||
"rel":"actions",
|
||||
"href":"/actions"
|
||||
},
|
||||
{
|
||||
"rel":"events",
|
||||
"href":"/events"
|
||||
},
|
||||
{
|
||||
"rel":"alternate",
|
||||
"href":"ws://192.168.0.23:9060/"
|
||||
}
|
||||
],
|
||||
"description":"test",
|
||||
"@type":[
|
||||
"MultiLevelSensor"
|
||||
],
|
||||
"base":"http://192.168.0.23:9060/",
|
||||
"securityDefinitions":{
|
||||
"nosec_sc":{
|
||||
"scheme":"nosec"
|
||||
}
|
||||
},
|
||||
"security":"nosec_sc"
|
||||
}
|
@ -0,0 +1 @@
|
||||
{"number_prop":80.5}
|
@ -0,0 +1,98 @@
|
||||
{
|
||||
"id":"urn:dev:ops:anwing-TB6612FNG",
|
||||
"title":"Awning lane1",
|
||||
"@context":"https://www.w3.org/2019/wot/td/v1",
|
||||
"properties":{
|
||||
"target_position":{
|
||||
"@type":"LevelProperty",
|
||||
"title":"awning lane1 target position",
|
||||
"type":"integer",
|
||||
"minimum":0,
|
||||
"maximum":100,
|
||||
"description":"awning lane1 target position",
|
||||
"links":[
|
||||
{
|
||||
"rel":"property",
|
||||
"href":"/0/properties/target_position"
|
||||
}
|
||||
]
|
||||
},
|
||||
"current_position":{
|
||||
"@type":"LevelProperty",
|
||||
"title":"awning lane1 current position",
|
||||
"type":"integer",
|
||||
"minimum":0,
|
||||
"maximum":100,
|
||||
"readOnly":true,
|
||||
"description":"awning lane1 current position",
|
||||
"links":[
|
||||
{
|
||||
"rel":"property",
|
||||
"href":"/0/properties/current_position"
|
||||
}
|
||||
]
|
||||
},
|
||||
"retracting":{
|
||||
"@type":"BooleanProperty",
|
||||
"title":"lane1 is retracting",
|
||||
"type":"boolean",
|
||||
"readOnly":true,
|
||||
"description":"lane1 is retracting",
|
||||
"links":[
|
||||
{
|
||||
"rel":"property",
|
||||
"href":"/0/properties/retracting"
|
||||
}
|
||||
]
|
||||
},
|
||||
"extending":{
|
||||
"@type":"BooleanProperty",
|
||||
"title":"lane1 is extending",
|
||||
"type":"boolean",
|
||||
"readOnly":true,
|
||||
"description":"lane1 is extending",
|
||||
"links":[
|
||||
{
|
||||
"rel":"property",
|
||||
"href":"/0/properties/extending"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"actions":{
|
||||
|
||||
},
|
||||
"events":{
|
||||
|
||||
},
|
||||
"links":[
|
||||
{
|
||||
"rel":"properties",
|
||||
"href":"/0/properties"
|
||||
},
|
||||
{
|
||||
"rel":"actions",
|
||||
"href":"/0/actions"
|
||||
},
|
||||
{
|
||||
"rel":"events",
|
||||
"href":"/0/events"
|
||||
},
|
||||
{
|
||||
"rel":"alternate",
|
||||
"href":"ws://192.168.4.12:9040/0"
|
||||
}
|
||||
],
|
||||
"description":"A web connected patio awnings controller on Raspberry Pi",
|
||||
"@type":[
|
||||
"MultiLevelSensor"
|
||||
],
|
||||
"href":"/0",
|
||||
"base":"http://192.168.4.12:9040/0",
|
||||
"securityDefinitions":{
|
||||
"nosec_sc":{
|
||||
"scheme":"nosec"
|
||||
}
|
||||
},
|
||||
"security":"nosec_sc"
|
||||
}
|
@ -0,0 +1,55 @@
|
||||
{
|
||||
"id":"urn:dev:ops:eltakowsSensor-1",
|
||||
"title":"Wind",
|
||||
"properties":{
|
||||
"windspeed":{
|
||||
"@type":"LevelProperty",
|
||||
"title":"Windspeed",
|
||||
"type":"number",
|
||||
"description":"The current windspeed",
|
||||
"unit":"km/h",
|
||||
"readOnly":true,
|
||||
"links":[
|
||||
{
|
||||
"rel":"property",
|
||||
"href":"/properties/windspeed"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"actions":{
|
||||
|
||||
},
|
||||
"events":{
|
||||
|
||||
},
|
||||
"links":[
|
||||
{
|
||||
"rel":"properties",
|
||||
"href":"/properties"
|
||||
},
|
||||
{
|
||||
"rel":"actions",
|
||||
"href":"/actions"
|
||||
},
|
||||
{
|
||||
"rel":"events",
|
||||
"href":"/events"
|
||||
},
|
||||
{
|
||||
"rel":"alternate",
|
||||
"href":"ws://192.168.0.23:9060/"
|
||||
}
|
||||
],
|
||||
"description":"A web connected Eltako windsensor measuring wind speed on Raspberry Pi",
|
||||
"@type":[
|
||||
"MultiLevelSensor"
|
||||
],
|
||||
"base":"http://192.168.0.23:9060/",
|
||||
"securityDefinitions":{
|
||||
"nosec_sc":{
|
||||
"scheme":"nosec"
|
||||
}
|
||||
},
|
||||
"security":"nosec_sc"
|
||||
}
|
@ -0,0 +1,75 @@
|
||||
{
|
||||
"title":"Virtual On/Off Light",
|
||||
"@context":"https://iot.mozilla.org/schemas",
|
||||
"@type":[
|
||||
"OnOffSwitch",
|
||||
"Light"
|
||||
],
|
||||
"description":"",
|
||||
"href":"/things/virtual-things-7",
|
||||
"properties":{
|
||||
"on":{
|
||||
"name":"on",
|
||||
"value":false,
|
||||
"visible":true,
|
||||
"title":"On/Off",
|
||||
"type":"boolean",
|
||||
"@type":"OnOffProperty",
|
||||
"links":[
|
||||
{
|
||||
"rel":"property",
|
||||
"href":"/things/virtual-things-7/properties/on"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"actions":{
|
||||
|
||||
},
|
||||
"events":{
|
||||
|
||||
},
|
||||
"links":[
|
||||
{
|
||||
"rel":"properties",
|
||||
"href":"/things/virtual-things-7/properties"
|
||||
},
|
||||
{
|
||||
"rel":"actions",
|
||||
"href":"/things/virtual-things-7/actions"
|
||||
},
|
||||
{
|
||||
"rel":"events",
|
||||
"href":"/things/virtual-things-7/events"
|
||||
},
|
||||
{
|
||||
"rel":"alternate",
|
||||
"mediaType":"text/html",
|
||||
"href":"/things/virtual-things-7"
|
||||
},
|
||||
{
|
||||
"rel":"alternate",
|
||||
"href":"ws://webthings/things/virtual-things-7"
|
||||
}
|
||||
],
|
||||
"layoutIndex":0,
|
||||
"selectedCapability":"Light",
|
||||
"iconHref":null,
|
||||
"id":"http://webthings/things/virtual-things-7",
|
||||
"base":"http://webthings/",
|
||||
"securityDefinitions":{
|
||||
"oauth2_sc":{
|
||||
"scheme":"oauth2",
|
||||
"flow":"code",
|
||||
"authorization":"http://webthings/oauth/authorize",
|
||||
"token":"http://webthings/oauth/token",
|
||||
"scopes":[
|
||||
"/things/virtual-things-7:readwrite",
|
||||
"/things/virtual-things-7",
|
||||
"/things:readwrite",
|
||||
"/things"
|
||||
]
|
||||
}
|
||||
},
|
||||
"security":"oauth2_sc"
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
{
|
||||
"windspeed":34
|
||||
}
|
@ -0,0 +1,56 @@
|
||||
{
|
||||
"id":"urn:dev:ops:eltakowsSensor-1",
|
||||
"title":"Wind",
|
||||
"@context":"https://iot.mozilla.org/schemas/",
|
||||
"properties":{
|
||||
"windspeed":{
|
||||
"@type":"LevelProperty",
|
||||
"title":"Windspeed",
|
||||
"type":"number",
|
||||
"description":"The current windspeed",
|
||||
"unit":"km/h",
|
||||
"readOnly":true,
|
||||
"links":[
|
||||
{
|
||||
"rel":"property",
|
||||
"href":"/properties/windspeed"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"actions":{
|
||||
|
||||
},
|
||||
"events":{
|
||||
|
||||
},
|
||||
"links":[
|
||||
{
|
||||
"rel":"properties",
|
||||
"href":"/properties"
|
||||
},
|
||||
{
|
||||
"rel":"actions",
|
||||
"href":"/actions"
|
||||
},
|
||||
{
|
||||
"rel":"events",
|
||||
"href":"/events"
|
||||
},
|
||||
{
|
||||
"rel":"alternate",
|
||||
"href":"ws://192.168.0.23:9060/"
|
||||
}
|
||||
],
|
||||
"description":"A web connected Eltako windsensor measuring wind speed on Raspberry Pi",
|
||||
"@type":[
|
||||
"MultiLevelSensor"
|
||||
],
|
||||
"base":"http://192.168.0.23:9060/",
|
||||
"securityDefinitions":{
|
||||
"nosec_sc":{
|
||||
"scheme":"nosec"
|
||||
}
|
||||
},
|
||||
"security":"nosec_sc"
|
||||
}
|
@ -0,0 +1,52 @@
|
||||
{
|
||||
"id":"urn:dev:ops:eltakowsSensor-1",
|
||||
"title":"Wind",
|
||||
"@context":"https://iot.mozilla.org/schemas",
|
||||
"properties":{
|
||||
"windspeed":{
|
||||
"@type":"LevelProperty",
|
||||
"title":"Windspeed",
|
||||
"type":"number",
|
||||
"description":"The current windspeed",
|
||||
"unit":"km/h",
|
||||
"readOnly":true,
|
||||
"links":[
|
||||
{
|
||||
"rel":"property",
|
||||
"href":"/properties/windspeed"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"actions":{
|
||||
|
||||
},
|
||||
"events":{
|
||||
|
||||
},
|
||||
"links":[
|
||||
{
|
||||
"rel":"properties",
|
||||
"href":"/properties"
|
||||
},
|
||||
{
|
||||
"rel":"actions",
|
||||
"href":"/actions"
|
||||
},
|
||||
{
|
||||
"rel":"events",
|
||||
"href":"/events"
|
||||
}
|
||||
],
|
||||
"description":"A web connected Eltako windsensor measuring wind speed on Raspberry Pi",
|
||||
"@type":[
|
||||
"MultiLevelSensor"
|
||||
],
|
||||
"base":"http://192.168.0.23:9060/",
|
||||
"securityDefinitions":{
|
||||
"nosec_sc":{
|
||||
"scheme":"nosec"
|
||||
}
|
||||
},
|
||||
"security":"nosec_sc"
|
||||
}
|
@ -326,6 +326,7 @@
|
||||
<module>org.openhab.binding.volvooncall</module>
|
||||
<module>org.openhab.binding.weathercompany</module>
|
||||
<module>org.openhab.binding.weatherunderground</module>
|
||||
<module>org.openhab.binding.webthing</module>
|
||||
<module>org.openhab.binding.wemo</module>
|
||||
<module>org.openhab.binding.wifiled</module>
|
||||
<module>org.openhab.binding.windcentrale</module>
|
||||
|
Loading…
Reference in New Issue
Block a user