[WebThing] Initial contribution (#9555)

Signed-off-by: Gregor Roth <gregor.roth@web.de>
This commit is contained in:
grro
2021-04-11 19:47:27 +02:00
committed by GitHub
parent 9bfb2f4313
commit d9ed461950
54 changed files with 4545 additions and 0 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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