[remoteopenhab] Remote openHAB binding - initial contributionn (#8791)

Fix #8407

Signed-off-by: Laurent Garnier <lg.hc@free.fr>
This commit is contained in:
lolodomo 2020-10-26 22:39:19 +01:00 committed by GitHub
parent 08405233ae
commit 4646ea68c3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 1804 additions and 0 deletions

View File

@ -200,6 +200,7 @@
/bundles/org.openhab.binding.pushbullet/ @hakan42
/bundles/org.openhab.binding.radiothermostat/ @mlobstein
/bundles/org.openhab.binding.regoheatpump/ @crnjan
/bundles/org.openhab.binding.remoteopenhab/ @lolodomo
/bundles/org.openhab.binding.rfxcom/ @martinvw @paulianttila
/bundles/org.openhab.binding.rme/ @kgoderis
/bundles/org.openhab.binding.robonect/ @reyem

View File

@ -991,6 +991,11 @@
<artifactId>org.openhab.binding.regoheatpump</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.openhab.addons.bundles</groupId>
<artifactId>org.openhab.binding.remoteopenhab</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.openhab.addons.bundles</groupId>
<artifactId>org.openhab.binding.rfxcom</artifactId>

View File

@ -0,0 +1,13 @@
This content is produced and maintained by the openHAB project.
* Project home: https://www.openhab.org
== Declared Project Licenses
This program and the accompanying materials are made available under the terms
of the Eclipse Public License 2.0 which is available at
https://www.eclipse.org/legal/epl-2.0/.
== Source Code
https://github.com/openhab/openhab-addons

View File

@ -0,0 +1,66 @@
# Remote openHAB Binding
The Remote openHAB binding allows to communicate with remote openHAB servers.
The communication is bidirectional.
The binding on the local server listens to any item state updates on the remote server and updates accordingly the linked channel on the local server.
It also transfers any item command from the local server to the remote server.
One first usage is the distribution of your home automation system on a set of openHAB servers.
A second usage is for users having old openHAB v1 bindings running that were not migrated to openHAB v2 or openHAB v3.
They can keep an openHAB v2 server to run their old openHAB v1 bindings and setup a new openHAB v3 server for everything else.
The Remote openHAB binding installed on the openHAB v3 server will then allow to use the openHAB v1 bindings through communication with the openHAB v2 server.
A third usage is for users that would like to keep unchanged an existing openHAB v2 server but would like to use the new UI from openHAB v3; they can simply setup a new openHAB v3 server with the Remote openHAB binding linked to their openHAB v2 server.
## Supported Things
There is one unique supported thing : the `server` bridge thing
## Discovery
All openHAB servers in the local network are automatically discovered (through mDNS) by the binding.
You will find in the inbox one discovery thing per remote server interface.
So if your remote server has one IPv4 address and one IPv6 address, you will discover two things in the inbox.
Just choose one of the two things.
## Binding Configuration
The binding has no configuration options, all configuration is done at Thing level.
## Thing Configuration
The thing has the following configuration parameters:
| Parameter | Required | Description |
|-----------|-------------------------------------------------------------------------------------------------------------------|
| host | yes | The host name or IP address of the remote openHAB server. |
| port | yes | The HTTP port to be used to communicate with the remote openHAB server. Default is 8080. |
| restPath | yes | The subpath of the REST API on the remote openHAB server. Default is /rest |
| token | no | The token to use when the remote openHAB server is setup to require authorization to run its REST API. |
## Channels
The channels are built dynamically and automatically by the binding.
One channel is created for each item from the remote server.
Only basic groups (with no state) are ignored.
The channel id of the built channel corresponds to the name of the item on the remote server.
## Limitations
* The binding will not try to communicate with an openHAB v1 server.
* The binding only uses the HTTP protocol for the communications with the remote server (not HTTPS).
## Example
### demo.things:
```
Bridge remoteopenhab:server:oh2 "OH2 server" [ host="192.168.0.100" ]
```
### demo.items:
```
DateTime MyDate "Date [%1$tA %1$td %1$tR]" <calendar> { channel="remoteopenhab:server:oh2:MyDate" }
```

View File

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

View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<features name="org.openhab.binding.remoteopenhab-${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-remoteopenhab" description="Remote openHAB Binding" version="${project.version}">
<feature>openhab-runtime-base</feature>
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.remoteopenhab/${project.version}
</bundle>
</feature>
</features>

View File

@ -0,0 +1,36 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.remoteopenhab.internal;
import java.util.Collections;
import java.util.Set;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.thing.ThingTypeUID;
/**
* The {@link RemoteopenhabBindingConstants} class defines common constants, which are
* used across the whole binding.
*
* @author Laurent Garnier - Initial contribution
*/
@NonNullByDefault
public class RemoteopenhabBindingConstants {
public static final String BINDING_ID = "remoteopenhab";
// List of all Thing Type UIDs
public static final ThingTypeUID BRIDGE_TYPE_SERVER = new ThingTypeUID(BINDING_ID, "server");
public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Collections.singleton(BRIDGE_TYPE_SERVER);
}

View File

@ -0,0 +1,60 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.remoteopenhab.internal;
import java.util.Collection;
import java.util.List;
import java.util.Locale;
import java.util.concurrent.CopyOnWriteArrayList;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.thing.type.ChannelType;
import org.openhab.core.thing.type.ChannelTypeProvider;
import org.openhab.core.thing.type.ChannelTypeUID;
import org.osgi.service.component.annotations.Component;
/**
* Channel type provider used for all the channel types built by the binding when building dynamically the channels.
* One different channel type is built for each different item type found on the remote openHAB server.
*
* @author Laurent Garnier - Initial contribution
*/
@Component(service = { ChannelTypeProvider.class, RemoteopenhabChannelTypeProvider.class })
@NonNullByDefault
public class RemoteopenhabChannelTypeProvider implements ChannelTypeProvider {
private final List<ChannelType> channelTypes = new CopyOnWriteArrayList<>();
@Override
public Collection<ChannelType> getChannelTypes(@Nullable Locale locale) {
return channelTypes;
}
@Override
public @Nullable ChannelType getChannelType(ChannelTypeUID channelTypeUID, @Nullable Locale locale) {
for (ChannelType channelType : channelTypes) {
if (channelType.getUID().equals(channelTypeUID)) {
return channelType;
}
}
return null;
}
public void addChannelType(ChannelType type) {
channelTypes.add(type);
}
public void removeChannelType(ChannelType type) {
channelTypes.remove(type);
}
}

View File

@ -0,0 +1,85 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.remoteopenhab.internal;
import static org.openhab.binding.remoteopenhab.internal.RemoteopenhabBindingConstants.*;
import javax.ws.rs.client.ClientBuilder;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.remoteopenhab.internal.handler.RemoteopenhabBridgeHandler;
import org.openhab.core.thing.Bridge;
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;
import org.osgi.service.jaxrs.client.SseEventSourceFactory;
import com.google.gson.FieldNamingPolicy;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
/**
* The {@link RemoteopenhabHandlerFactory} is responsible for creating things and thing
* handlers.
*
* @author Laurent Garnier - Initial contribution
*/
@NonNullByDefault
@Component(service = ThingHandlerFactory.class, configurationPid = "binding.remoteopenhab")
public class RemoteopenhabHandlerFactory extends BaseThingHandlerFactory {
private final ClientBuilder clientBuilder;
private final SseEventSourceFactory eventSourceFactory;
private final RemoteopenhabChannelTypeProvider channelTypeProvider;
private final RemoteopenhabStateDescriptionOptionProvider stateDescriptionProvider;
private final Gson jsonParser;
@Activate
public RemoteopenhabHandlerFactory(final @Reference ClientBuilder clientBuilder,
final @Reference SseEventSourceFactory eventSourceFactory,
final @Reference RemoteopenhabChannelTypeProvider channelTypeProvider,
final @Reference RemoteopenhabStateDescriptionOptionProvider stateDescriptionProvider) {
this.clientBuilder = clientBuilder;
this.eventSourceFactory = eventSourceFactory;
this.channelTypeProvider = channelTypeProvider;
this.stateDescriptionProvider = stateDescriptionProvider;
jsonParser = new GsonBuilder().setFieldNamingPolicy(FieldNamingPolicy.IDENTITY).create();
}
/**
* The things this factory supports creating.
*/
@Override
public boolean supportsThingType(ThingTypeUID thingTypeUID) {
return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID);
}
/**
* Creates a handler for the specific thing.
*/
@Override
protected @Nullable ThingHandler createHandler(Thing thing) {
ThingTypeUID thingTypeUID = thing.getThingTypeUID();
return BRIDGE_TYPE_SERVER.equals(thingTypeUID)
? new RemoteopenhabBridgeHandler((Bridge) thing, clientBuilder, eventSourceFactory, channelTypeProvider,
stateDescriptionProvider, jsonParser)
: null;
}
}

