[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
28 changed files with 1804 additions and 0 deletions

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>