mirror of
https://github.com/openhab/openhab-addons.git
synced 2025-01-10 15:11:59 +01:00
[remoteopenhab] Remote openHAB binding - initial contributionn (#8791)
Fix #8407 Signed-off-by: Laurent Garnier <lg.hc@free.fr>
This commit is contained in:
parent
08405233ae
commit
4646ea68c3
@ -200,6 +200,7 @@
|
|||||||
/bundles/org.openhab.binding.pushbullet/ @hakan42
|
/bundles/org.openhab.binding.pushbullet/ @hakan42
|
||||||
/bundles/org.openhab.binding.radiothermostat/ @mlobstein
|
/bundles/org.openhab.binding.radiothermostat/ @mlobstein
|
||||||
/bundles/org.openhab.binding.regoheatpump/ @crnjan
|
/bundles/org.openhab.binding.regoheatpump/ @crnjan
|
||||||
|
/bundles/org.openhab.binding.remoteopenhab/ @lolodomo
|
||||||
/bundles/org.openhab.binding.rfxcom/ @martinvw @paulianttila
|
/bundles/org.openhab.binding.rfxcom/ @martinvw @paulianttila
|
||||||
/bundles/org.openhab.binding.rme/ @kgoderis
|
/bundles/org.openhab.binding.rme/ @kgoderis
|
||||||
/bundles/org.openhab.binding.robonect/ @reyem
|
/bundles/org.openhab.binding.robonect/ @reyem
|
||||||
|
@ -991,6 +991,11 @@
|
|||||||
<artifactId>org.openhab.binding.regoheatpump</artifactId>
|
<artifactId>org.openhab.binding.regoheatpump</artifactId>
|
||||||
<version>${project.version}</version>
|
<version>${project.version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.openhab.addons.bundles</groupId>
|
||||||
|
<artifactId>org.openhab.binding.remoteopenhab</artifactId>
|
||||||
|
<version>${project.version}</version>
|
||||||
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.openhab.addons.bundles</groupId>
|
<groupId>org.openhab.addons.bundles</groupId>
|
||||||
<artifactId>org.openhab.binding.rfxcom</artifactId>
|
<artifactId>org.openhab.binding.rfxcom</artifactId>
|
||||||
|
13
bundles/org.openhab.binding.remoteopenhab/NOTICE
Normal file
13
bundles/org.openhab.binding.remoteopenhab/NOTICE
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
This content is produced and maintained by the openHAB project.
|
||||||
|
|
||||||
|
* Project home: https://www.openhab.org
|
||||||
|
|
||||||
|
== Declared Project Licenses
|
||||||
|
|
||||||
|
This program and the accompanying materials are made available under the terms
|
||||||
|
of the Eclipse Public License 2.0 which is available at
|
||||||
|
https://www.eclipse.org/legal/epl-2.0/.
|
||||||
|
|
||||||
|
== Source Code
|
||||||
|
|
||||||
|
https://github.com/openhab/openhab-addons
|
66
bundles/org.openhab.binding.remoteopenhab/README.md
Normal file
66
bundles/org.openhab.binding.remoteopenhab/README.md
Normal 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" }
|
||||||
|
```
|
17
bundles/org.openhab.binding.remoteopenhab/pom.xml
Normal file
17
bundles/org.openhab.binding.remoteopenhab/pom.xml
Normal 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>
|
@ -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>
|
@ -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);
|
||||||
|
}
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
@ -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 = "";
|
||||||
|
}
|
@ -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 = "";
|
||||||
|
}
|
@ -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 = "";
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
@ -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 = "";
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
@ -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 = "";
|
||||||
|
}
|
@ -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 = "";
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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);
|
||||||
|
}
|
@ -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];
|
||||||
|
}
|
||||||
|
}
|
@ -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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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>
|
@ -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>
|
@ -232,6 +232,7 @@
|
|||||||
<module>org.openhab.binding.pushbullet</module>
|
<module>org.openhab.binding.pushbullet</module>
|
||||||
<module>org.openhab.binding.radiothermostat</module>
|
<module>org.openhab.binding.radiothermostat</module>
|
||||||
<module>org.openhab.binding.regoheatpump</module>
|
<module>org.openhab.binding.regoheatpump</module>
|
||||||
|
<module>org.openhab.binding.remoteopenhab</module>
|
||||||
<module>org.openhab.binding.rfxcom</module>
|
<module>org.openhab.binding.rfxcom</module>
|
||||||
<module>org.openhab.binding.rme</module>
|
<module>org.openhab.binding.rme</module>
|
||||||
<module>org.openhab.binding.robonect</module>
|
<module>org.openhab.binding.robonect</module>
|
||||||
|
Loading…
Reference in New Issue
Block a user