View File

@ -0,0 +1,50 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.remoteopenhab.internal;
import java.util.List;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.binding.BaseDynamicStateDescriptionProvider;
import org.openhab.core.thing.i18n.ChannelTypeI18nLocalizationService;
import org.openhab.core.thing.type.DynamicStateDescriptionProvider;
import org.openhab.core.types.StateOption;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
/**
* Dynamic provider of state options while leaving other state description fields as original.
*
* @author Laurent Garnier - Initial contribution
*/
@Component(service = { DynamicStateDescriptionProvider.class, RemoteopenhabStateDescriptionOptionProvider.class })
@NonNullByDefault
public class RemoteopenhabStateDescriptionOptionProvider extends BaseDynamicStateDescriptionProvider {
public @Nullable List<StateOption> getStateOptions(ChannelUID channelUID) {
return channelOptionsMap.get(channelUID);
}
@Reference
protected void setChannelTypeI18nLocalizationService(
final ChannelTypeI18nLocalizationService channelTypeI18nLocalizationService) {
this.channelTypeI18nLocalizationService = channelTypeI18nLocalizationService;
}
protected void unsetChannelTypeI18nLocalizationService(
final ChannelTypeI18nLocalizationService channelTypeI18nLocalizationService) {
this.channelTypeI18nLocalizationService = null;
}
}

View File

@ -0,0 +1,34 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.remoteopenhab.internal.config;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* The {@link RemoteopenhabInstanceConfiguration} is responsible for holding
* configuration informations associated to a remote openHAB server
* thing type
*
* @author Laurent Garnier - Initial contribution
*/
@NonNullByDefault
public class RemoteopenhabInstanceConfiguration {
public static final String HOST = "host";
public static final String PORT = "port";
public static final String REST_PATH = "restPath";
public String host = "";
public int port = 8080;
public String restPath = "/rest";
public String token = "";
}

View File

@ -0,0 +1,28 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.remoteopenhab.internal.data;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Event received through the SSE connection.
*
* @author Laurent Garnier - Initial contribution
*/
@NonNullByDefault
public class Event {
public String type = "";
public String topic = "";
public String payload = "";
}

View File

@ -0,0 +1,27 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.remoteopenhab.internal.data;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Payload from ItemStateEvent / GroupItemStateChangedEvent events received through the SSE connection.
*
* @author Laurent Garnier - Initial contribution
*/
@NonNullByDefault
public class EventPayload {
public String type = "";
public String value = "";
}

View File

@ -0,0 +1,32 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.remoteopenhab.internal.data;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* Response to the API GET /rest/items
* Also payload from ItemAddedEvent / ItemRemovedEvent / ItemUpdatedEvent events received through the SSE connection.
*
* @author Laurent Garnier - Initial contribution
*/
@NonNullByDefault
public class Item {
public String name = "";
public String type = "";
public String state = "";
public String groupType = "";
public @Nullable StateDescription stateDescription;
}

View File

@ -0,0 +1,27 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.remoteopenhab.internal.data;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Part of {@link StateDescription} containing one state option
*
* @author Laurent Garnier - Initial contribution
*/
@NonNullByDefault
public class Option {
public String value = "";
public String label = "";
}

View File

@ -0,0 +1,27 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.remoteopenhab.internal.data;
import org.eclipse.jdt.annotation.Nullable;
/**
* Response to the API GET /rest
*
* @author Laurent Garnier - Initial contribution
*/
public class RestApi {
public String version;
public RestApiEndpoint[] links;
public @Nullable RuntimeInfo runtimeInfo;
}

View File

@ -0,0 +1,27 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.remoteopenhab.internal.data;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Subpart of the response to the API GET /rest
*
* @author Laurent Garnier - Initial contribution
*/
@NonNullByDefault
public class RestApiEndpoint {
public String type = "";
public String url = "";
}

View File

@ -0,0 +1,27 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.remoteopenhab.internal.data;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Subpart of the response to the API GET /rest containing the runtime information
*
* @author Laurent Garnier - Initial contribution
*/
@NonNullByDefault
public class RuntimeInfo {
public String version = "";
public String buildString = "";
}

View File

@ -0,0 +1,31 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.remoteopenhab.internal.data;
import java.util.List;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* Part of {@link Item} containing the state description
*
* @author Laurent Garnier - Initial contribution
*/
@NonNullByDefault
public class StateDescription {
public String pattern = "";
public boolean readOnly;
public @Nullable List<Option> options;
}

View File

@ -0,0 +1,99 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.remoteopenhab.internal.discovery;
import static org.openhab.binding.remoteopenhab.internal.RemoteopenhabBindingConstants.*;
import static org.openhab.binding.remoteopenhab.internal.config.RemoteopenhabInstanceConfiguration.*;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import javax.jmdns.ServiceInfo;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.config.discovery.DiscoveryResult;
import org.openhab.core.config.discovery.DiscoveryResultBuilder;
import org.openhab.core.config.discovery.mdns.MDNSDiscoveryParticipant;
import org.openhab.core.net.NetUtil;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.ThingUID;
import org.osgi.service.component.annotations.Component;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link RemoteopenhabDiscoveryParticipant} is responsible for discovering
* the remote openHAB servers using mDNS discovery service.
*
* @author Laurent Garnier - Initial contribution
*/
@NonNullByDefault
@Component(service = MDNSDiscoveryParticipant.class, configurationPid = "mdnsdiscovery.remoteopenhab")
public class RemoteopenhabDiscoveryParticipant implements MDNSDiscoveryParticipant {
private static final String SERVICE_TYPE = "_openhab-server._tcp.local.";
private final Logger logger = LoggerFactory.getLogger(RemoteopenhabDiscoveryParticipant.class);
@Override
public Set<ThingTypeUID> getSupportedThingTypeUIDs() {
return SUPPORTED_THING_TYPES_UIDS;
}
@Override
public String getServiceType() {
return SERVICE_TYPE;
}
@Override
public @Nullable ThingUID getThingUID(ServiceInfo service) {
// We use the first host address as thing ID
String ip = (service.getHostAddresses() != null && service.getHostAddresses().length > 0
&& !service.getHostAddresses()[0].isEmpty()) ? service.getHostAddresses()[0].replaceAll("\\[|\\]", "")
: null;
// Host address matching a local IP address are ignored
if (getServiceType().equals(service.getType()) && ip != null && !matchLocalIpAddress(ip)) {
return new ThingUID(BRIDGE_TYPE_SERVER, ip.replaceAll("[^A-Za-z0-9_]", "_"));
}
return null;
}
private boolean matchLocalIpAddress(String ipAddress) {
List<String> localIpAddresses = NetUtil.getAllInterfaceAddresses().stream()
.filter(a -> !a.getAddress().isLinkLocalAddress())
.map(a -> a.getAddress().getHostAddress().split("%")[0]).collect(Collectors.toList());
return localIpAddresses.contains(ipAddress);
}
@Override
public @Nullable DiscoveryResult createResult(ServiceInfo service) {
logger.debug("createResult ServiceInfo: {}", service);
DiscoveryResult result = null;
String ip = (service.getHostAddresses() != null && service.getHostAddresses().length > 0
&& !service.getHostAddresses()[0].isEmpty()) ? service.getHostAddresses()[0].replaceAll("\\[|\\]", "")
: null;
String restPath = service.getPropertyString("uri");
ThingUID thingUID = getThingUID(service);
if (thingUID != null && ip != null && restPath != null) {
String label = "openHAB server";
logger.debug("Created a DiscoveryResult for remote openHAB server {} with IP {}", thingUID, ip);
Map<String, Object> properties = Map.of(HOST, ip, REST_PATH, restPath);
result = DiscoveryResultBuilder.create(thingUID).withProperties(properties).withRepresentationProperty(HOST)
.withLabel(label).build();
}
return result;
}
}

View File

@ -0,0 +1,37 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.remoteopenhab.internal.exceptions;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Exceptions thrown by this binding.
*
* @author Laurent Garnier - Initial contribution
*/
@NonNullByDefault
@SuppressWarnings("serial")
public class RemoteopenhabException extends Exception {
public RemoteopenhabException(String message) {
super(message);
}
public RemoteopenhabException(String message, Throwable cause) {
super(message, cause);
}
public RemoteopenhabException(Throwable cause) {
super(cause);
}
}

View File

@ -0,0 +1,565 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.remoteopenhab.internal.handler;
import static org.openhab.binding.remoteopenhab.internal.RemoteopenhabBindingConstants.BINDING_ID;
import java.net.MalformedURLException;
import java.net.URL;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import javax.ws.rs.client.ClientBuilder;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.remoteopenhab.internal.RemoteopenhabChannelTypeProvider;
import org.openhab.binding.remoteopenhab.internal.RemoteopenhabStateDescriptionOptionProvider;
import org.openhab.binding.remoteopenhab.internal.config.RemoteopenhabInstanceConfiguration;
import org.openhab.binding.remoteopenhab.internal.data.Item;
import org.openhab.binding.remoteopenhab.internal.data.Option;
import org.openhab.binding.remoteopenhab.internal.data.StateDescription;
import org.openhab.binding.remoteopenhab.internal.exceptions.RemoteopenhabException;
import org.openhab.binding.remoteopenhab.internal.listener.RemoteopenhabStreamingDataListener;
import org.openhab.binding.remoteopenhab.internal.rest.RemoteopenhabRestClient;
import org.openhab.core.library.CoreItemFactory;
import org.openhab.core.library.types.DateTimeType;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.HSBType;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.OpenClosedType;
import org.openhab.core.library.types.PercentType;
import org.openhab.core.library.types.PlayPauseType;
import org.openhab.core.library.types.PointType;
import org.openhab.core.library.types.QuantityType;
import org.openhab.core.library.types.RawType;
import org.openhab.core.library.types.StringType;
import org.openhab.core.net.NetUtil;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.Channel;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingStatusDetail;
import org.openhab.core.thing.binding.BaseBridgeHandler;
import org.openhab.core.thing.binding.builder.ChannelBuilder;
import org.openhab.core.thing.binding.builder.ThingBuilder;
import org.openhab.core.thing.type.AutoUpdatePolicy;
import org.openhab.core.thing.type.ChannelKind;
import org.openhab.core.thing.type.ChannelType;
import org.openhab.core.thing.type.ChannelTypeBuilder;
import org.openhab.core.thing.type.ChannelTypeUID;
import org.openhab.core.types.Command;
import org.openhab.core.types.RefreshType;
import org.openhab.core.types.State;
import org.openhab.core.types.StateDescriptionFragmentBuilder;
import org.openhab.core.types.StateOption;
import org.openhab.core.types.TypeParser;
import org.openhab.core.types.UnDefType;
import org.osgi.service.jaxrs.client.SseEventSourceFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.gson.Gson;
/**
* The {@link RemoteopenhabBridgeHandler} is responsible for handling commands and updating states
* using the REST API of the remote openHAB server.
*
* @author Laurent Garnier - Initial contribution
*/
@NonNullByDefault
public class RemoteopenhabBridgeHandler extends BaseBridgeHandler implements RemoteopenhabStreamingDataListener {
private static final String DATE_FORMAT_PATTERN = "yyyy-MM-dd'T'HH:mm:ss.SSSZ";
private static final DateTimeFormatter FORMATTER_DATE = DateTimeFormatter.ofPattern(DATE_FORMAT_PATTERN);
private static final long CONNECTION_TIMEOUT_MILLIS = TimeUnit.MILLISECONDS.convert(5, TimeUnit.MINUTES);
private static final int MAX_STATE_SIZE_FOR_LOGGING = 50;
private final Logger logger = LoggerFactory.getLogger(RemoteopenhabBridgeHandler.class);
private final ClientBuilder clientBuilder;
private final SseEventSourceFactory eventSourceFactory;
private final RemoteopenhabChannelTypeProvider channelTypeProvider;
private final RemoteopenhabStateDescriptionOptionProvider stateDescriptionProvider;
private final Gson jsonParser;
private final Object updateThingLock = new Object();
private @NonNullByDefault({}) RemoteopenhabInstanceConfiguration config;
private @Nullable ScheduledFuture<?> checkConnectionJob;
private @Nullable RemoteopenhabRestClient restClient;
public RemoteopenhabBridgeHandler(Bridge bridge, ClientBuilder clientBuilder,
SseEventSourceFactory eventSourceFactory, RemoteopenhabChannelTypeProvider channelTypeProvider,
RemoteopenhabStateDescriptionOptionProvider stateDescriptionProvider, final Gson jsonParser) {
super(bridge);
this.clientBuilder = clientBuilder;
this.eventSourceFactory = eventSourceFactory;
this.channelTypeProvider = channelTypeProvider;
this.stateDescriptionProvider = stateDescriptionProvider;
this.jsonParser = jsonParser;
}
@Override
public void initialize() {
logger.debug("Initializing remote openHAB handler for bridge {}", getThing().getUID());
config = getConfigAs(RemoteopenhabInstanceConfiguration.class);
String host = config.host.trim();
if (host.length() == 0) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
"Undefined server address setting in the thing configuration");
return;
}
List<String> localIpAddresses = NetUtil.getAllInterfaceAddresses().stream()
.filter(a -> !a.getAddress().isLinkLocalAddress())
.map(a -> a.getAddress().getHostAddress().split("%")[0]).collect(Collectors.toList());
if (localIpAddresses.contains(host)) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
"Do not use the local server as a remote server in the thing configuration");
return;
}
String path = config.restPath.trim();
if (path.length() == 0 || !path.startsWith("/")) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
"Invalid REST API path setting in the thing configuration");
return;
}
URL url;
try {
url = new URL("http", host, config.port, path);
} catch (MalformedURLException e) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
"Invalid REST URL built from the settings in the thing configuration");
return;
}
String urlStr = url.toString();
if (urlStr.endsWith("/")) {
urlStr = urlStr.substring(0, urlStr.length() - 1);
}
logger.debug("REST URL = {}", urlStr);
RemoteopenhabRestClient client = new RemoteopenhabRestClient(clientBuilder, eventSourceFactory, jsonParser,
config.token, urlStr);
restClient = client;
updateStatus(ThingStatus.UNKNOWN);
startCheckConnectionJob(client);
}
@Override
public void dispose() {
logger.debug("Disposing remote openHAB handler for bridge {}", getThing().getUID());
stopStreamingUpdates();
stopCheckConnectionJob();
this.restClient = null;
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
if (getThing().getStatus() != ThingStatus.ONLINE) {
return;
}
RemoteopenhabRestClient client = restClient;
if (client == null) {
return;
}
try {
if (command instanceof RefreshType) {
String state = client.getRemoteItemState(channelUID.getId());
updateChannelState(channelUID.getId(), null, state);
} else if (isLinked(channelUID)) {
client.sendCommandToRemoteItem(channelUID.getId(), command);
String commandStr = command.toFullString();
logger.debug("Sending command {} to remote item {} succeeded",
commandStr.length() < MAX_STATE_SIZE_FOR_LOGGING ? commandStr
: commandStr.substring(0, MAX_STATE_SIZE_FOR_LOGGING) + "...",
channelUID.getId());
}
} catch (RemoteopenhabException e) {
logger.debug("{}", e.getMessage());
}
}
private void createChannels(List<Item> items, boolean replace) {
synchronized (updateThingLock) {
int nbGroups = 0;
List<Channel> channels = new ArrayList<>();
for (Item item : items) {
String itemType = item.type;
boolean readOnly = false;
if ("Group".equals(itemType)) {
if (item.groupType.isEmpty()) {
// Standard groups are ignored
nbGroups++;
continue;
} else {
itemType = item.groupType;
}
} else {
if (item.stateDescription != null && item.stateDescription.readOnly) {
readOnly = true;
}
}
String channelTypeId = String.format("item%s%s", itemType.replace(":", ""), readOnly ? "RO" : "");
ChannelTypeUID channelTypeUID = new ChannelTypeUID(BINDING_ID, channelTypeId);
ChannelType channelType = channelTypeProvider.getChannelType(channelTypeUID, null);
String label;
String description;
if (channelType == null) {
logger.trace("Create the channel type {} for item type {}", channelTypeUID, itemType);
label = String.format("Remote %s Item", itemType);
description = String.format("An item of type %s from the remote server.", itemType);
channelType = ChannelTypeBuilder.state(channelTypeUID, label, itemType).withDescription(description)
.withStateDescriptionFragment(
StateDescriptionFragmentBuilder.create().withReadOnly(readOnly).build())
.withAutoUpdatePolicy(AutoUpdatePolicy.VETO).build();
channelTypeProvider.addChannelType(channelType);
}
ChannelUID channelUID = new ChannelUID(getThing().getUID(), item.name);
logger.trace("Create the channel {} of type {}", channelUID, channelTypeUID);
label = "Item " + item.name;
description = String.format("Item %s from the remote server.", item.name);
channels.add(ChannelBuilder.create(channelUID, itemType).withType(channelTypeUID)
.withKind(ChannelKind.STATE).withLabel(label).withDescription(description).build());
}
ThingBuilder thingBuilder = editThing();
if (replace) {
thingBuilder.withChannels(channels);
updateThing(thingBuilder.build());
logger.debug("{} channels defined for the thing {} (from {} items including {} groups)",
channels.size(), getThing().getUID(), items.size(), nbGroups);
} else if (channels.size() > 0) {
int nbRemoved = 0;
for (Channel channel : channels) {
if (getThing().getChannel(channel.getUID()) != null) {
thingBuilder.withoutChannel(channel.getUID());
nbRemoved++;
}
}
if (nbRemoved > 0) {
logger.debug("{} channels removed for the thing {} (from {} items)", nbRemoved, getThing().getUID(),
items.size());
}
for (Channel channel : channels) {
thingBuilder.withChannel(channel);
}
updateThing(thingBuilder.build());
if (nbGroups > 0) {
logger.debug("{} channels added for the thing {} (from {} items including {} groups)",
channels.size(), getThing().getUID(), items.size(), nbGroups);
} else {
logger.debug("{} channels added for the thing {} (from {} items)", channels.size(),
getThing().getUID(), items.size());
}
}
}
}
private void removeChannels(List<Item> items) {
synchronized (updateThingLock) {
int nbRemoved = 0;
ThingBuilder thingBuilder = editThing();
for (Item item : items) {
Channel channel = getThing().getChannel(item.name);
if (channel != null) {
thingBuilder.withoutChannel(channel.getUID());
nbRemoved++;
}
}
if (nbRemoved > 0) {
updateThing(thingBuilder.build());
logger.debug("{} channels removed for the thing {} (from {} items)", nbRemoved, getThing().getUID(),
items.size());
}
}
}
private void setStateOptions(List<Item> items) {
for (Item item : items) {
Channel channel = getThing().getChannel(item.name);
StateDescription descr = item.stateDescription;
List<Option> options = descr == null ? null : descr.options;
if (channel != null && options != null && options.size() > 0) {
List<StateOption> stateOptions = new ArrayList<>();
for (Option option : options) {
stateOptions.add(new StateOption(option.value, option.label));
}
stateDescriptionProvider.setStateOptions(channel.getUID(), stateOptions);
logger.trace("{} options set for the channel {}", options.size(), channel.getUID());
}
}
}
public void checkConnection(RemoteopenhabRestClient client) {
logger.debug("Try the root REST API...");
try {
client.tryApi();
if (client.getRestApiVersion() == null) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
"OH 1.x server not supported by the binding");
} else {
List<Item> items = client.getRemoteItems();
createChannels(items, true);
setStateOptions(items);
for (Item item : items) {
updateChannelState(item.name, null, item.state);
}
updateStatus(ThingStatus.ONLINE);
restartStreamingUpdates();
}
} catch (RemoteopenhabException e) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
stopStreamingUpdates();
}
}
private void startCheckConnectionJob(RemoteopenhabRestClient client) {
ScheduledFuture<?> localCheckConnectionJob = checkConnectionJob;
if (localCheckConnectionJob == null || localCheckConnectionJob.isCancelled()) {
checkConnectionJob = scheduler.scheduleWithFixedDelay(() -> {
long millisSinceLastEvent = System.currentTimeMillis() - client.getLastEventTimestamp();
if (millisSinceLastEvent > CONNECTION_TIMEOUT_MILLIS) {
logger.debug("Check: Disconnected from streaming events, millisSinceLastEvent={}",
millisSinceLastEvent);
checkConnection(client);
} else {
logger.debug("Check: Receiving streaming events, millisSinceLastEvent={}", millisSinceLastEvent);
}
}, 0, CONNECTION_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS);
}
}
private void stopCheckConnectionJob() {
ScheduledFuture<?> localCheckConnectionJob = checkConnectionJob;
if (localCheckConnectionJob != null) {
localCheckConnectionJob.cancel(true);
checkConnectionJob = null;
}
}
private void restartStreamingUpdates() {
RemoteopenhabRestClient client = restClient;
if (client != null) {
synchronized (client) {
stopStreamingUpdates();
startStreamingUpdates();
}
}
}
private void startStreamingUpdates() {
RemoteopenhabRestClient client = restClient;
if (client != null) {
synchronized (client) {
client.addStreamingDataListener(this);
client.start();
}
}
}
private void stopStreamingUpdates() {
RemoteopenhabRestClient client = restClient;
if (client != null) {
synchronized (client) {
client.stop();
client.removeStreamingDataListener(this);
}
}
}
@Override
public void onConnected() {
updateStatus(ThingStatus.ONLINE);
}
@Override
public void onError(String message) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, message);
}
@Override
public void onItemStateEvent(String itemName, String stateType, String state) {
updateChannelState(itemName, stateType, state);
}
@Override
public void onItemAdded(Item item) {
createChannels(List.of(item), false);
}
@Override
public void onItemRemoved(Item item) {
removeChannels(List.of(item));
}
@Override
public void onItemUpdated(Item newItem, Item oldItem) {
if (!newItem.type.equals(oldItem.type)) {
createChannels(List.of(newItem), false);
} else {
logger.trace("Updated remote item {} ignored because item type {} is unchanged", newItem.name,
newItem.type);
}
}
private void updateChannelState(String itemName, @Nullable String stateType, String state) {
Channel channel = getThing().getChannel(itemName);
if (channel == null) {
logger.trace("No channel for item {}", itemName);
return;
}
String acceptedItemType = channel.getAcceptedItemType();
if (acceptedItemType == null) {
logger.trace("Channel without accepted item type for item {}", itemName);
return;
}
if (!isLinked(channel.getUID())) {
logger.trace("Unlinked channel {}", channel.getUID());
return;
}
State channelState = null;
if (stateType == null && "NULL".equals(state)) {
channelState = UnDefType.NULL;
} else if (stateType == null && "UNDEF".equals(state)) {
channelState = UnDefType.UNDEF;
} else if ("UnDef".equals(stateType)) {
switch (state) {
case "NULL":
channelState = UnDefType.NULL;
break;
case "UNDEF":
channelState = UnDefType.UNDEF;
break;
default:
logger.debug("Invalid UnDef value {} for item {}", state, itemName);
break;
}
} else if (acceptedItemType.startsWith(CoreItemFactory.NUMBER + ":")) {
// Item type Number with dimension
if (checkStateType(itemName, stateType, "Quantity")) {
List<Class<? extends State>> stateTypes = Collections.singletonList(QuantityType.class);
channelState = TypeParser.parseState(stateTypes, state);
}
} else {
switch (acceptedItemType) {
case CoreItemFactory.STRING:
if (checkStateType(itemName, stateType, "String")) {
channelState = new StringType(state);
}
break;
case CoreItemFactory.NUMBER:
if (checkStateType(itemName, stateType, "Decimal")) {
channelState = new DecimalType(state);
}
break;
case CoreItemFactory.SWITCH:
if (checkStateType(itemName, stateType, "OnOff")) {
channelState = "ON".equals(state) ? OnOffType.ON : OnOffType.OFF;
}
break;
case CoreItemFactory.CONTACT:
if (checkStateType(itemName, stateType, "OpenClosed")) {
channelState = "OPEN".equals(state) ? OpenClosedType.OPEN : OpenClosedType.CLOSED;
}
break;
case CoreItemFactory.DIMMER:
if (checkStateType(itemName, stateType, "Percent")) {
channelState = new PercentType(state);
}
break;
case CoreItemFactory.COLOR:
if (checkStateType(itemName, stateType, "HSB")) {
channelState = HSBType.valueOf(state);
}
break;
case CoreItemFactory.DATETIME:
if (checkStateType(itemName, stateType, "DateTime")) {
try {
channelState = new DateTimeType(ZonedDateTime.parse(state, FORMATTER_DATE));
} catch (DateTimeParseException e) {
logger.debug("Failed to parse date {} for item {}", state, itemName);
channelState = null;
}
}
break;
case CoreItemFactory.LOCATION:
if (checkStateType(itemName, stateType, "Point")) {
channelState = new PointType(state);
}
break;
case CoreItemFactory.IMAGE:
if (checkStateType(itemName, stateType, "Raw")) {
channelState = RawType.valueOf(state);
}
break;
case CoreItemFactory.PLAYER:
if (checkStateType(itemName, stateType, "PlayPause")) {
switch (state) {
case "PLAY":
channelState = PlayPauseType.PLAY;
break;
case "PAUSE":
channelState = PlayPauseType.PAUSE;
break;
default:
logger.debug("Unexpected value {} for item {}", state, itemName);
break;
}
}
break;
case CoreItemFactory.ROLLERSHUTTER:
if (checkStateType(itemName, stateType, "Percent")) {
channelState = new PercentType(state);
}
break;
default:
logger.debug("Item type {} is not yet supported", acceptedItemType);
break;
}
}
if (channelState != null) {
updateState(channel.getUID(), channelState);
String channelStateStr = channelState.toFullString();
logger.debug("updateState {} with {}", channel.getUID(),
channelStateStr.length() < MAX_STATE_SIZE_FOR_LOGGING ? channelStateStr
: channelStateStr.substring(0, MAX_STATE_SIZE_FOR_LOGGING) + "...");
}
}
private boolean checkStateType(String itemName, @Nullable String stateType, String expectedType) {
if (stateType != null && !expectedType.equals(stateType)) {
logger.debug("Unexpected value type {} for item {}", stateType, itemName);
return false;
} else {
return true;
}
}
}

View File

@ -0,0 +1,56 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.remoteopenhab.internal.listener;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.remoteopenhab.internal.data.Item;
import org.openhab.binding.remoteopenhab.internal.rest.RemoteopenhabRestClient;
/**
* Interface for listeners of events generated by the {@link RemoteopenhabRestClient}.
*
* @author Laurent Garnier - Initial contribution
*/
@NonNullByDefault
public interface RemoteopenhabStreamingDataListener {
/**
* The client successfully established a connection.
*/
void onConnected();
/**
* An error message was published.
*/
void onError(String message);
/**
* A new ItemStateEvent was published.
*/
void onItemStateEvent(String itemName, String stateType, String state);
/**
* A new ItemAddedEvent was published.
*/
void onItemAdded(Item item);
/**
* A new ItemRemovedEvent was published.
*/
void onItemRemoved(Item item);
/**
* A new ItemUpdatedEvent was published.
*/
void onItemUpdated(Item newItem, Item oldItem);
}

View File

@ -0,0 +1,336 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.remoteopenhab.internal.rest;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.List;
import java.util.Properties;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.TimeUnit;
import javax.ws.rs.client.Client;
import javax.ws.rs.client.ClientBuilder;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.sse.InboundSseEvent;
import javax.ws.rs.sse.SseEventSource;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.remoteopenhab.internal.data.Event;
import org.openhab.binding.remoteopenhab.internal.data.EventPayload;
import org.openhab.binding.remoteopenhab.internal.data.Item;
import org.openhab.binding.remoteopenhab.internal.data.RestApi;
import org.openhab.binding.remoteopenhab.internal.exceptions.RemoteopenhabException;
import org.openhab.binding.remoteopenhab.internal.listener.RemoteopenhabStreamingDataListener;
import org.openhab.core.io.net.http.HttpUtil;
import org.openhab.core.types.Command;
import org.osgi.service.jaxrs.client.SseEventSourceFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.gson.Gson;
import com.google.gson.JsonSyntaxException;
/**
* A client to use the openHAB REST API and to receive/parse events received from the openHAB REST API Server-Sent
* Events (SSE).
*
* @author Laurent Garnier - Initial contribution
*/
@NonNullByDefault
public class RemoteopenhabRestClient {
private static final int REQUEST_TIMEOUT = (int) TimeUnit.SECONDS.toMillis(30);
private final Logger logger = LoggerFactory.getLogger(RemoteopenhabRestClient.class);
private final ClientBuilder clientBuilder;
private final SseEventSourceFactory eventSourceFactory;
private final Gson jsonParser;
private String accessToken;
private final String restUrl;
private final Object startStopLock = new Object();
private final List<RemoteopenhabStreamingDataListener> listeners = new CopyOnWriteArrayList<>();
private @Nullable String restApiVersion;
private @Nullable String restApiItems;
private @Nullable String restApiEvents;
private @Nullable String topicNamespace;
private boolean connected;
private @Nullable SseEventSource eventSource;
private long lastEventTimestamp;
public RemoteopenhabRestClient(final ClientBuilder clientBuilder, final SseEventSourceFactory eventSourceFactory,
final Gson jsonParser, final String accessToken, final String restUrl) {
this.clientBuilder = clientBuilder;
this.eventSourceFactory = eventSourceFactory;
this.jsonParser = jsonParser;
this.accessToken = accessToken;
this.restUrl = restUrl;
}
public void tryApi() throws RemoteopenhabException {
try {
Properties httpHeaders = new Properties();
if (!accessToken.isEmpty()) {
httpHeaders.put(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken);
}
httpHeaders.put(HttpHeaders.ACCEPT, "application/json");
String jsonResponse = HttpUtil.executeUrl("GET", restUrl, httpHeaders, null, null, REQUEST_TIMEOUT);
if (jsonResponse.isEmpty()) {
throw new RemoteopenhabException("Failed to execute the root REST API");
}
RestApi restApi = jsonParser.fromJson(jsonResponse, RestApi.class);
restApiVersion = restApi.version;
logger.debug("REST API version = {}", restApiVersion);
restApiItems = null;
for (int i = 0; i < restApi.links.length; i++) {
if ("items".equals(restApi.links[i].type)) {
restApiItems = restApi.links[i].url;
} else if ("events".equals(restApi.links[i].type)) {
restApiEvents = restApi.links[i].url;
}
}
logger.debug("REST API items = {}", restApiItems);
logger.debug("REST API events = {}", restApiEvents);
topicNamespace = restApi.runtimeInfo != null ? "openhab" : "smarthome";
logger.debug("topic namespace = {}", topicNamespace);
} catch (RemoteopenhabException e) {
throw new RemoteopenhabException(e.getMessage());
} catch (JsonSyntaxException e) {
throw new RemoteopenhabException("Failed to parse the result of the root REST API", e);
} catch (IOException e) {
throw new RemoteopenhabException("Failed to execute the root REST API", e);
}
}
public List<Item> getRemoteItems() throws RemoteopenhabException {
try {
Properties httpHeaders = new Properties();
if (!accessToken.isEmpty()) {
httpHeaders.put(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken);
}
httpHeaders.put(HttpHeaders.ACCEPT, "application/json");
String url = String.format("%s?recursive=fasle", getRestApiItems());
String jsonResponse = HttpUtil.executeUrl("GET", url, httpHeaders, null, null, REQUEST_TIMEOUT);
return Arrays.asList(jsonParser.fromJson(jsonResponse, Item[].class));
} catch (IOException | JsonSyntaxException e) {
throw new RemoteopenhabException("Failed to get the list of remote items using the items REST API", e);
}
}
public String getRemoteItemState(String itemName) throws RemoteopenhabException {
try {
Properties httpHeaders = new Properties();
if (!accessToken.isEmpty()) {
httpHeaders.put(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken);
}
httpHeaders.put(HttpHeaders.ACCEPT, "text/plain");
String url = String.format("%s/%s/state", getRestApiItems(), itemName);
return HttpUtil.executeUrl("GET", url, httpHeaders, null, null, REQUEST_TIMEOUT);
} catch (IOException e) {
throw new RemoteopenhabException(
"Failed to get the state of remote item " + itemName + " using the items REST API", e);
}
}
public void sendCommandToRemoteItem(String itemName, Command command) throws RemoteopenhabException {
try {
Properties httpHeaders = new Properties();
if (!accessToken.isEmpty()) {
httpHeaders.put(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken);
}
httpHeaders.put(HttpHeaders.ACCEPT, "application/json");
String url = String.format("%s/%s", getRestApiItems(), itemName);
InputStream stream = new ByteArrayInputStream(command.toFullString().getBytes(StandardCharsets.UTF_8));
HttpUtil.executeUrl("POST", url, httpHeaders, stream, "text/plain", REQUEST_TIMEOUT);
stream.close();
} catch (IOException e) {
throw new RemoteopenhabException(
"Failed to send command to the remote item " + itemName + " using the items REST API", e);
}
}
public @Nullable String getRestApiVersion() {
return restApiVersion;
}
public String getRestApiItems() {
String url = restApiItems;
return url != null ? url : restUrl + "/items";
}
public String getRestApiEvents() {
String url = restApiEvents;
return url != null ? url : restUrl + "/events";
}
public String getTopicNamespace() {
String namespace = topicNamespace;
return namespace != null ? namespace : "openhab";
}
public void start() {
synchronized (startStopLock) {
logger.debug("Opening EventSource {}", getItemsRestSseUrl());
reopenEventSource();
logger.debug("EventSource started");
}
}
public void stop() {
synchronized (startStopLock) {
logger.debug("Closing EventSource {}", getItemsRestSseUrl());
closeEventSource(0, TimeUnit.SECONDS);
logger.debug("EventSource stopped");
}
}
private String getItemsRestSseUrl() {
return String.format("%s?topics=%s/items/*/*", getRestApiEvents(), getTopicNamespace());
}
private SseEventSource createEventSource(String restSseUrl) {
Client client = clientBuilder.register(new RemoteopenhabStreamingRequestFilter(accessToken)).build();
SseEventSource eventSource = eventSourceFactory.newSource(client.target(restSseUrl));
eventSource.register(this::onEvent, this::onError);
return eventSource;
}
private void reopenEventSource() {
logger.debug("Reopening EventSource");
closeEventSource(10, TimeUnit.SECONDS);
logger.debug("Opening new EventSource {}", getItemsRestSseUrl());
SseEventSource localEventSource = createEventSource(getItemsRestSseUrl());
localEventSource.open();
eventSource = localEventSource;
}
private void closeEventSource(long timeout, TimeUnit timeoutUnit) {
SseEventSource localEventSource = eventSource;
if (localEventSource != null) {
if (!localEventSource.isOpen()) {
logger.debug("Existing EventSource is already closed");
} else if (localEventSource.close(timeout, timeoutUnit)) {
logger.debug("Succesfully closed existing EventSource");
} else {
logger.debug("Failed to close existing EventSource");
}
eventSource = null;
}
connected = false;
}
public boolean addStreamingDataListener(RemoteopenhabStreamingDataListener listener) {
return listeners.add(listener);
}
public boolean removeStreamingDataListener(RemoteopenhabStreamingDataListener listener) {
return listeners.remove(listener);
}
public long getLastEventTimestamp() {
return lastEventTimestamp;
}
private void onEvent(InboundSseEvent inboundEvent) {
String name = inboundEvent.getName();
String data = inboundEvent.readData();
logger.trace("Received event name {} date {}", name, data);
lastEventTimestamp = System.currentTimeMillis();
if (!connected) {
logger.debug("Connected to streaming events");
connected = true;
listeners.forEach(listener -> listener.onConnected());
}
if (!"message".equals(name)) {
logger.debug("Received unhandled event with name '{}' and data '{}'", name, data);
return;
}
try {
Event event = jsonParser.fromJson(data, Event.class);
String itemName;
EventPayload payload;
Item item;
switch (event.type) {
case "ItemStateEvent":
itemName = extractItemNameFromTopic(event.topic, event.type, "state");
payload = jsonParser.fromJson(event.payload, EventPayload.class);
listeners.forEach(listener -> listener.onItemStateEvent(itemName, payload.type, payload.value));
break;
case "GroupItemStateChangedEvent":
itemName = extractItemNameFromTopic(event.topic, event.type, "statechanged");
payload = jsonParser.fromJson(event.payload, EventPayload.class);
listeners.forEach(listener -> listener.onItemStateEvent(itemName, payload.type, payload.value));
break;
case "ItemAddedEvent":
itemName = extractItemNameFromTopic(event.topic, event.type, "added");
item = jsonParser.fromJson(event.payload, Item.class);
listeners.forEach(listener -> listener.onItemAdded(item));
break;
case "ItemRemovedEvent":
itemName = extractItemNameFromTopic(event.topic, event.type, "removed");
item = jsonParser.fromJson(event.payload, Item.class);
listeners.forEach(listener -> listener.onItemRemoved(item));
break;
case "ItemUpdatedEvent":
itemName = extractItemNameFromTopic(event.topic, event.type, "updated");
Item[] updItem = jsonParser.fromJson(event.payload, Item[].class);
if (updItem.length == 2) {
listeners.forEach(listener -> listener.onItemUpdated(updItem[0], updItem[1]));
} else {
logger.debug("Invalid payload for event type {} for topic {}", event.type, event.topic);
}
break;
case "ItemStatePredictedEvent":
case "ItemStateChangedEvent":
case "ItemCommandEvent":
logger.trace("Ignored event type {} for topic {}", event.type, event.topic);
break;
default:
logger.debug("Unexpected event type {} for topic {}", event.type, event.topic);
break;
}
} catch (RemoteopenhabException | JsonSyntaxException e) {
logger.debug("An exception occurred while processing the inbound '{}' event containg data: {}", name, data,
e);
}
}
private void onError(Throwable error) {
logger.debug("Error occurred while receiving events", error);
listeners.forEach(listener -> listener.onError("Error occurred while receiving events"));
}
private String extractItemNameFromTopic(String topic, String eventType, String finalPart)
throws RemoteopenhabException {
String[] parts = topic.split("/");
int expectedNbParts = "GroupItemStateChangedEvent".equals(eventType) ? 5 : 4;
if (parts.length != expectedNbParts || !getTopicNamespace().equals(parts[0]) || !"items".equals(parts[1])
|| !finalPart.equals(parts[parts.length - 1])) {
throw new RemoteopenhabException("Invalid event topic " + topic + " for event type " + eventType);
}
return parts[2];
}
}

View File

@ -0,0 +1,49 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.remoteopenhab.internal.rest;
import java.io.IOException;
import javax.ws.rs.client.ClientRequestContext;
import javax.ws.rs.client.ClientRequestFilter;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.MultivaluedMap;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* Inserts Authorization and Cache-Control headers for requests on the streaming REST API.
*
* @author Laurent Garnier - Initial contribution
*/
@NonNullByDefault
public class RemoteopenhabStreamingRequestFilter implements ClientRequestFilter {
private final String accessToken;
public RemoteopenhabStreamingRequestFilter(String accessToken) {
this.accessToken = accessToken;
}
@Override
public void filter(@Nullable ClientRequestContext requestContext) throws IOException {
if (requestContext != null) {
MultivaluedMap<String, Object> headers = requestContext.getHeaders();
if (!accessToken.isEmpty()) {
headers.putSingle(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken);
}
headers.putSingle(HttpHeaders.CACHE_CONTROL, "no-cache");
}
}
}

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<binding:binding id="remoteopenhab" 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>Remote openHAB Binding</name>
<description>The Remote openHAB binding allows to communicate with remote openHAB servers.</description>
<author>Laurent Garnier</author>
</binding:binding>

View File

@ -0,0 +1,47 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="remoteopenhab"
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">
<bridge-type id="server">
<label>Remote openHAB Server</label>
<description>A remote openHAB server.</description>
<representation-property>host</representation-property>
<config-description>
<parameter name="host" type="text">
<context>network-address</context>
<label>Server Address</label>
<description>The host name or IP address of the remote openHAB server.</description>
<required>true</required>
</parameter>
<parameter name="port" type="integer">
<label>Server HTTP Port</label>
<description>The HTTP port to be used to communicate with the remote openHAB server.</description>
<required>true</required>
<default>8080</default>
<advanced>true</advanced>
</parameter>
<parameter name="restPath" type="text">
<label>REST API Path</label>
<description>The subpath of the REST API on the remote openHAB server.</description>
<required>true</required>
<default>/rest</default>
<advanced>true</advanced>
</parameter>
<parameter name="token" type="text">
<context>password</context>
<label>Token</label>
<description>The token to use when the remote openHAB server is setup to require authorization to run its REST API.</description>
<required>false</required>
<advanced>true</advanced>
</parameter>
</config-description>
</bridge-type>
</thing:thing-descriptions>

View File

@ -232,6 +232,7 @@
<module>org.openhab.binding.pushbullet</module>
<module>org.openhab.binding.radiothermostat</module>
<module>org.openhab.binding.regoheatpump</module>
<module>org.openhab.binding.remoteopenhab</module>
<module>org.openhab.binding.rfxcom</module>
<module>org.openhab.binding.rme</module>
<module>org.openhab.binding.robonect</module>