[hdpowerview] Add support for Generation 3 (#13355)
Signed-off-by: Andrew Fiddian-Green <software@whitebear.ch>
This commit is contained in:
committed by
GitHub
parent
96d0293913
commit
df9c270acf
@@ -0,0 +1,439 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2023 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.hdpowerview.internal;
|
||||
|
||||
import java.io.Closeable;
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
|
||||
import javax.net.ssl.HostnameVerifier;
|
||||
import javax.net.ssl.SSLSession;
|
||||
import javax.ws.rs.client.Client;
|
||||
import javax.ws.rs.client.ClientBuilder;
|
||||
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.eclipse.jetty.client.HttpClient;
|
||||
import org.eclipse.jetty.client.api.ContentResponse;
|
||||
import org.eclipse.jetty.client.api.Request;
|
||||
import org.eclipse.jetty.client.util.StringContentProvider;
|
||||
import org.eclipse.jetty.http.HttpHeader;
|
||||
import org.eclipse.jetty.http.HttpMethod;
|
||||
import org.eclipse.jetty.http.HttpStatus;
|
||||
import org.openhab.binding.hdpowerview.internal.HDPowerViewWebTargets.Query;
|
||||
import org.openhab.binding.hdpowerview.internal.dto.gen3.Info;
|
||||
import org.openhab.binding.hdpowerview.internal.dto.gen3.Scene;
|
||||
import org.openhab.binding.hdpowerview.internal.dto.gen3.Shade;
|
||||
import org.openhab.binding.hdpowerview.internal.dto.gen3.ShadeEvent;
|
||||
import org.openhab.binding.hdpowerview.internal.dto.gen3.ShadePosition;
|
||||
import org.openhab.binding.hdpowerview.internal.dto.requests.ShadeMotion;
|
||||
import org.openhab.binding.hdpowerview.internal.exceptions.HubProcessingException;
|
||||
import org.openhab.binding.hdpowerview.internal.handler.GatewayBridgeHandler;
|
||||
import org.openhab.core.thing.Thing;
|
||||
import org.osgi.service.jaxrs.client.SseEventSourceFactory;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.JsonParseException;
|
||||
|
||||
/**
|
||||
* JAX-RS targets for communicating with an HD PowerView Generation 3 Gateway.
|
||||
*
|
||||
* @author Andrew Fiddian-Green - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class GatewayWebTargets implements Closeable, HostnameVerifier {
|
||||
|
||||
private static final String IDS = "ids";
|
||||
private static final int SLEEP_SECONDS = 360;
|
||||
private static final Set<Integer> HTTP_OK_CODES = Set.of(HttpStatus.OK_200, HttpStatus.NO_CONTENT_204);
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(GatewayWebTargets.class);
|
||||
private final Gson jsonParser = new Gson();
|
||||
|
||||
private final String shades;
|
||||
private final String scenes;
|
||||
private final String sceneActivate;
|
||||
private final String shadeMotion;
|
||||
private final String shadePositions;
|
||||
private final String shadeSingle;
|
||||
private final String shadeStop;
|
||||
private final String info;
|
||||
private final String register;
|
||||
private final String shadeEvents;
|
||||
private final HttpClient httpClient;
|
||||
private final ClientBuilder clientBuilder;
|
||||
private final SseEventSourceFactory eventSourceFactory;
|
||||
private final GatewayBridgeHandler hubHandler;
|
||||
private final String ipAddress;
|
||||
|
||||
private boolean registered;
|
||||
private boolean closing;
|
||||
|
||||
private @Nullable SseEventSource shadeEventSource;
|
||||
private @Nullable ScheduledFuture<?> sseQuietCheck;
|
||||
|
||||
/**
|
||||
* Initialize the web targets
|
||||
*
|
||||
* @param httpClient the HTTP client (the binding)
|
||||
* @param ipAddress the IP address of the server (the hub)
|
||||
*/
|
||||
public GatewayWebTargets(GatewayBridgeHandler hubHandler, HttpClient httpClient, ClientBuilder clientBuilder,
|
||||
SseEventSourceFactory eventSourceFactory, String ipAddress) {
|
||||
this.ipAddress = ipAddress;
|
||||
this.httpClient = httpClient;
|
||||
this.clientBuilder = clientBuilder;
|
||||
this.eventSourceFactory = eventSourceFactory;
|
||||
this.hubHandler = hubHandler;
|
||||
|
||||
String base = "http://" + ipAddress + "/";
|
||||
String home = base + "home/";
|
||||
shades = home + "shades";
|
||||
scenes = home + "scenes";
|
||||
sceneActivate = home + "scenes/%d/activate";
|
||||
shadeMotion = home + "shades/%d/motion";
|
||||
shadePositions = home + "shades/positions";
|
||||
shadeSingle = home + "shades/%d";
|
||||
shadeStop = home + "shades/stop";
|
||||
shadeEvents = home + "shades/events?sse=true";
|
||||
info = base + "gateway/info";
|
||||
|
||||
/*
|
||||
* Hunter Douglas keeps a statistical count of systems (e.g. openHAB, Home Assistant, Amazon etc.) that are
|
||||
* using their Generation 3 REST API to connect to their gateways. So we are asked to register with the gateway
|
||||
* on startup. => So do not change the 'openhab.org' tag below !!
|
||||
*/
|
||||
register = home + "integration/openhab.org";
|
||||
}
|
||||
|
||||
/**
|
||||
* Issue a command to activate a scene.
|
||||
*
|
||||
* @param sceneId the scene to be activated.
|
||||
* @throws HubProcessingException if any error occurs.
|
||||
*/
|
||||
public void activateScene(int sceneId) throws HubProcessingException {
|
||||
invoke(HttpMethod.PUT, String.format(sceneActivate, sceneId), null, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
closing = true;
|
||||
sseClose();
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the binding with the hub (if not already registered).
|
||||
*
|
||||
* @throws HubProcessingException if any error occurs.
|
||||
*/
|
||||
public boolean gatewayRegister() throws HubProcessingException {
|
||||
if (!registered) {
|
||||
invoke(HttpMethod.PUT, register, null, null);
|
||||
registered = true;
|
||||
}
|
||||
return registered;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get hub properties.
|
||||
*
|
||||
* @return a map containing the hub properties.
|
||||
* @throws HubProcessingException if any error occurs.
|
||||
*/
|
||||
public Map<String, String> getInformation() throws HubProcessingException {
|
||||
String json = invoke(HttpMethod.GET, info, null, null);
|
||||
try {
|
||||
Info result = jsonParser.fromJson(json, Info.class);
|
||||
if (result == null) {
|
||||
throw new HubProcessingException("getInformation(): missing response");
|
||||
}
|
||||
return Map.of( //
|
||||
Thing.PROPERTY_FIRMWARE_VERSION, result.getFwVersion(), //
|
||||
Thing.PROPERTY_SERIAL_NUMBER, result.getSerialNumber());
|
||||
} catch (JsonParseException e) {
|
||||
throw new HubProcessingException("getFirmwareVersions(): JsonParseException");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the list of scenes.
|
||||
*
|
||||
* @return the list of scenes.
|
||||
* @throws HubProcessingException if any error occurs.
|
||||
*/
|
||||
public List<Scene> getScenes() throws HubProcessingException {
|
||||
String json = invoke(HttpMethod.GET, scenes, null, null);
|
||||
try {
|
||||
return List.of(jsonParser.fromJson(json, Scene[].class));
|
||||
} catch (JsonParseException e) {
|
||||
throw new HubProcessingException("getScenes() JsonParseException");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the data for a single shade.
|
||||
*
|
||||
* @param shadeId the id of the shade to get.
|
||||
* @return the shade.
|
||||
* @throws HubProcessingException if any error occurs.
|
||||
*/
|
||||
public Shade getShade(int shadeId) throws HubProcessingException {
|
||||
String json = invoke(HttpMethod.GET, String.format(shadeSingle, shadeId), null, null);
|
||||
try {
|
||||
Shade result = jsonParser.fromJson(json, Shade.class);
|
||||
if (result == null) {
|
||||
throw new HubProcessingException("getShade() missing response");
|
||||
}
|
||||
return result;
|
||||
} catch (JsonParseException e) {
|
||||
throw new HubProcessingException("getShade() JsonParseException");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the list of shades.
|
||||
*
|
||||
* @return the list of shades.
|
||||
* @throws HubProcessingException if any error occurs.
|
||||
*/
|
||||
public List<Shade> getShades() throws HubProcessingException {
|
||||
String json = invoke(HttpMethod.GET, shades, null, null);
|
||||
try {
|
||||
return List.of(jsonParser.fromJson(json, Shade[].class));
|
||||
} catch (JsonParseException e) {
|
||||
throw new HubProcessingException("getShades() JsonParseException");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoke a call on the hub server to retrieve information or send a command.
|
||||
*
|
||||
* @param method GET or PUT.
|
||||
* @param url the host URL to be called.
|
||||
* @param query the HTTP query parameter.
|
||||
* @param jsonCommand the request command content (as a JSON string).
|
||||
* @return the response content (as a JSON string).
|
||||
* @throws HubProcessingException if something goes wrong.
|
||||
*/
|
||||
protected synchronized String invoke(HttpMethod method, String url, @Nullable Query query,
|
||||
@Nullable String jsonCommand) throws HubProcessingException {
|
||||
if (logger.isTraceEnabled()) {
|
||||
if (query != null) {
|
||||
logger.trace("invoke() method:{}, url:{}, query:{}", method, url, query);
|
||||
} else {
|
||||
logger.trace("invoke() method:{}, url:{}", method, url);
|
||||
}
|
||||
if (jsonCommand != null) {
|
||||
logger.trace("invoke() request JSON:{}", jsonCommand);
|
||||
}
|
||||
}
|
||||
Request request = httpClient.newRequest(url).method(method).header("Connection", "close").accept("*/*");
|
||||
if (query != null) {
|
||||
request.param(query.getKey(), query.getValue());
|
||||
}
|
||||
if (jsonCommand != null) {
|
||||
request.header(HttpHeader.CONTENT_TYPE, "application/json").content(new StringContentProvider(jsonCommand));
|
||||
}
|
||||
ContentResponse response;
|
||||
try {
|
||||
response = request.send();
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
throw new HubProcessingException(String.format("%s: \"%s\"", e.getClass().getName(), e.getMessage()));
|
||||
} catch (TimeoutException | ExecutionException e) {
|
||||
throw new HubProcessingException(String.format("%s: \"%s\"", e.getClass().getName(), e.getMessage()));
|
||||
}
|
||||
int statusCode = response.getStatus();
|
||||
if (!HTTP_OK_CODES.contains(statusCode)) {
|
||||
throw new HubProcessingException(String.format("HTTP %d error", statusCode));
|
||||
}
|
||||
String jsonResponse = response.getContentAsString();
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace("invoke() response JSON:{}", jsonResponse);
|
||||
}
|
||||
if (method == HttpMethod.GET && jsonResponse.isEmpty()) {
|
||||
throw new HubProcessingException("Empty response entity");
|
||||
}
|
||||
return jsonResponse;
|
||||
}
|
||||
|
||||
/**
|
||||
* Issue a jog command to a shade.
|
||||
*
|
||||
* @param shadeId the shade to be jogged.
|
||||
* @throws HubProcessingException if any error occurs.
|
||||
*/
|
||||
public void jogShade(int shadeId) throws HubProcessingException {
|
||||
String json = jsonParser.toJson(new ShadeMotion(ShadeMotion.Type.JOG));
|
||||
invoke(HttpMethod.PUT, String.format(shadeMotion, shadeId), null, json);
|
||||
}
|
||||
|
||||
/**
|
||||
* Issue a command to move a shade.
|
||||
*
|
||||
* @param shadeId the shade to be moved.
|
||||
* @param shade DTO with the new position.
|
||||
* @throws HubProcessingException if any error occurs.
|
||||
*/
|
||||
public void moveShade(int shadeId, Shade shade) throws HubProcessingException {
|
||||
invoke(HttpMethod.PUT, shadePositions, Query.of(IDS, Integer.toString(shadeId)), jsonParser.toJson(shade));
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the SSE event channel has not received any events for a long time. This could mean that the event
|
||||
* source socket has dropped. So restart the SSE connection.
|
||||
*/
|
||||
public void onSseQuiet() {
|
||||
if (!closing) {
|
||||
sseReOpen();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle SSE errors. For the time being just log them, because the framework should automatically recover itself.
|
||||
*
|
||||
* @param e the error that was thrown.
|
||||
*/
|
||||
private void onSseError(Throwable e) {
|
||||
if (!closing) {
|
||||
logger.debug("onSseShadeError() {}", e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle inbound shade SSE events.
|
||||
*
|
||||
* @param sseEvent the inbound event.
|
||||
*/
|
||||
private void onSSeEvent(InboundSseEvent sseEvent) {
|
||||
if (closing) {
|
||||
return;
|
||||
}
|
||||
ScheduledFuture<?> task = sseQuietCheck;
|
||||
if (task != null && !task.isCancelled()) {
|
||||
task.cancel(true);
|
||||
}
|
||||
sseQuietCheck = hubHandler.getScheduler().schedule(this::onSseQuiet, SLEEP_SECONDS, TimeUnit.SECONDS);
|
||||
if (sseEvent.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
String json = sseEvent.readData();
|
||||
if (json == null) {
|
||||
return;
|
||||
}
|
||||
logger.trace("onSseShadeEvent() json:{}", json);
|
||||
ShadeEvent shadeEvent = jsonParser.fromJson(json, ShadeEvent.class);
|
||||
if (shadeEvent != null) {
|
||||
ShadePosition positions = shadeEvent.getCurrentPositions();
|
||||
hubHandler
|
||||
.onShadeEvent(new Shade().setId(shadeEvent.getId()).setShadePosition(positions).setPartialState());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Close the SSE links.
|
||||
*/
|
||||
private synchronized void sseClose() {
|
||||
logger.debug("sseClose() called");
|
||||
ScheduledFuture<?> task = sseQuietCheck;
|
||||
if (task != null) {
|
||||
task.cancel(true);
|
||||
sseQuietCheck = null;
|
||||
}
|
||||
SseEventSource source;
|
||||
source = this.shadeEventSource;
|
||||
if (source != null) {
|
||||
source.close();
|
||||
this.shadeEventSource = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Open the SSE links.
|
||||
*/
|
||||
public synchronized void sseOpen() {
|
||||
sseClose();
|
||||
|
||||
logger.debug("sseOpen() called");
|
||||
Client client = clientBuilder.sslContext(httpClient.getSslContextFactory().getSslContext())
|
||||
.hostnameVerifier(null).hostnameVerifier(this).readTimeout(0, TimeUnit.SECONDS).build();
|
||||
|
||||
try {
|
||||
// open SSE channel for shades
|
||||
SseEventSource shadeEventSource = eventSourceFactory.newSource(client.target(shadeEvents));
|
||||
shadeEventSource.register(this::onSSeEvent, this::onSseError);
|
||||
shadeEventSource.open();
|
||||
this.shadeEventSource = shadeEventSource;
|
||||
} catch (Exception e) {
|
||||
// SSE documentation does not say what exceptions may be thrown, so catch everything
|
||||
logger.warn("sseOpen() {}", e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reopen the SSE links. If the event source already exists, try first to simply close and re-open it, but if that
|
||||
* fails, then completely destroy and re-create it.
|
||||
*/
|
||||
private synchronized void sseReOpen() {
|
||||
logger.debug("sseReOpen() called");
|
||||
SseEventSource shadeEventSource = this.shadeEventSource;
|
||||
if (shadeEventSource != null) {
|
||||
try {
|
||||
if (shadeEventSource.isOpen()) {
|
||||
shadeEventSource.close();
|
||||
}
|
||||
if (!shadeEventSource.isOpen()) {
|
||||
shadeEventSource.open();
|
||||
}
|
||||
return;
|
||||
} catch (Exception e) {
|
||||
// SSE documentation does not say what exceptions may be thrown, so catch everything
|
||||
logger.warn("sseReOpen() {}", e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
sseOpen();
|
||||
}
|
||||
|
||||
/**
|
||||
* Issue a stop command to a shade.
|
||||
*
|
||||
* @param shadeId the shade to be stopped.
|
||||
* @throws HubProcessingException if any error occurs.
|
||||
*/
|
||||
public void stopShade(int shadeId) throws HubProcessingException {
|
||||
invoke(HttpMethod.PUT, shadeStop, Query.of(IDS, Integer.valueOf(shadeId).toString()), null);
|
||||
}
|
||||
|
||||
/**
|
||||
* HostnameVerifier method implementation that validates the host name when opening SSE connections.
|
||||
*
|
||||
* @param hostName the host name to be verified.
|
||||
* @param sslSession (not used).
|
||||
* @return true if the host name matches our own.
|
||||
*/
|
||||
@Override
|
||||
public boolean verify(@Nullable String hostName, @Nullable SSLSession sslSession) {
|
||||
return this.ipAddress.equals(hostName);
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,7 @@ package org.openhab.binding.hdpowerview.internal;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.core.thing.ThingTypeUID;
|
||||
@@ -74,6 +75,17 @@ public class HDPowerViewBindingConstants {
|
||||
|
||||
public static final List<String> NETBIOS_NAMES = Arrays.asList("PDBU-Hub3.0", "PowerView-Hub");
|
||||
|
||||
public static final Pattern VALID_IP_V4_ADDRESS = Pattern
|
||||
.compile("\\b((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(\\.|$)){4}\\b");
|
||||
|
||||
// generation 3
|
||||
public static final ThingTypeUID THING_TYPE_GATEWAY = new ThingTypeUID(BINDING_ID, "gateway");
|
||||
public static final ThingTypeUID THING_TYPE_SHADE3 = new ThingTypeUID(BINDING_ID, "shade3");
|
||||
|
||||
public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Set.of(THING_TYPE_HUB, THING_TYPE_SHADE,
|
||||
THING_TYPE_REPEATER);
|
||||
THING_TYPE_REPEATER, THING_TYPE_GATEWAY, THING_TYPE_SHADE3);
|
||||
|
||||
public static final String PROPERTY_NAME = "name";
|
||||
public static final String PROPERTY_POWER_TYPE = "powerType";
|
||||
public static final String PROPERTY_BLE_NAME = "bleName";
|
||||
}
|
||||
|
||||
@@ -14,13 +14,18 @@ package org.openhab.binding.hdpowerview.internal;
|
||||
|
||||
import java.util.Hashtable;
|
||||
|
||||
import javax.ws.rs.client.ClientBuilder;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.eclipse.jetty.client.HttpClient;
|
||||
import org.openhab.binding.hdpowerview.internal.discovery.HDPowerViewDeviceDiscoveryService;
|
||||
import org.openhab.binding.hdpowerview.internal.discovery.ShadeDiscoveryService;
|
||||
import org.openhab.binding.hdpowerview.internal.handler.GatewayBridgeHandler;
|
||||
import org.openhab.binding.hdpowerview.internal.handler.HDPowerViewHubHandler;
|
||||
import org.openhab.binding.hdpowerview.internal.handler.HDPowerViewRepeaterHandler;
|
||||
import org.openhab.binding.hdpowerview.internal.handler.HDPowerViewShadeHandler;
|
||||
import org.openhab.binding.hdpowerview.internal.handler.ShadeThingHandler;
|
||||
import org.openhab.core.config.discovery.DiscoveryService;
|
||||
import org.openhab.core.i18n.LocaleProvider;
|
||||
import org.openhab.core.i18n.TranslationProvider;
|
||||
@@ -35,6 +40,7 @@ import org.osgi.service.component.ComponentContext;
|
||||
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;
|
||||
|
||||
/**
|
||||
* The {@link HDPowerViewHandlerFactory} is responsible for creating things and thing
|
||||
@@ -48,15 +54,20 @@ public class HDPowerViewHandlerFactory extends BaseThingHandlerFactory {
|
||||
|
||||
private final HttpClient httpClient;
|
||||
private final HDPowerViewTranslationProvider translationProvider;
|
||||
private final ClientBuilder clientBuilder;
|
||||
private final SseEventSourceFactory eventSourceFactory;
|
||||
|
||||
@Activate
|
||||
public HDPowerViewHandlerFactory(@Reference HttpClientFactory httpClientFactory,
|
||||
final @Reference TranslationProvider i18nProvider, final @Reference LocaleProvider localeProvider,
|
||||
final @Reference ClientBuilder clientBuilder, final @Reference SseEventSourceFactory eventSourceFactory,
|
||||
ComponentContext componentContext) {
|
||||
super.activate(componentContext);
|
||||
this.httpClient = httpClientFactory.getCommonHttpClient();
|
||||
this.translationProvider = new HDPowerViewTranslationProvider(getBundleContext().getBundle(), i18nProvider,
|
||||
localeProvider);
|
||||
this.clientBuilder = clientBuilder;
|
||||
this.eventSourceFactory = eventSourceFactory;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -67,8 +78,14 @@ public class HDPowerViewHandlerFactory extends BaseThingHandlerFactory {
|
||||
@Override
|
||||
protected @Nullable ThingHandler createHandler(Thing thing) {
|
||||
ThingTypeUID thingTypeUID = thing.getThingTypeUID();
|
||||
|
||||
if (HDPowerViewBindingConstants.THING_TYPE_HUB.equals(thingTypeUID)) {
|
||||
if (HDPowerViewBindingConstants.THING_TYPE_GATEWAY.equals(thingTypeUID)) {
|
||||
GatewayBridgeHandler handler = new GatewayBridgeHandler((Bridge) thing, httpClient, translationProvider,
|
||||
clientBuilder, eventSourceFactory);
|
||||
registerService(new ShadeDiscoveryService(handler));
|
||||
return handler;
|
||||
} else if (HDPowerViewBindingConstants.THING_TYPE_SHADE3.equals(thingTypeUID)) {
|
||||
return new ShadeThingHandler(thing);
|
||||
} else if (HDPowerViewBindingConstants.THING_TYPE_HUB.equals(thingTypeUID)) {
|
||||
HDPowerViewHubHandler handler = new HDPowerViewHubHandler((Bridge) thing, httpClient, translationProvider);
|
||||
registerService(new HDPowerViewDeviceDiscoveryService(handler));
|
||||
return handler;
|
||||
@@ -77,7 +94,6 @@ public class HDPowerViewHandlerFactory extends BaseThingHandlerFactory {
|
||||
} else if (HDPowerViewBindingConstants.THING_TYPE_REPEATER.equals(thingTypeUID)) {
|
||||
return new HDPowerViewRepeaterHandler(thing);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -102,9 +102,9 @@ public class HDPowerViewWebTargets {
|
||||
private final HttpClient httpClient;
|
||||
|
||||
/**
|
||||
* private helper class for passing http url query parameters
|
||||
* helper class for passing http url query parameters
|
||||
*/
|
||||
private static class Query {
|
||||
public static class Query {
|
||||
private final String key;
|
||||
private final String value;
|
||||
|
||||
@@ -425,7 +425,7 @@ public class HDPowerViewWebTargets {
|
||||
|
||||
/**
|
||||
* Enables or disables a scheduled event in the hub.
|
||||
*
|
||||
*
|
||||
* @param scheduledEventId id of the scheduled event to be enabled or disabled
|
||||
* @param enable true to enable scheduled event, false to disable
|
||||
* @throws HubInvalidResponseException if response is invalid
|
||||
@@ -527,7 +527,7 @@ public class HDPowerViewWebTargets {
|
||||
|
||||
/**
|
||||
* Enables or disables blinking for a repeater
|
||||
*
|
||||
*
|
||||
* @param repeaterId id of the repeater for which to be enable or disable blinking
|
||||
* @param enable true to enable blinking, false to disable
|
||||
* @return RepeaterData class instance
|
||||
|
||||
@@ -17,11 +17,14 @@ import java.util.List;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.hdpowerview.internal.GatewayWebTargets;
|
||||
import org.openhab.binding.hdpowerview.internal.HDPowerViewBindingConstants;
|
||||
import org.openhab.binding.hdpowerview.internal.HDPowerViewWebTargets;
|
||||
import org.openhab.binding.hdpowerview.internal.dto.ShadeData;
|
||||
import org.openhab.binding.hdpowerview.internal.dto.gen3.Shade;
|
||||
import org.openhab.binding.hdpowerview.internal.dto.responses.RepeaterData;
|
||||
import org.openhab.binding.hdpowerview.internal.exceptions.HubException;
|
||||
import org.openhab.binding.hdpowerview.internal.handler.GatewayBridgeHandler;
|
||||
import org.openhab.binding.hdpowerview.internal.handler.HDPowerViewHubHandler;
|
||||
import org.openhab.core.io.console.Console;
|
||||
import org.openhab.core.io.console.ConsoleCommandCompleter;
|
||||
@@ -65,7 +68,7 @@ public class HDPowerViewCommandExtension extends AbstractConsoleCommandExtension
|
||||
for (Thing thing : thingRegistry.getAll()) {
|
||||
ThingHandler thingHandler = thing.getHandler();
|
||||
if (thingHandler instanceof HDPowerViewHubHandler) {
|
||||
console.println("API bridge: " + thing.getLabel());
|
||||
console.println("Generation 1/2 API hub: " + thing.getLabel());
|
||||
HDPowerViewWebTargets webTargets = ((HDPowerViewHubHandler) thingHandler).getWebTargets();
|
||||
|
||||
try {
|
||||
@@ -87,6 +90,21 @@ public class HDPowerViewCommandExtension extends AbstractConsoleCommandExtension
|
||||
} catch (HubException e) {
|
||||
console.println("Error retrieving ID's: " + e.getMessage());
|
||||
}
|
||||
} else if (thingHandler instanceof GatewayBridgeHandler) {
|
||||
console.println("Generation 3 API gateway: " + thing.getLabel());
|
||||
GatewayWebTargets webTargets = ((GatewayBridgeHandler) thingHandler).getWebTargets();
|
||||
|
||||
try {
|
||||
List<Shade> shades = webTargets.getShades();
|
||||
if (!shades.isEmpty()) {
|
||||
console.println(" - Shades:");
|
||||
for (Shade shade : shades) {
|
||||
console.println(" - ID: " + shade.getId() + " (" + shade.getName() + ")");
|
||||
}
|
||||
}
|
||||
} catch (HubException e) {
|
||||
console.println("Error retrieving ID's: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -129,7 +129,7 @@ public class ShadeCapabilitiesDatabase {
|
||||
protected Type() {
|
||||
}
|
||||
|
||||
protected Type(int type) {
|
||||
public Type(int type) {
|
||||
intValue = type;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2023 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.hdpowerview.internal.discovery;
|
||||
|
||||
import static org.openhab.binding.hdpowerview.internal.HDPowerViewBindingConstants.*;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
import javax.jmdns.ServiceInfo;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.hdpowerview.internal.config.HDPowerViewHubConfiguration;
|
||||
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.thing.ThingTypeUID;
|
||||
import org.openhab.core.thing.ThingUID;
|
||||
import org.osgi.service.component.annotations.Component;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* Discovers HD PowerView Generation 3 Gateways by means of mDNS.
|
||||
*
|
||||
* @author Andrew Fiddian-Green - Initial contribution.
|
||||
*/
|
||||
@NonNullByDefault
|
||||
@Component
|
||||
public class GatewayDiscoveryParticipant implements MDNSDiscoveryParticipant {
|
||||
|
||||
private static final String LABEL_KEY = "discovery.gateway.label";
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(GatewayDiscoveryParticipant.class);
|
||||
|
||||
@Override
|
||||
public @Nullable DiscoveryResult createResult(ServiceInfo service) {
|
||||
for (String host : service.getHostAddresses()) {
|
||||
if (VALID_IP_V4_ADDRESS.matcher(host).matches()) {
|
||||
ThingUID thingUID = new ThingUID(THING_TYPE_GATEWAY, host.replace('.', '_'));
|
||||
DiscoveryResult hub = DiscoveryResultBuilder.create(thingUID)
|
||||
.withProperty(HDPowerViewHubConfiguration.HOST, host)
|
||||
.withRepresentationProperty(HDPowerViewHubConfiguration.HOST)
|
||||
.withLabel(String.format("@text/%s [\"%s\"]", LABEL_KEY, host)).build();
|
||||
logger.debug("mDNS discovered Gen 3 gateway on host '{}'", host);
|
||||
return hub;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getServiceType() {
|
||||
return "_powerview-g3._tcp.local.";
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<ThingTypeUID> getSupportedThingTypeUIDs() {
|
||||
return Set.of(THING_TYPE_GATEWAY);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable ThingUID getThingUID(ServiceInfo service) {
|
||||
for (String host : service.getHostAddresses()) {
|
||||
if (VALID_IP_V4_ADDRESS.matcher(host).matches()) {
|
||||
return new ThingUID(THING_TYPE_GATEWAY, host.replace('.', '_'));
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -55,7 +55,7 @@ public class HDPowerViewDeviceDiscoveryService extends AbstractDiscoveryService
|
||||
private final ShadeCapabilitiesDatabase db = new ShadeCapabilitiesDatabase();
|
||||
|
||||
public HDPowerViewDeviceDiscoveryService(HDPowerViewHubHandler hub) {
|
||||
super(Collections.singleton(HDPowerViewBindingConstants.THING_TYPE_SHADE), 600, true);
|
||||
super(Collections.singleton(HDPowerViewBindingConstants.THING_TYPE_SHADE), 60, true);
|
||||
this.hub = hub;
|
||||
this.scanner = createScanner();
|
||||
}
|
||||
|
||||
@@ -16,7 +16,6 @@ import static org.openhab.binding.hdpowerview.internal.HDPowerViewBindingConstan
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Set;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import javax.jmdns.ServiceInfo;
|
||||
|
||||
@@ -41,10 +40,9 @@ import org.slf4j.LoggerFactory;
|
||||
@Component
|
||||
public class HDPowerViewHubDiscoveryParticipant implements MDNSDiscoveryParticipant {
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(HDPowerViewHubDiscoveryParticipant.class);
|
||||
private static final String LABEL_KEY = "discovery.hub.label";
|
||||
|
||||
private static final Pattern VALID_IP_V4_ADDRESS = Pattern
|
||||
.compile("\\b((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(\\.|$)){4}\\b");
|
||||
private final Logger logger = LoggerFactory.getLogger(HDPowerViewHubDiscoveryParticipant.class);
|
||||
|
||||
@Override
|
||||
public Set<ThingTypeUID> getSupportedThingTypeUIDs() {
|
||||
@@ -64,8 +62,8 @@ public class HDPowerViewHubDiscoveryParticipant implements MDNSDiscoveryParticip
|
||||
DiscoveryResult hub = DiscoveryResultBuilder.create(thingUID)
|
||||
.withProperty(HDPowerViewHubConfiguration.HOST, host)
|
||||
.withRepresentationProperty(HDPowerViewHubConfiguration.HOST)
|
||||
.withLabel("PowerView Hub (" + host + ")").build();
|
||||
logger.debug("mDNS discovered hub on host '{}'", host);
|
||||
.withLabel(String.format("@text/%s [\"%s\"]", LABEL_KEY, host)).build();
|
||||
logger.debug("mDNS discovered Gen 1/2 hub on host '{}'", host);
|
||||
return hub;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,7 +48,7 @@ public class HDPowerViewHubDiscoveryService extends AbstractDiscoveryService {
|
||||
private @Nullable ScheduledFuture<?> backgroundFuture;
|
||||
|
||||
public HDPowerViewHubDiscoveryService() {
|
||||
super(Collections.singleton(THING_TYPE_HUB), 600, true);
|
||||
super(Collections.singleton(THING_TYPE_HUB), 60, true);
|
||||
scanner = createScanner();
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2023 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.hdpowerview.internal.discovery;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.hdpowerview.internal.GatewayWebTargets;
|
||||
import org.openhab.binding.hdpowerview.internal.HDPowerViewBindingConstants;
|
||||
import org.openhab.binding.hdpowerview.internal.config.HDPowerViewShadeConfiguration;
|
||||
import org.openhab.binding.hdpowerview.internal.dto.gen3.Shade;
|
||||
import org.openhab.binding.hdpowerview.internal.exceptions.HubProcessingException;
|
||||
import org.openhab.binding.hdpowerview.internal.handler.GatewayBridgeHandler;
|
||||
import org.openhab.core.config.discovery.AbstractDiscoveryService;
|
||||
import org.openhab.core.config.discovery.DiscoveryResultBuilder;
|
||||
import org.openhab.core.thing.ThingUID;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* Discovers shades in an HD PowerView Generation 3 Gateway.
|
||||
*
|
||||
* @author Andrew Fiddian-Green - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class ShadeDiscoveryService extends AbstractDiscoveryService {
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(ShadeDiscoveryService.class);
|
||||
private final GatewayBridgeHandler hub;
|
||||
private final Runnable scanner;
|
||||
private @Nullable ScheduledFuture<?> backgroundFuture;
|
||||
|
||||
public ShadeDiscoveryService(GatewayBridgeHandler hub) {
|
||||
super(Collections.singleton(HDPowerViewBindingConstants.THING_TYPE_SHADE3), 60, true);
|
||||
this.hub = hub;
|
||||
this.scanner = createScanner();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void startScan() {
|
||||
scheduler.execute(scanner);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void startBackgroundDiscovery() {
|
||||
ScheduledFuture<?> backgroundFuture = this.backgroundFuture;
|
||||
if (backgroundFuture != null && !backgroundFuture.isDone()) {
|
||||
backgroundFuture.cancel(true);
|
||||
}
|
||||
this.backgroundFuture = scheduler.scheduleWithFixedDelay(scanner, 0, 60, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void stopBackgroundDiscovery() {
|
||||
ScheduledFuture<?> backgroundFuture = this.backgroundFuture;
|
||||
if (backgroundFuture != null && !backgroundFuture.isDone()) {
|
||||
backgroundFuture.cancel(true);
|
||||
this.backgroundFuture = null;
|
||||
}
|
||||
super.stopBackgroundDiscovery();
|
||||
}
|
||||
|
||||
private Runnable createScanner() {
|
||||
return () -> {
|
||||
try {
|
||||
GatewayWebTargets webTargets = hub.getWebTargets();
|
||||
discoverShades(webTargets);
|
||||
} catch (HubProcessingException e) {
|
||||
logger.warn("Unexpected exception:{}, message:{}", e.getClass().getSimpleName(), e.getMessage());
|
||||
} catch (IllegalStateException e) {
|
||||
// ignore
|
||||
}
|
||||
stopScan();
|
||||
};
|
||||
}
|
||||
|
||||
private void discoverShades(GatewayWebTargets webTargets) throws HubProcessingException {
|
||||
ThingUID bridgeUid = hub.getThing().getUID();
|
||||
for (Shade shade : webTargets.getShades()) {
|
||||
if (shade.getId() == 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
String id = Integer.toString(shade.getId());
|
||||
ThingUID thingUID = new ThingUID(HDPowerViewBindingConstants.THING_TYPE_SHADE3, bridgeUid, id);
|
||||
|
||||
DiscoveryResultBuilder builder = DiscoveryResultBuilder.create(thingUID).withLabel(shade.getName())
|
||||
.withBridge(bridgeUid).withProperty(HDPowerViewShadeConfiguration.ID, id)
|
||||
.withRepresentationProperty(HDPowerViewShadeConfiguration.ID);
|
||||
String type = shade.getTypeString();
|
||||
if (type != null) {
|
||||
builder.withProperty(HDPowerViewBindingConstants.PROPERTY_SHADE_TYPE, type);
|
||||
}
|
||||
logger.debug("Hub discovered shade '{}'", id);
|
||||
thingDiscovered(builder.build());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -75,7 +75,8 @@ public class ShadePosition {
|
||||
* @param posKindCoords the actuator class (coordinate system) whose state is to be changed.
|
||||
* @param percent the new position value.
|
||||
*/
|
||||
private void setPosition1(Capabilities shadeCapabilities, CoordinateSystem posKindCoords, int percent) {
|
||||
private void setPosition1(Capabilities shadeCapabilities, CoordinateSystem posKindCoords, int percentArg) {
|
||||
int percent = percentArg;
|
||||
switch (posKindCoords) {
|
||||
case PRIMARY_POSITION:
|
||||
/*
|
||||
@@ -202,7 +203,8 @@ public class ShadePosition {
|
||||
* @param posKindCoords the actuator class (coordinate system) whose state is to be changed.
|
||||
* @param percent the new position value.
|
||||
*/
|
||||
private void setPosition2(Capabilities shadeCapabilities, CoordinateSystem posKindCoords, int percent) {
|
||||
private void setPosition2(Capabilities shadeCapabilities, CoordinateSystem posKindCoords, int percentArg) {
|
||||
int percent = percentArg;
|
||||
switch (posKindCoords) {
|
||||
case PRIMARY_POSITION:
|
||||
/*
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2023 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.hdpowerview.internal.dto.gen3;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
/**
|
||||
* DTO for the Generation 3 Gateway information.
|
||||
*
|
||||
* @author Andrew Fiddian-Green - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class Info {
|
||||
private @NonNullByDefault({}) String fwVersion;
|
||||
private @NonNullByDefault({}) String serialNumber;
|
||||
|
||||
public String getFwVersion() {
|
||||
return fwVersion;
|
||||
}
|
||||
|
||||
public String getSerialNumber() {
|
||||
return serialNumber;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2023 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.hdpowerview.internal.dto.gen3;
|
||||
|
||||
/**
|
||||
* Enum for Generation 3 shade power types.
|
||||
*
|
||||
* @author Andrew Fiddian-Green - Initial contribution
|
||||
*/
|
||||
public enum PowerType {
|
||||
BATTERY,
|
||||
HARDWIRED,
|
||||
RECHARGEABLE;
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2023 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.hdpowerview.internal.dto.gen3;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Base64;
|
||||
import java.util.List;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
/**
|
||||
* DTO for a scene as returned by an HD PowerView Generation 3 Gateway.
|
||||
*
|
||||
* @author Andrew Fiddian-Green - Initial contribution.
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class Scene {
|
||||
private int id;
|
||||
private @NonNullByDefault({}) String name;
|
||||
private @NonNullByDefault({}) String ptName;
|
||||
private @NonNullByDefault({}) String color;
|
||||
private @NonNullByDefault({}) String icon;
|
||||
private @NonNullByDefault({}) List<Integer> roomIds;
|
||||
|
||||
public String getColor() {
|
||||
return color;
|
||||
}
|
||||
|
||||
public String getIcon() {
|
||||
return icon;
|
||||
}
|
||||
|
||||
public int getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return String.join(" ", new String(Base64.getDecoder().decode(name), StandardCharsets.UTF_8), ptName);
|
||||
}
|
||||
|
||||
public String getPtName() {
|
||||
return ptName;
|
||||
}
|
||||
|
||||
public List<Integer> getRoomIds() {
|
||||
List<Integer> roomIds = this.roomIds;
|
||||
return roomIds != null ? roomIds : List.of();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2023 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.hdpowerview.internal.dto.gen3;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
/**
|
||||
* DTO for scene SSE event object as supplied an HD PowerView Generation 3 Gateway.
|
||||
*
|
||||
* @author Andrew Fiddian-Green - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class SceneEvent {
|
||||
private int id;
|
||||
private @NonNullByDefault({}) Scene scene;
|
||||
|
||||
public int getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public Scene getScene() {
|
||||
return scene;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2023 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.hdpowerview.internal.dto.gen3;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Base64;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.hdpowerview.internal.dto.CoordinateSystem;
|
||||
import org.openhab.binding.hdpowerview.internal.dto.Firmware;
|
||||
import org.openhab.core.library.types.DecimalType;
|
||||
import org.openhab.core.library.types.OnOffType;
|
||||
import org.openhab.core.library.types.PercentType;
|
||||
import org.openhab.core.library.types.QuantityType;
|
||||
import org.openhab.core.library.unit.Units;
|
||||
import org.openhab.core.types.State;
|
||||
import org.openhab.core.types.UnDefType;
|
||||
|
||||
/**
|
||||
* DTO for a shade as returned by an HD PowerView Generation 3 Gateway.
|
||||
*
|
||||
* @author Andrew Fiddian-Green - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class Shade {
|
||||
private @Nullable Integer id;
|
||||
private @Nullable Integer type;
|
||||
private @Nullable String name;
|
||||
private @Nullable @SuppressWarnings("unused") String ptName;
|
||||
private @Nullable Integer capabilities;
|
||||
private @Nullable Integer powerType;
|
||||
private @Nullable Integer batteryStatus;
|
||||
private @Nullable Integer signalStrength;
|
||||
private @Nullable String bleName;
|
||||
private @Nullable Firmware firmware;
|
||||
private @Nullable ShadePosition positions;
|
||||
|
||||
private transient boolean partialState;
|
||||
|
||||
public State getBatteryLevel() {
|
||||
Integer batteryStatus = this.batteryStatus;
|
||||
return batteryStatus == null ? UnDefType.UNDEF
|
||||
: new DecimalType(Math.max(0, Math.min(100, (100 * batteryStatus) / 3)));
|
||||
}
|
||||
|
||||
public @Nullable String getBleName() {
|
||||
return bleName;
|
||||
}
|
||||
|
||||
public @Nullable Integer getCapabilities() {
|
||||
return capabilities;
|
||||
}
|
||||
|
||||
public @Nullable String getCapabilitieString() {
|
||||
Integer capabilities = this.capabilities;
|
||||
return capabilities == null ? null : Integer.toString(capabilities);
|
||||
}
|
||||
|
||||
public @Nullable String getFirmware() {
|
||||
Firmware firmware = this.firmware;
|
||||
return firmware == null ? null
|
||||
: String.format("%d.%d.%d", firmware.revision, firmware.subRevision, firmware.build);
|
||||
}
|
||||
|
||||
public int getId() {
|
||||
Integer id = this.id;
|
||||
return id != null ? id.intValue() : 0;
|
||||
}
|
||||
|
||||
public State getLowBattery() {
|
||||
Integer batteryStatus = this.batteryStatus;
|
||||
return batteryStatus == null ? UnDefType.UNDEF : OnOffType.from(batteryStatus == 0);
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return new String(Base64.getDecoder().decode(name), StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
public State getPosition(CoordinateSystem posKindCoords) {
|
||||
ShadePosition positions = this.positions;
|
||||
return positions == null ? UnDefType.UNDEF : positions.getState(posKindCoords);
|
||||
}
|
||||
|
||||
public PowerType getPowerType() {
|
||||
Integer powerType = this.powerType;
|
||||
return PowerType.values()[powerType != null ? powerType.intValue() : 0];
|
||||
}
|
||||
|
||||
public @Nullable ShadePosition getShadePositions() {
|
||||
return positions;
|
||||
}
|
||||
|
||||
public State getSignalStrength() {
|
||||
Integer signalStrength = this.signalStrength;
|
||||
return signalStrength != null ? new QuantityType<>(signalStrength, Units.DECIBEL_MILLIWATTS) : UnDefType.UNDEF;
|
||||
}
|
||||
|
||||
public @Nullable Integer getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public @Nullable String getTypeString() {
|
||||
Integer type = this.type;
|
||||
return type == null ? null : Integer.toString(type);
|
||||
}
|
||||
|
||||
public boolean hasFullState() {
|
||||
return !partialState;
|
||||
}
|
||||
|
||||
public boolean isMainsPowered() {
|
||||
return getPowerType() == PowerType.HARDWIRED;
|
||||
}
|
||||
|
||||
public Shade setCapabilities(int capabilities) {
|
||||
this.capabilities = capabilities;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Shade setId(int id) {
|
||||
this.id = id;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Shade setPartialState() {
|
||||
this.partialState = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Shade setPosition(CoordinateSystem coordinates, PercentType percent) {
|
||||
ShadePosition positions = this.positions;
|
||||
if (positions == null) {
|
||||
positions = new ShadePosition();
|
||||
this.positions = positions;
|
||||
}
|
||||
positions.setPosition(coordinates, percent);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Shade setShadePosition(ShadePosition position) {
|
||||
this.positions = position;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Shade setType(int type) {
|
||||
this.type = type;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2023 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.hdpowerview.internal.dto.gen3;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
/**
|
||||
* DTO for a shade SSE event object as supplied an HD PowerView Generation 3 Gateway.
|
||||
*
|
||||
* @author Andrew Fiddian-Green - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class ShadeEvent {
|
||||
private int id;
|
||||
private @NonNullByDefault({}) ShadePosition currentPositions;
|
||||
|
||||
public ShadePosition getCurrentPositions() {
|
||||
return currentPositions;
|
||||
}
|
||||
|
||||
public int getId() {
|
||||
return id;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2023 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.hdpowerview.internal.dto.gen3;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.math.MathContext;
|
||||
import java.math.RoundingMode;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.binding.hdpowerview.internal.dto.CoordinateSystem;
|
||||
import org.openhab.core.library.types.PercentType;
|
||||
import org.openhab.core.types.State;
|
||||
import org.openhab.core.types.UnDefType;
|
||||
|
||||
/**
|
||||
* DTO for the position of a shade as returned by an HD PowerView Generation 3 Gateway.
|
||||
*
|
||||
* @author Andrew Fiddian-Green - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class ShadePosition {
|
||||
private static final MathContext MATH_CONTEXT = new MathContext(4, RoundingMode.HALF_UP);
|
||||
|
||||
private @NonNullByDefault({}) Double primary;
|
||||
private @NonNullByDefault({}) Double secondary;
|
||||
private @NonNullByDefault({}) Double tilt;
|
||||
|
||||
public State getState(CoordinateSystem posKindCoords) {
|
||||
Double value;
|
||||
switch (posKindCoords) {
|
||||
case PRIMARY_POSITION:
|
||||
value = primary;
|
||||
break;
|
||||
case SECONDARY_POSITION:
|
||||
value = secondary;
|
||||
break;
|
||||
case VANE_TILT_POSITION:
|
||||
value = tilt;
|
||||
break;
|
||||
default:
|
||||
value = null;
|
||||
}
|
||||
return value != null ? new PercentType(new BigDecimal(value * 100f, MATH_CONTEXT)) : UnDefType.UNDEF;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a new position value for this object based on the given coordinates, and the given new value.
|
||||
*
|
||||
* @param coordinates which of the position fields shall be set.
|
||||
* @param percent the new value in percent.
|
||||
* @return this object.
|
||||
*/
|
||||
public ShadePosition setPosition(CoordinateSystem coordinates, PercentType percent) {
|
||||
Double value = percent.doubleValue() / 100f;
|
||||
switch (coordinates) {
|
||||
case PRIMARY_POSITION:
|
||||
primary = value;
|
||||
break;
|
||||
case SECONDARY_POSITION:
|
||||
secondary = value;
|
||||
break;
|
||||
case VANE_TILT_POSITION:
|
||||
tilt = value;
|
||||
break;
|
||||
default:
|
||||
}
|
||||
return this;
|
||||
}
|
||||
}
|
||||
@@ -20,7 +20,7 @@ import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
* @author Jacob Laursen - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
class ShadeMotion {
|
||||
public class ShadeMotion {
|
||||
|
||||
public enum Type {
|
||||
STOP("stop"),
|
||||
|
||||
@@ -0,0 +1,321 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2023 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.hdpowerview.internal.handler;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
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.eclipse.jetty.client.HttpClient;
|
||||
import org.openhab.binding.hdpowerview.internal.GatewayWebTargets;
|
||||
import org.openhab.binding.hdpowerview.internal.HDPowerViewBindingConstants;
|
||||
import org.openhab.binding.hdpowerview.internal.HDPowerViewTranslationProvider;
|
||||
import org.openhab.binding.hdpowerview.internal.config.HDPowerViewHubConfiguration;
|
||||
import org.openhab.binding.hdpowerview.internal.dto.gen3.Scene;
|
||||
import org.openhab.binding.hdpowerview.internal.dto.gen3.Shade;
|
||||
import org.openhab.binding.hdpowerview.internal.exceptions.HubProcessingException;
|
||||
import org.openhab.core.library.CoreItemFactory;
|
||||
import org.openhab.core.library.types.OnOffType;
|
||||
import org.openhab.core.thing.Bridge;
|
||||
import org.openhab.core.thing.Channel;
|
||||
import org.openhab.core.thing.ChannelGroupUID;
|
||||
import org.openhab.core.thing.ChannelUID;
|
||||
import org.openhab.core.thing.Thing;
|
||||
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.ThingHandler;
|
||||
import org.openhab.core.thing.binding.builder.ChannelBuilder;
|
||||
import org.openhab.core.thing.type.AutoUpdatePolicy;
|
||||
import org.openhab.core.thing.type.ChannelTypeUID;
|
||||
import org.openhab.core.types.Command;
|
||||
import org.openhab.core.types.RefreshType;
|
||||
import org.osgi.service.jaxrs.client.SseEventSourceFactory;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* Bridge handler for an HD PowerView Generation 3 Gateway.
|
||||
*
|
||||
* @author Andrew Fiddian-Green - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class GatewayBridgeHandler extends BaseBridgeHandler {
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(GatewayBridgeHandler.class);
|
||||
private final String channelTypeId = HDPowerViewBindingConstants.CHANNELTYPE_SCENE_ACTIVATE;
|
||||
private final String channelGroupId = HDPowerViewBindingConstants.CHANNEL_GROUP_SCENES;
|
||||
|
||||
private final HttpClient httpClient;
|
||||
private final HDPowerViewTranslationProvider translationProvider;
|
||||
private final ClientBuilder clientBuilder;
|
||||
private final SseEventSourceFactory eventSourceFactory;
|
||||
|
||||
private @Nullable GatewayWebTargets webTargets;
|
||||
private @Nullable ScheduledFuture<?> refreshTask;
|
||||
|
||||
private boolean scenesLoaded;
|
||||
private boolean propertiesLoaded;
|
||||
private boolean disposing;
|
||||
|
||||
public GatewayBridgeHandler(Bridge bridge, HttpClient httpClient,
|
||||
HDPowerViewTranslationProvider translationProvider, ClientBuilder clientBuilder,
|
||||
SseEventSourceFactory eventSourceFactory) {
|
||||
super(bridge);
|
||||
this.httpClient = httpClient;
|
||||
this.translationProvider = translationProvider;
|
||||
this.clientBuilder = clientBuilder;
|
||||
this.eventSourceFactory = eventSourceFactory;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void childHandlerInitialized(ThingHandler childHandler, Thing childThing) {
|
||||
if (childHandler instanceof ShadeThingHandler) {
|
||||
refreshShade(((ShadeThingHandler) childHandler).getShadeId());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose() {
|
||||
disposing = true;
|
||||
ScheduledFuture<?> future = this.refreshTask;
|
||||
if (future != null) {
|
||||
future.cancel(true);
|
||||
}
|
||||
this.refreshTask = null;
|
||||
|
||||
GatewayWebTargets webTargets = this.webTargets;
|
||||
if (webTargets != null) {
|
||||
scheduler.submit(() -> disposeWebTargets(webTargets));
|
||||
this.webTargets = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void disposeWebTargets(GatewayWebTargets webTargets) {
|
||||
try {
|
||||
webTargets.close();
|
||||
} catch (IOException e) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh the state of all things. Normally the thing's position state is updated by SSE. However we must do a
|
||||
* refresh once on start up in order to get the initial state. Also the other properties (battery, signal strength
|
||||
* etc.) are not updated by SSE. Furthermore we need to do periodic refreshes just in case the SSE connection may
|
||||
* have been lost.
|
||||
*/
|
||||
private void doRefresh() {
|
||||
logger.debug("doRefresh()");
|
||||
try {
|
||||
if (getWebTargets().gatewayRegister()) {
|
||||
updateStatus(ThingStatus.ONLINE);
|
||||
getWebTargets().sseOpen();
|
||||
refreshProperties();
|
||||
refreshShades();
|
||||
refreshScenes();
|
||||
}
|
||||
} catch (IllegalStateException | HubProcessingException e) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
|
||||
logger.debug("doRefresh() exception:{}, message:{}", e.getClass().getSimpleName(), e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
public ScheduledExecutorService getScheduler() {
|
||||
return scheduler;
|
||||
}
|
||||
|
||||
/**
|
||||
* Getter for the list of all child shade thing handlers.
|
||||
*
|
||||
* @return the list of shade handlers.
|
||||
* @throws IllegalStateException if the bridge is not properly initialized.
|
||||
*/
|
||||
private List<ShadeThingHandler> getShadeThingHandlers() throws IllegalStateException {
|
||||
logger.debug("getShadeThingHandlers()");
|
||||
return getThing().getThings().stream().map(Thing::getHandler).filter(ShadeThingHandler.class::isInstance)
|
||||
.map(ShadeThingHandler.class::cast).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* Getter for the webTargets.
|
||||
*
|
||||
* @return the webTargets.
|
||||
* @throws IllegalStateException if webTargets is not initialized.
|
||||
*/
|
||||
public GatewayWebTargets getWebTargets() throws IllegalStateException {
|
||||
GatewayWebTargets webTargets = this.webTargets;
|
||||
if (webTargets != null) {
|
||||
return webTargets;
|
||||
}
|
||||
throw new IllegalStateException("WebTargets not initialized.");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleCommand(ChannelUID channelUID, Command command) {
|
||||
if (RefreshType.REFRESH == command) {
|
||||
scheduler.submit(() -> doRefresh());
|
||||
return;
|
||||
}
|
||||
Channel channel = getThing().getChannel(channelUID.getId());
|
||||
if (channel == null) {
|
||||
return;
|
||||
}
|
||||
ChannelTypeUID channelTypeUID = channel.getChannelTypeUID();
|
||||
if (channelTypeUID == null) {
|
||||
return;
|
||||
}
|
||||
if (channelTypeId.equals(channelTypeUID.getId()) && OnOffType.ON == command) {
|
||||
try {
|
||||
getWebTargets().activateScene(Integer.parseInt(channelUID.getIdWithoutGroup()));
|
||||
} catch (HubProcessingException | IllegalStateException e) {
|
||||
logger.warn("handleCommand() exception:{}, message:{}", e.getClass().getSimpleName(), e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initialize() {
|
||||
HDPowerViewHubConfiguration config = getConfigAs(HDPowerViewHubConfiguration.class);
|
||||
String host = config.host;
|
||||
|
||||
if (host == null || host.isEmpty()) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
|
||||
"@text/offline.conf-error.no-host-address");
|
||||
return;
|
||||
}
|
||||
|
||||
webTargets = new GatewayWebTargets(this, httpClient, clientBuilder, eventSourceFactory, host);
|
||||
scenesLoaded = false;
|
||||
propertiesLoaded = false;
|
||||
disposing = false;
|
||||
|
||||
/*
|
||||
* Normally the thing's position state is updated by SSE. However we must do a refresh once on start up in order
|
||||
* to get the initial state. Also the other properties (battery, signal strength etc.) are not updated by SSE.
|
||||
* Furthermore we need to do periodic refreshes just in case the SSE connection may have been lost. So we
|
||||
* schedule the refresh at the 'hardRefresh' interval.
|
||||
*/
|
||||
refreshTask = scheduler.scheduleWithFixedDelay(() -> doRefresh(), 0, config.hardRefresh, TimeUnit.MINUTES);
|
||||
updateStatus(ThingStatus.UNKNOWN);
|
||||
}
|
||||
|
||||
/**
|
||||
* Method that is called when a scene changes state.
|
||||
*
|
||||
* @param scene the one that changed.
|
||||
*/
|
||||
public void onSceneEvent(Scene scene) {
|
||||
// TODO perhaps we should trigger an OH core event here ??
|
||||
}
|
||||
|
||||
/**
|
||||
* Method that is called when a shade changes state.
|
||||
*
|
||||
* @param shade the one that changed.
|
||||
*/
|
||||
public void onShadeEvent(Shade shade) {
|
||||
if (disposing) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
for (ShadeThingHandler handler : getShadeThingHandlers()) {
|
||||
if (handler.notify(shade)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (IllegalStateException e) {
|
||||
logger.warn("onShadeEvent() exception:{}, message:{}", e.getClass().getSimpleName(), e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
private void refreshProperties() throws HubProcessingException, IllegalStateException {
|
||||
if (propertiesLoaded || disposing) {
|
||||
return;
|
||||
}
|
||||
logger.debug("refreshProperties()");
|
||||
thing.setProperties(getWebTargets().getInformation());
|
||||
propertiesLoaded = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the dynamic list of scene channels.
|
||||
*
|
||||
* @throws HubProcessingException if the web target connection caused an error.
|
||||
* @throws IllegalStateException if this handler is in an illegal state.
|
||||
*/
|
||||
private void refreshScenes() throws HubProcessingException, IllegalStateException {
|
||||
if (scenesLoaded || disposing) {
|
||||
return;
|
||||
}
|
||||
logger.debug("refreshScenes()");
|
||||
ChannelTypeUID typeUID = new ChannelTypeUID(HDPowerViewBindingConstants.BINDING_ID, channelTypeId);
|
||||
ChannelGroupUID groupUID = new ChannelGroupUID(thing.getUID(), channelGroupId);
|
||||
List<Channel> channels = new ArrayList<>();
|
||||
for (Scene scene : getWebTargets().getScenes()) {
|
||||
ChannelUID channelUID = new ChannelUID(groupUID, Integer.toString(scene.getId()));
|
||||
String name = scene.getName();
|
||||
String description = translationProvider.getText("dynamic-channel.scene-activate.description", name);
|
||||
channels.add(ChannelBuilder.create(channelUID, CoreItemFactory.SWITCH).withType(typeUID).withLabel(name)
|
||||
.withDescription(description).withAutoUpdatePolicy(AutoUpdatePolicy.VETO).build());
|
||||
}
|
||||
updateThing(editThing().withChannels(channels).build());
|
||||
scenesLoaded = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh a single shade.
|
||||
*
|
||||
* @param shadeId the id of the shade to be refreshed.
|
||||
*/
|
||||
private void refreshShade(int shadeId) {
|
||||
try {
|
||||
Shade shade = getWebTargets().getShade(shadeId);
|
||||
for (ShadeThingHandler handler : getShadeThingHandlers()) {
|
||||
if (handler.notify(shade)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (HubProcessingException | IllegalStateException e) {
|
||||
logger.warn("refreshShade() exception:{}, message:{}", e.getClass().getSimpleName(), e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the full list of shades data and notify each of the thing handlers.
|
||||
*
|
||||
* @throws HubProcessingException if the web target connection caused an error.
|
||||
* @throws IllegalStateException if this handler is in an illegal state.
|
||||
*/
|
||||
private void refreshShades() throws HubProcessingException, IllegalStateException {
|
||||
if (disposing) {
|
||||
return;
|
||||
}
|
||||
logger.debug("refreshShades()");
|
||||
List<ShadeThingHandler> handlers = getShadeThingHandlers();
|
||||
for (Shade shade : getWebTargets().getShades()) {
|
||||
for (ShadeThingHandler handler : handlers) {
|
||||
if (handler.notify(shade)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,334 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2023 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.hdpowerview.internal.handler;
|
||||
|
||||
import static org.openhab.binding.hdpowerview.internal.HDPowerViewBindingConstants.*;
|
||||
import static org.openhab.binding.hdpowerview.internal.dto.CoordinateSystem.*;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.StringJoiner;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.hdpowerview.internal.GatewayWebTargets;
|
||||
import org.openhab.binding.hdpowerview.internal.HDPowerViewBindingConstants;
|
||||
import org.openhab.binding.hdpowerview.internal.config.HDPowerViewShadeConfiguration;
|
||||
import org.openhab.binding.hdpowerview.internal.database.ShadeCapabilitiesDatabase;
|
||||
import org.openhab.binding.hdpowerview.internal.database.ShadeCapabilitiesDatabase.Capabilities;
|
||||
import org.openhab.binding.hdpowerview.internal.database.ShadeCapabilitiesDatabase.Type;
|
||||
import org.openhab.binding.hdpowerview.internal.dto.gen3.Shade;
|
||||
import org.openhab.binding.hdpowerview.internal.dto.gen3.ShadePosition;
|
||||
import org.openhab.binding.hdpowerview.internal.exceptions.HubProcessingException;
|
||||
import org.openhab.core.library.types.PercentType;
|
||||
import org.openhab.core.library.types.StopMoveType;
|
||||
import org.openhab.core.library.types.StringType;
|
||||
import org.openhab.core.library.types.UpDownType;
|
||||
import org.openhab.core.thing.Bridge;
|
||||
import org.openhab.core.thing.Channel;
|
||||
import org.openhab.core.thing.ChannelUID;
|
||||
import org.openhab.core.thing.Thing;
|
||||
import org.openhab.core.thing.ThingStatus;
|
||||
import org.openhab.core.thing.ThingStatusDetail;
|
||||
import org.openhab.core.thing.binding.BaseThingHandler;
|
||||
import org.openhab.core.thing.binding.BridgeHandler;
|
||||
import org.openhab.core.types.Command;
|
||||
import org.openhab.core.types.RefreshType;
|
||||
import org.openhab.core.types.State;
|
||||
import org.openhab.core.types.UnDefType;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* Thing handler for shades in an HD PowerView Generation 3 Gateway.
|
||||
*
|
||||
* @author Andrew Fiddian-Green - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class ShadeThingHandler extends BaseThingHandler {
|
||||
|
||||
private static final String INVALID_CHANNEL = "invalid channel";
|
||||
private static final String INVALID_COMMAND = "invalid command";
|
||||
|
||||
private static final String COMMAND_IDENTIFY = "IDENTIFY";
|
||||
|
||||
private static final ShadeCapabilitiesDatabase DB = new ShadeCapabilitiesDatabase();
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(ShadeThingHandler.class);
|
||||
|
||||
private int shadeId;
|
||||
private boolean isInitialized;
|
||||
private @Nullable Capabilities capabilities;
|
||||
|
||||
public ShadeThingHandler(Thing thing) {
|
||||
super(thing);
|
||||
}
|
||||
|
||||
/**
|
||||
* Getter for the hub handler.
|
||||
*
|
||||
* @return the hub handler.
|
||||
* @throws IllegalStateException if the bridge or its handler are not initialized.
|
||||
*/
|
||||
private GatewayBridgeHandler getBridgeHandler() throws IllegalStateException {
|
||||
Bridge bridge = this.getBridge();
|
||||
if (bridge == null) {
|
||||
throw new IllegalStateException("Bridge not initialised.");
|
||||
}
|
||||
BridgeHandler handler = bridge.getHandler();
|
||||
if (!(handler instanceof GatewayBridgeHandler)) {
|
||||
throw new IllegalStateException("Bridge handler not initialised.");
|
||||
}
|
||||
return (GatewayBridgeHandler) handler;
|
||||
}
|
||||
|
||||
public int getShadeId() {
|
||||
return shadeId;
|
||||
}
|
||||
|
||||
private Type getType(Shade shade) {
|
||||
Integer type = shade.getType();
|
||||
return type != null ? DB.getType(type) : new ShadeCapabilitiesDatabase.Type(0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleCommand(ChannelUID channelUID, Command command) {
|
||||
if (RefreshType.REFRESH == command) {
|
||||
getBridgeHandler().handleCommand(channelUID, command);
|
||||
return;
|
||||
}
|
||||
|
||||
GatewayWebTargets webTargets = getBridgeHandler().getWebTargets();
|
||||
ShadePosition position = new ShadePosition();
|
||||
try {
|
||||
switch (channelUID.getId()) {
|
||||
case CHANNEL_SHADE_POSITION:
|
||||
if (command instanceof PercentType) {
|
||||
position.setPosition(PRIMARY_POSITION, ((PercentType) command));
|
||||
webTargets.moveShade(shadeId, new Shade().setShadePosition(position));
|
||||
break;
|
||||
} else if (command instanceof UpDownType) {
|
||||
position.setPosition(PRIMARY_POSITION,
|
||||
(UpDownType.UP == command) && !Objects.requireNonNull(capabilities).isPrimaryInverted()
|
||||
? PercentType.HUNDRED
|
||||
: PercentType.ZERO);
|
||||
webTargets.moveShade(shadeId, new Shade().setShadePosition(position));
|
||||
break;
|
||||
} else if (StopMoveType.STOP == command) {
|
||||
webTargets.stopShade(shadeId);
|
||||
break;
|
||||
}
|
||||
throw new IllegalArgumentException(INVALID_COMMAND);
|
||||
|
||||
case CHANNEL_SHADE_SECONDARY_POSITION:
|
||||
if (command instanceof PercentType) {
|
||||
position.setPosition(SECONDARY_POSITION, ((PercentType) command));
|
||||
webTargets.moveShade(shadeId, new Shade().setShadePosition(position));
|
||||
break;
|
||||
} else if (command instanceof UpDownType) {
|
||||
position.setPosition(SECONDARY_POSITION,
|
||||
(UpDownType.UP == command)
|
||||
&& !Objects.requireNonNull(capabilities).supportsSecondaryOverlapped()
|
||||
? PercentType.ZERO
|
||||
: PercentType.HUNDRED);
|
||||
webTargets.moveShade(shadeId, new Shade().setShadePosition(position));
|
||||
break;
|
||||
} else if (StopMoveType.STOP == command) {
|
||||
webTargets.stopShade(shadeId);
|
||||
break;
|
||||
}
|
||||
throw new IllegalArgumentException(INVALID_COMMAND);
|
||||
|
||||
case CHANNEL_SHADE_VANE:
|
||||
if (command instanceof PercentType) {
|
||||
position.setPosition(VANE_TILT_POSITION, ((PercentType) command));
|
||||
webTargets.moveShade(shadeId, new Shade().setShadePosition(position));
|
||||
break;
|
||||
} else if (command instanceof UpDownType) {
|
||||
position.setPosition(VANE_TILT_POSITION,
|
||||
UpDownType.UP == command ? PercentType.HUNDRED : PercentType.ZERO);
|
||||
webTargets.moveShade(shadeId, new Shade().setShadePosition(position));
|
||||
break;
|
||||
}
|
||||
throw new IllegalArgumentException(INVALID_COMMAND);
|
||||
|
||||
case CHANNEL_SHADE_COMMAND:
|
||||
if ((command instanceof StringType) && COMMAND_IDENTIFY.equals(((StringType) command).toString())) {
|
||||
webTargets.jogShade(shadeId);
|
||||
break;
|
||||
}
|
||||
throw new IllegalArgumentException(INVALID_COMMAND);
|
||||
|
||||
default:
|
||||
throw new IllegalArgumentException(INVALID_CHANNEL);
|
||||
}
|
||||
} catch (HubProcessingException | IllegalArgumentException e) {
|
||||
logger.warn("handleCommand() shadeId:{}, channelUID:{}, command:{}, exception:{}, message:{}", shadeId,
|
||||
channelUID, command, e.getClass().getSimpleName(), e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private boolean hasPrimary() {
|
||||
return Objects.requireNonNull(capabilities).supportsPrimary();
|
||||
}
|
||||
|
||||
private boolean hasSecondary() {
|
||||
Capabilities capabilities = Objects.requireNonNull(this.capabilities);
|
||||
return capabilities.supportsSecondary() || capabilities.supportsSecondaryOverlapped();
|
||||
}
|
||||
|
||||
private boolean hasVane() {
|
||||
Capabilities capabilities = Objects.requireNonNull(this.capabilities);
|
||||
return capabilities.supportsTilt180() || capabilities.supportsTiltAnywhere()
|
||||
|| capabilities.supportsTiltOnClosed();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initialize() {
|
||||
shadeId = getConfigAs(HDPowerViewShadeConfiguration.class).id;
|
||||
Bridge bridge = getBridge();
|
||||
BridgeHandler bridgeHandler = bridge != null ? bridge.getHandler() : null;
|
||||
if (!(bridgeHandler instanceof GatewayBridgeHandler)) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
|
||||
"@text/offline.conf-error.invalid-bridge-handler");
|
||||
return;
|
||||
}
|
||||
isInitialized = false;
|
||||
updateStatus(ThingStatus.UNKNOWN);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle shade state change notifications.
|
||||
*
|
||||
* @param shade the new shade state.
|
||||
* @return true if we handled the call.
|
||||
*/
|
||||
public boolean notify(Shade shade) {
|
||||
if (shadeId == shade.getId()) {
|
||||
updateStatus(ThingStatus.ONLINE);
|
||||
if (!isInitialized && shade.hasFullState()) {
|
||||
updateCapabilities(shade);
|
||||
updateProperties(shade);
|
||||
updateDynamicChannels(shade);
|
||||
isInitialized = true;
|
||||
}
|
||||
updateChannels(shade);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the capabilities object based on the data in the passed shade instance.
|
||||
*
|
||||
* @param shade containing the channel data.
|
||||
*/
|
||||
private void updateCapabilities(Shade shade) {
|
||||
Capabilities capabilities = this.capabilities;
|
||||
if (capabilities == null) {
|
||||
capabilities = DB.getCapabilities(shade.getCapabilities());
|
||||
this.capabilities = capabilities;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update channels based on the data in the passed shade instance.
|
||||
*
|
||||
* @param shade containing the channel data.
|
||||
*/
|
||||
private void updateChannels(Shade shade) {
|
||||
updateState(CHANNEL_SHADE_POSITION, hasPrimary() ? shade.getPosition(PRIMARY_POSITION) : UnDefType.UNDEF);
|
||||
updateState(CHANNEL_SHADE_VANE, hasVane() ? shade.getPosition(VANE_TILT_POSITION) : UnDefType.UNDEF);
|
||||
updateState(CHANNEL_SHADE_SECONDARY_POSITION,
|
||||
hasSecondary() ? shade.getPosition(SECONDARY_POSITION) : UnDefType.UNDEF);
|
||||
if (shade.hasFullState()) {
|
||||
updateState(CHANNEL_SHADE_LOW_BATTERY, shade.getLowBattery());
|
||||
updateState(CHANNEL_SHADE_BATTERY_LEVEL, shade.getBatteryLevel());
|
||||
updateState(CHANNEL_SHADE_SIGNAL_STRENGTH, shade.getSignalStrength());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* If the given channel exists in the thing, but is NOT required in the thing, then add it to a list of channels to
|
||||
* be removed. Or if the channel does NOT exist in the thing, but is required in the thing, then log a warning.
|
||||
*
|
||||
* @param removeList the list of channels to be removed from the thing.
|
||||
* @param channelId the id of the channel to be (eventually) removed.
|
||||
* @param channelRequired true if the thing requires this channel.
|
||||
*/
|
||||
private void updateDynamicChannel(List<Channel> removeList, String channelId, boolean channelRequired) {
|
||||
Channel channel = thing.getChannel(channelId);
|
||||
if (!channelRequired && channel != null) {
|
||||
removeList.add(channel);
|
||||
} else if (channelRequired && channel == null) {
|
||||
logger.warn("updateDynamicChannel() shadeId:{} is missing channel:{} => please recreate the thing", shadeId,
|
||||
channelId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove previously statically created channels if the shade does not support them or they are not relevant.
|
||||
*
|
||||
* @param shade containing the channel data.
|
||||
*/
|
||||
private void updateDynamicChannels(Shade shade) {
|
||||
List<Channel> removeChannels = new ArrayList<>();
|
||||
updateDynamicChannel(removeChannels, CHANNEL_SHADE_POSITION, hasPrimary());
|
||||
updateDynamicChannel(removeChannels, CHANNEL_SHADE_SECONDARY_POSITION, hasSecondary());
|
||||
updateDynamicChannel(removeChannels, CHANNEL_SHADE_VANE, hasVane());
|
||||
updateDynamicChannel(removeChannels, CHANNEL_SHADE_BATTERY_LEVEL, !shade.isMainsPowered());
|
||||
updateDynamicChannel(removeChannels, CHANNEL_SHADE_LOW_BATTERY, !shade.isMainsPowered());
|
||||
if (!removeChannels.isEmpty()) {
|
||||
if (logger.isDebugEnabled()) {
|
||||
StringJoiner joiner = new StringJoiner(", ");
|
||||
removeChannels.forEach(c -> joiner.add(c.getUID().getId()));
|
||||
logger.debug("updateDynamicChannels() shadeId:{}, removing unsupported channels:{}", shadeId,
|
||||
joiner.toString());
|
||||
}
|
||||
updateThing(editThing().withoutChannels(removeChannels).build());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update thing properties based on the data in the passed shade instance.
|
||||
*
|
||||
* @param shade containing the property data.
|
||||
*/
|
||||
private void updateProperties(Shade shade) {
|
||||
thing.setProperties(Stream.of(new String[][] { //
|
||||
{ HDPowerViewBindingConstants.PROPERTY_NAME, shade.getName() },
|
||||
{ HDPowerViewBindingConstants.PROPERTY_SHADE_TYPE, getType(shade).toString() },
|
||||
{ HDPowerViewBindingConstants.PROPERTY_SHADE_CAPABILITIES,
|
||||
Objects.requireNonNull(capabilities).toString() },
|
||||
{ HDPowerViewBindingConstants.PROPERTY_POWER_TYPE, shade.getPowerType().name().toLowerCase() },
|
||||
{ HDPowerViewBindingConstants.PROPERTY_BLE_NAME, shade.getBleName() },
|
||||
{ Thing.PROPERTY_FIRMWARE_VERSION, shade.getFirmware() } //
|
||||
}).collect(Collectors.toMap(data -> data[0], data -> data[1])));
|
||||
}
|
||||
|
||||
/**
|
||||
* Override base method to only update the channel if it actually exists.
|
||||
*
|
||||
* @param channelID id of the channel, which was updated
|
||||
* @param state new state
|
||||
*/
|
||||
@Override
|
||||
protected void updateState(String channelID, State state) {
|
||||
if (thing.getChannel(channelID) != null) {
|
||||
super.updateState(channelID, state);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,7 @@
|
||||
<config-description uri="thing-type:hdpowerview:shade">
|
||||
<parameter name="id" type="integer" min="1" required="true">
|
||||
<label>ID</label>
|
||||
<description>The numeric ID of the PowerView Shade in the Hub</description>
|
||||
<description>The numeric ID of the PowerView Shade in the Hub/Gateway</description>
|
||||
</parameter>
|
||||
</config-description>
|
||||
|
||||
|
||||
@@ -5,6 +5,8 @@ addon.hdpowerview.description = The Hunter Douglas PowerView binding provides ac
|
||||
|
||||
# thing types
|
||||
|
||||
thing-type.hdpowerview.gateway.label = PowerView Gen3 Gateway
|
||||
thing-type.hdpowerview.gateway.description = Hunter Douglas (Luxaflex) PowerView Generation 3 Gateway/Gateway Pro
|
||||
thing-type.hdpowerview.hub.label = PowerView Hub
|
||||
thing-type.hdpowerview.hub.description = Hunter Douglas (Luxaflex) PowerView Hub
|
||||
thing-type.hdpowerview.repeater.label = PowerView Repeater
|
||||
@@ -12,28 +14,36 @@ thing-type.hdpowerview.repeater.description = Hunter Douglas (Luxaflex) PowerVie
|
||||
thing-type.hdpowerview.repeater.channel.brightness.description = Controls the brightness of the LED ring
|
||||
thing-type.hdpowerview.repeater.channel.color.description = Controls the color of the LED ring
|
||||
thing-type.hdpowerview.shade.label = PowerView Shade
|
||||
thing-type.hdpowerview.shade.description = Hunter Douglas (Luxaflex) PowerView Shade
|
||||
thing-type.hdpowerview.shade.description = Hunter Douglas (Luxaflex) PowerView Gen 1/2 Shade
|
||||
thing-type.hdpowerview.shade.channel.hubRssi.label = Hub RSSI
|
||||
thing-type.hdpowerview.shade.channel.hubRssi.description = Received Signal Strength Indicator for Hub
|
||||
thing-type.hdpowerview.shade.channel.repeaterRssi.label = Repeater RSSI
|
||||
thing-type.hdpowerview.shade.channel.repeaterRssi.description = Received Signal Strength Indicator for Repeater
|
||||
thing-type.hdpowerview.shade.channel.secondary.label = Secondary Position
|
||||
thing-type.hdpowerview.shade.channel.secondary.description = The secondary vertical position (on top-down/bottom-up shades)
|
||||
thing-type.hdpowerview.shade3.label = PowerView Shade
|
||||
thing-type.hdpowerview.shade3.description = Hunter Douglas (Luxaflex) PowerView Gen3 Shade
|
||||
thing-type.hdpowerview.shade3.channel.secondary.label = Secondary Position
|
||||
thing-type.hdpowerview.shade3.channel.secondary.description = The secondary vertical position (on top-down/bottom-up shades)
|
||||
|
||||
# thing types config
|
||||
|
||||
thing-type.config.hdpowerview.gateway.hardRefresh.label = Hard Refresh Interval
|
||||
thing-type.config.hdpowerview.gateway.hardRefresh.description = The number of minutes between hard refreshes of the PowerView Gateway
|
||||
thing-type.config.hdpowerview.gateway.host.label = Host
|
||||
thing-type.config.hdpowerview.gateway.host.description = The host address of the PowerView Gateway
|
||||
thing-type.config.hdpowerview.hub.hardRefresh.label = Hard Position Refresh Interval
|
||||
thing-type.config.hdpowerview.hub.hardRefresh.description = The number of minutes between hard refreshes of positions from the PowerView Hub (or 0 to disable)
|
||||
thing-type.config.hdpowerview.hub.hardRefreshBatteryLevel.label = Hard Battery Level Refresh Interval
|
||||
thing-type.config.hdpowerview.hub.hardRefreshBatteryLevel.description = The number of hours between hard refreshes of battery levels from the PowerView Hub (or 0 to disable, default is weekly)
|
||||
thing-type.config.hdpowerview.hub.host.label = Host
|
||||
thing-type.config.hdpowerview.hub.host.description = The Host address of the PowerView Hub
|
||||
thing-type.config.hdpowerview.hub.host.description = The host address of the PowerView Hub
|
||||
thing-type.config.hdpowerview.hub.refresh.label = Refresh Interval
|
||||
thing-type.config.hdpowerview.hub.refresh.description = The number of milliseconds between fetches of the PowerView Hub shade state
|
||||
thing-type.config.hdpowerview.repeater.id.label = ID
|
||||
thing-type.config.hdpowerview.repeater.id.description = The numeric ID of the PowerView Repeater in the Hub
|
||||
thing-type.config.hdpowerview.shade.id.label = ID
|
||||
thing-type.config.hdpowerview.shade.id.description = The numeric ID of the PowerView Shade in the Hub
|
||||
thing-type.config.hdpowerview.shade.id.description = The numeric ID of the PowerView Shade in the Hub/Gateway
|
||||
|
||||
# channel group types
|
||||
|
||||
@@ -46,6 +56,8 @@ channel-group-type.hdpowerview.scenes.label = Scenes
|
||||
channel-type.hdpowerview.automation-enabled.label = Enable
|
||||
channel-type.hdpowerview.battery-voltage.label = Battery Voltage
|
||||
channel-type.hdpowerview.battery-voltage.description = Battery voltage reported by the shade
|
||||
channel-type.hdpowerview.ble-signal-strength.label = Signal Strength
|
||||
channel-type.hdpowerview.ble-signal-strength.description = Signal strength of Bluetooth Low Energy communication
|
||||
channel-type.hdpowerview.repeater-blinking-enabled.label = Blinking Enabled
|
||||
channel-type.hdpowerview.repeater-blinking-enabled.description = Blink during commands
|
||||
channel-type.hdpowerview.repeater-identify.label = Identify
|
||||
@@ -63,12 +75,20 @@ channel-type.hdpowerview.shade-position.label = Position
|
||||
channel-type.hdpowerview.shade-position.description = The vertical position of the shade
|
||||
channel-type.hdpowerview.shade-vane.label = Vane
|
||||
channel-type.hdpowerview.shade-vane.description = The opening of the slats in the shade
|
||||
channel-type.hdpowerview.shade3-command.label = Command
|
||||
channel-type.hdpowerview.shade3-command.description = Send a command to identify the shade
|
||||
channel-type.hdpowerview.shade3-command.command.option.IDENTIFY = Identify
|
||||
|
||||
# thing status descriptions
|
||||
|
||||
offline.conf-error.no-host-address = Host address must be set
|
||||
offline.conf-error.invalid-bridge-handler = Invalid bridge handler
|
||||
offline.gone.shade-unknown-to-hub = Shade is unknown to Hub
|
||||
offline.gone.shade-unknown-to-hub = Shade is unknown to Hub/Gateway
|
||||
|
||||
# discovery
|
||||
|
||||
discovery.hub.label = PowerView Gen 1/2 Hub ({0})
|
||||
discovery.gateway.label = PowerView Gen 3 Gateway ({0})
|
||||
|
||||
# dynamic channels
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
<config-description>
|
||||
<parameter name="host" type="text" required="true">
|
||||
<label>Host</label>
|
||||
<description>The Host address of the PowerView Hub</description>
|
||||
<description>The host address of the PowerView Hub</description>
|
||||
<context>network-address</context>
|
||||
</parameter>
|
||||
<parameter name="refresh" type="integer" required="false">
|
||||
@@ -47,4 +47,33 @@
|
||||
</config-description>
|
||||
</bridge-type>
|
||||
|
||||
<bridge-type id="gateway">
|
||||
<label>PowerView Gen3 Gateway</label>
|
||||
<description>Hunter Douglas (Luxaflex) PowerView Generation 3 Gateway/Gateway Pro</description>
|
||||
|
||||
<channel-groups>
|
||||
<channel-group id="scenes" typeId="scenes"/>
|
||||
</channel-groups>
|
||||
|
||||
<properties>
|
||||
<property name="vendor">Hunter Douglas (Luxaflex)</property>
|
||||
<property name="modelId">PowerView Gen 3 Gateway</property>
|
||||
</properties>
|
||||
|
||||
<representation-property>host</representation-property>
|
||||
|
||||
<config-description>
|
||||
<parameter name="host" type="text" required="true">
|
||||
<label>Host</label>
|
||||
<description>The host address of the PowerView Gateway</description>
|
||||
<context>network-address</context>
|
||||
</parameter>
|
||||
<parameter name="hardRefresh" type="integer" required="false">
|
||||
<label>Hard Refresh Interval</label>
|
||||
<description>The number of minutes between hard refreshes of the PowerView Gateway</description>
|
||||
<default>180</default>
|
||||
</parameter>
|
||||
</config-description>
|
||||
</bridge-type>
|
||||
|
||||
</thing:thing-descriptions>
|
||||
|
||||
@@ -8,13 +8,15 @@
|
||||
<item-type>Rollershutter</item-type>
|
||||
<label>Position</label>
|
||||
<description>The vertical position of the shade</description>
|
||||
<category>blinds</category>
|
||||
<category>Blinds</category>
|
||||
<state min="0" max="100" pattern="%.1f %%"/>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="shade-vane">
|
||||
<item-type>Dimmer</item-type>
|
||||
<label>Vane</label>
|
||||
<description>The opening of the slats in the shade</description>
|
||||
<state min="0" max="100" pattern="%.1f %%"/>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="shade-command" advanced="true">
|
||||
@@ -30,6 +32,18 @@
|
||||
<autoUpdatePolicy>veto</autoUpdatePolicy>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="shade3-command" advanced="true">
|
||||
<item-type>String</item-type>
|
||||
<label>Command</label>
|
||||
<description>Send a command to identify the shade</description>
|
||||
<command>
|
||||
<options>
|
||||
<option value="IDENTIFY">Identify</option>
|
||||
</options>
|
||||
</command>
|
||||
<autoUpdatePolicy>veto</autoUpdatePolicy>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="scene-activate">
|
||||
<item-type>Switch</item-type>
|
||||
<label>Activate</label>
|
||||
@@ -77,6 +91,14 @@
|
||||
<description>Blink during commands</description>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="ble-signal-strength">
|
||||
<item-type>Number:Power</item-type>
|
||||
<label>Signal Strength</label>
|
||||
<description>Signal strength of Bluetooth Low Energy communication</description>
|
||||
<category>QualityOfService</category>
|
||||
<state readOnly="true" pattern="%.0f dBm"/>
|
||||
</channel-type>
|
||||
|
||||
<channel-group-type id="scenes">
|
||||
<label>Scenes</label>
|
||||
</channel-group-type>
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
<bridge-type-ref id="hub"/>
|
||||
</supported-bridge-type-refs>
|
||||
<label>PowerView Shade</label>
|
||||
<description>Hunter Douglas (Luxaflex) PowerView Shade</description>
|
||||
<description>Hunter Douglas (Luxaflex) PowerView Gen 1/2 Shade</description>
|
||||
|
||||
<channels>
|
||||
<channel id="position" typeId="shade-position"/>
|
||||
@@ -43,4 +43,34 @@
|
||||
<config-description-ref uri="thing-type:hdpowerview:shade"/>
|
||||
</thing-type>
|
||||
|
||||
<thing-type id="shade3">
|
||||
<supported-bridge-type-refs>
|
||||
<bridge-type-ref id="gateway"/>
|
||||
</supported-bridge-type-refs>
|
||||
<label>PowerView Shade</label>
|
||||
<description>Hunter Douglas (Luxaflex) PowerView Gen3 Shade</description>
|
||||
|
||||
<channels>
|
||||
<channel id="position" typeId="shade-position"/>
|
||||
<channel id="secondary" typeId="shade-position">
|
||||
<label>Secondary Position</label>
|
||||
<description>The secondary vertical position (on top-down/bottom-up shades)</description>
|
||||
</channel>
|
||||
<channel id="vane" typeId="shade-vane"/>
|
||||
<channel id="command" typeId="shade3-command"/>
|
||||
<channel id="lowBattery" typeId="system.low-battery"/>
|
||||
<channel id="batteryLevel" typeId="system.battery-level"/>
|
||||
<channel id="signalStrength" typeId="ble-signal-strength"/>
|
||||
</channels>
|
||||
|
||||
<properties>
|
||||
<property name="vendor">Hunter Douglas (Luxaflex)</property>
|
||||
<property name="modelId">PowerView Motorized Shade</property>
|
||||
</properties>
|
||||
|
||||
<representation-property>id</representation-property>
|
||||
|
||||
<config-description-ref uri="thing-type:hdpowerview:shade"/>
|
||||
</thing-type>
|
||||
|
||||
</thing:thing-descriptions>
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>
|
||||
<update:update-descriptions xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:update="https://openhab.org/schemas/update-description/v1.0.0"
|
||||
xsi:schemaLocation="https://openhab.org/schemas/update-description/v1.0.0 https://openhab.org/schemas/update-description-1.0.0.xsd">
|
||||
|
||||
<thing-type uid="powerview:shade">
|
||||
<instruction-set targetVersion="1">
|
||||
<update-channel id="position">
|
||||
<type>powerview:shade-position</type>
|
||||
</update-channel>
|
||||
<update-channel id="secondary">
|
||||
<type>powerview:shade-position</type>
|
||||
<label>Secondary Position</label>
|
||||
<description>The secondary vertical position (on top-down/bottom-up shades)</description>
|
||||
</update-channel>
|
||||
<update-channel id="vane">
|
||||
<type>powerview:shade-vane</type>
|
||||
</update-channel>
|
||||
</instruction-set>
|
||||
</thing-type>
|
||||
|
||||
</update:update-descriptions>
|
||||
@@ -0,0 +1,284 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2023 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.hdpowerview.internal.gen3;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.openhab.binding.hdpowerview.internal.HDPowerViewBindingConstants.*;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.openhab.binding.hdpowerview.internal.HDPowerViewJUnitTests;
|
||||
import org.openhab.binding.hdpowerview.internal.database.ShadeCapabilitiesDatabase;
|
||||
import org.openhab.binding.hdpowerview.internal.database.ShadeCapabilitiesDatabase.Capabilities;
|
||||
import org.openhab.binding.hdpowerview.internal.dto.CoordinateSystem;
|
||||
import org.openhab.binding.hdpowerview.internal.dto.gen3.Scene;
|
||||
import org.openhab.binding.hdpowerview.internal.dto.gen3.SceneEvent;
|
||||
import org.openhab.binding.hdpowerview.internal.dto.gen3.Shade;
|
||||
import org.openhab.binding.hdpowerview.internal.dto.gen3.ShadeEvent;
|
||||
import org.openhab.binding.hdpowerview.internal.dto.gen3.ShadePosition;
|
||||
import org.openhab.binding.hdpowerview.internal.handler.ShadeThingHandler;
|
||||
import org.openhab.core.library.types.DecimalType;
|
||||
import org.openhab.core.library.types.OnOffType;
|
||||
import org.openhab.core.library.types.PercentType;
|
||||
import org.openhab.core.library.types.QuantityType;
|
||||
import org.openhab.core.library.unit.Units;
|
||||
import org.openhab.core.thing.Channel;
|
||||
import org.openhab.core.thing.ChannelUID;
|
||||
import org.openhab.core.thing.Thing;
|
||||
import org.openhab.core.thing.ThingTypeUID;
|
||||
import org.openhab.core.thing.ThingUID;
|
||||
import org.openhab.core.thing.binding.ThingHandlerCallback;
|
||||
import org.openhab.core.thing.binding.builder.ChannelBuilder;
|
||||
import org.openhab.core.thing.binding.builder.ThingBuilder;
|
||||
import org.openhab.core.types.UnDefType;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
|
||||
/**
|
||||
* Unit tests for Generation 3 DTO's.
|
||||
*
|
||||
* @author Andrew Fiddian-Green - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class Generation3DtoTest {
|
||||
|
||||
private final Gson gson = new Gson();
|
||||
private final ShadeCapabilitiesDatabase db = new ShadeCapabilitiesDatabase();
|
||||
|
||||
private String loadJson(String filename) throws IOException {
|
||||
try (InputStream inputStream = HDPowerViewJUnitTests.class.getResourceAsStream(filename)) {
|
||||
if (inputStream == null) {
|
||||
throw new IOException("inputstream is null");
|
||||
}
|
||||
byte[] bytes = inputStream.readAllBytes();
|
||||
if (bytes == null) {
|
||||
throw new IOException("Resulting byte-array empty");
|
||||
}
|
||||
return new String(bytes, StandardCharsets.UTF_8);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test JSON scene event response.
|
||||
*/
|
||||
@Test
|
||||
public void testSceneEventParsing() throws IOException {
|
||||
String json = loadJson("gen3/scene-event.json");
|
||||
SceneEvent sceneEvent = gson.fromJson(json, SceneEvent.class);
|
||||
assertNotNull(sceneEvent);
|
||||
Scene scene = sceneEvent.getScene();
|
||||
assertNotNull(scene);
|
||||
assertEquals("Open All Office Shades\n Open All Office Shades", scene.getName());
|
||||
assertEquals(234, scene.getId());
|
||||
}
|
||||
|
||||
/**
|
||||
* Test JSON scenes response.
|
||||
*/
|
||||
@Test
|
||||
public void testScenesParsing() throws IOException {
|
||||
String json = loadJson("gen3/scenes.json");
|
||||
List<Scene> sceneList = List.of(gson.fromJson(json, Scene[].class));
|
||||
assertNotNull(sceneList);
|
||||
assertEquals(1, sceneList.size());
|
||||
Scene scene = sceneList.get(0);
|
||||
assertEquals("Open All Office Shades\n ABC", scene.getName());
|
||||
assertEquals(234, scene.getId());
|
||||
}
|
||||
|
||||
/**
|
||||
* Test JSON shade event response.
|
||||
*/
|
||||
@Test
|
||||
public void testShadeEventParsing() throws IOException {
|
||||
String json = loadJson("gen3/shade-event.json");
|
||||
ShadeEvent shadeEvent = gson.fromJson(json, ShadeEvent.class);
|
||||
assertNotNull(shadeEvent);
|
||||
ShadePosition position = shadeEvent.getCurrentPositions();
|
||||
assertNotNull(position);
|
||||
assertEquals(PercentType.valueOf("99"), position.getState(CoordinateSystem.PRIMARY_POSITION));
|
||||
assertEquals(PercentType.valueOf("98"), position.getState(CoordinateSystem.SECONDARY_POSITION));
|
||||
assertEquals(PercentType.ZERO, position.getState(CoordinateSystem.VANE_TILT_POSITION));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test JSON shade position setting.
|
||||
*/
|
||||
@Test
|
||||
public void testShadePositions() {
|
||||
ShadePosition pos;
|
||||
|
||||
pos = new ShadePosition();
|
||||
pos.setPosition(CoordinateSystem.PRIMARY_POSITION, new PercentType(11));
|
||||
assertEquals(PercentType.valueOf("11"), pos.getState(CoordinateSystem.PRIMARY_POSITION));
|
||||
assertEquals(UnDefType.UNDEF, pos.getState(CoordinateSystem.SECONDARY_POSITION));
|
||||
assertEquals(UnDefType.UNDEF, pos.getState(CoordinateSystem.VANE_TILT_POSITION));
|
||||
|
||||
pos = new ShadePosition();
|
||||
pos.setPosition(CoordinateSystem.PRIMARY_POSITION, new PercentType(11));
|
||||
pos.setPosition(CoordinateSystem.SECONDARY_POSITION, new PercentType(22));
|
||||
pos.setPosition(CoordinateSystem.VANE_TILT_POSITION, new PercentType(33));
|
||||
assertEquals(PercentType.valueOf("11"), pos.getState(CoordinateSystem.PRIMARY_POSITION));
|
||||
assertEquals(PercentType.valueOf("22"), pos.getState(CoordinateSystem.SECONDARY_POSITION));
|
||||
assertEquals(PercentType.valueOf("33"), pos.getState(CoordinateSystem.VANE_TILT_POSITION));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test JSON shades response.
|
||||
*/
|
||||
@Test
|
||||
public void testShadesParsing() throws IOException {
|
||||
String json = loadJson("gen3/shades.json");
|
||||
List<Shade> shadeList = List.of(gson.fromJson(json, Shade[].class));
|
||||
assertNotNull(shadeList);
|
||||
assertEquals(2, shadeList.size());
|
||||
Shade shadeData = shadeList.get(0);
|
||||
assertEquals("Upper Left", shadeData.getName());
|
||||
assertEquals(2, shadeData.getId());
|
||||
assertFalse(shadeData.isMainsPowered());
|
||||
assertEquals(new DecimalType(66), shadeData.getBatteryLevel());
|
||||
assertEquals(OnOffType.OFF, shadeData.getLowBattery());
|
||||
ShadePosition positions = shadeData.getShadePositions();
|
||||
assertNotNull(positions);
|
||||
Integer caps = shadeData.getCapabilities();
|
||||
assertNotNull(caps);
|
||||
Capabilities capabilities = db.getCapabilities(caps);
|
||||
assertTrue(capabilities.supportsPrimary());
|
||||
assertFalse(capabilities.supportsSecondary());
|
||||
assertFalse(capabilities.supportsTilt180());
|
||||
assertFalse(capabilities.supportsTiltAnywhere());
|
||||
assertFalse(capabilities.supportsTiltOnClosed());
|
||||
|
||||
shadeData = shadeList.get(1);
|
||||
assertEquals(3, shadeData.getId());
|
||||
assertTrue(shadeData.isMainsPowered());
|
||||
positions = shadeData.getShadePositions();
|
||||
assertNotNull(positions);
|
||||
caps = shadeData.getCapabilities();
|
||||
assertNotNull(caps);
|
||||
capabilities = db.getCapabilities(caps);
|
||||
assertTrue(capabilities.supportsPrimary());
|
||||
assertFalse(capabilities.supportsSecondary());
|
||||
assertFalse(capabilities.supportsTilt180());
|
||||
assertFalse(capabilities.supportsTiltAnywhere());
|
||||
assertTrue(capabilities.supportsTiltOnClosed());
|
||||
}
|
||||
|
||||
/**
|
||||
* Test sending properties and dynamic channel values to a shade handler.
|
||||
*/
|
||||
@Test
|
||||
public void testShadeHandlerPropertiesAndChannels() throws IOException {
|
||||
ThingTypeUID thingTypeUID = new ThingTypeUID("hdpowerview:shade");
|
||||
ThingUID thingUID = new ThingUID(thingTypeUID, "test");
|
||||
|
||||
List<Channel> channels = new ArrayList<Channel>();
|
||||
for (String channelId : Set.of(CHANNEL_SHADE_POSITION, CHANNEL_SHADE_SECONDARY_POSITION, CHANNEL_SHADE_VANE,
|
||||
CHANNEL_SHADE_BATTERY_LEVEL, CHANNEL_SHADE_LOW_BATTERY, CHANNEL_SHADE_SIGNAL_STRENGTH)) {
|
||||
ChannelUID channelUID = new ChannelUID(thingUID, channelId);
|
||||
channels.add(ChannelBuilder.create(channelUID).build());
|
||||
}
|
||||
|
||||
String json = loadJson("gen3/shades.json");
|
||||
List<Shade> shadeList = List.of(gson.fromJson(json, Shade[].class));
|
||||
assertNotNull(shadeList);
|
||||
assertEquals(2, shadeList.size());
|
||||
|
||||
Thing thing = ThingBuilder.create(thingTypeUID, thingUID).withChannels(channels).build();
|
||||
ShadeThingHandler shadeThingHandler;
|
||||
Shade shadeData;
|
||||
|
||||
/*
|
||||
* Use the first JSON Shade entry.
|
||||
* It should support 4 dynamic channels.
|
||||
*/
|
||||
shadeThingHandler = new ShadeThingHandler(thing);
|
||||
shadeThingHandler.setCallback(mock(ThingHandlerCallback.class));
|
||||
shadeData = shadeList.get(0).setId(0);
|
||||
assertTrue(shadeData.hasFullState());
|
||||
shadeThingHandler.notify(shadeData);
|
||||
Thing handlerThing = shadeThingHandler.getThing();
|
||||
assertEquals("Duette (6)", handlerThing.getProperties().get("type"));
|
||||
assertEquals("battery", handlerThing.getProperties().get("powerType"));
|
||||
assertEquals("3.0.359", handlerThing.getProperties().get("firmwareVersion"));
|
||||
assertEquals(new QuantityType<>(-50, Units.DECIBEL_MILLIWATTS), shadeData.getSignalStrength());
|
||||
assertEquals(4, handlerThing.getChannels().size());
|
||||
|
||||
/*
|
||||
* Use the second JSON Shade entry.
|
||||
* It should support only 3 dynamic channels.
|
||||
*/
|
||||
shadeThingHandler = new ShadeThingHandler(thing);
|
||||
shadeThingHandler.setCallback(mock(ThingHandlerCallback.class));
|
||||
shadeData = shadeList.get(1).setId(0);
|
||||
assertTrue(shadeData.hasFullState());
|
||||
shadeThingHandler.notify(shadeData);
|
||||
handlerThing = shadeThingHandler.getThing();
|
||||
assertEquals("Silhouette (23)", handlerThing.getProperties().get("type"));
|
||||
assertEquals("hardwired", handlerThing.getProperties().get("powerType"));
|
||||
assertEquals("3.0.359", handlerThing.getProperties().get("firmwareVersion"));
|
||||
assertEquals(new QuantityType<>(-51, Units.DECIBEL_MILLIWATTS), shadeData.getSignalStrength());
|
||||
assertEquals(3, handlerThing.getChannels().size());
|
||||
}
|
||||
|
||||
/**
|
||||
* Test sending state change events to shade handler.
|
||||
*/
|
||||
@Test
|
||||
public void testShadeHandlerEvents() throws IOException {
|
||||
ThingTypeUID thingTypeUID = new ThingTypeUID("hdpowerview:shade");
|
||||
ThingUID thingUID = new ThingUID(thingTypeUID, "test");
|
||||
|
||||
List<Channel> channels = new ArrayList<Channel>();
|
||||
for (String channelId : Set.of(CHANNEL_SHADE_POSITION, CHANNEL_SHADE_SECONDARY_POSITION, CHANNEL_SHADE_VANE,
|
||||
CHANNEL_SHADE_BATTERY_LEVEL, CHANNEL_SHADE_LOW_BATTERY, CHANNEL_SHADE_SIGNAL_STRENGTH)) {
|
||||
ChannelUID channelUID = new ChannelUID(thingUID, channelId);
|
||||
channels.add(ChannelBuilder.create(channelUID).build());
|
||||
}
|
||||
|
||||
String json = loadJson("gen3/shades.json");
|
||||
List<Shade> shadeList = List.of(gson.fromJson(json, Shade[].class));
|
||||
assertNotNull(shadeList);
|
||||
assertEquals(2, shadeList.size());
|
||||
|
||||
Thing thing = ThingBuilder.create(thingTypeUID, thingUID).withChannels(channels).build();
|
||||
ShadeThingHandler shadeThingHandler;
|
||||
Shade shadeData;
|
||||
|
||||
/*
|
||||
* Use the second JSON Shade entry, which only has a primary channel.
|
||||
*/
|
||||
shadeThingHandler = new ShadeThingHandler(thing);
|
||||
shadeThingHandler.setCallback(mock(ThingHandlerCallback.class));
|
||||
shadeData = shadeList.get(1).setId(0);
|
||||
shadeThingHandler.notify(shadeData);
|
||||
|
||||
/*
|
||||
* And try to update it with an event that has all 3 channels.
|
||||
*/
|
||||
json = loadJson("gen3/shade-event.json");
|
||||
ShadeEvent event = gson.fromJson(json, ShadeEvent.class);
|
||||
assertNotNull(event);
|
||||
shadeData = new Shade().setId(0).setShadePosition(event.getCurrentPositions()).setPartialState();
|
||||
assertFalse(shadeData.hasFullState());
|
||||
shadeThingHandler.notify(shadeData);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
[
|
||||
{
|
||||
"id": 33,
|
||||
"type": 6,
|
||||
"enabled": true,
|
||||
"days": "24",
|
||||
"hour": 4,
|
||||
"min": 5,
|
||||
"bleId": 83,
|
||||
"sceneId": 116,
|
||||
"errorShd_Ids": null
|
||||
}
|
||||
]
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"evt": "scene-activated",
|
||||
"isoDate": "2021-12-06T20:01:11.934Z",
|
||||
"id": 55,
|
||||
"scene": {
|
||||
"id": 234,
|
||||
"name": "T3BlbiBBbGwgT2ZmaWNlIFNoYWRlcwo=",
|
||||
"ptName": "Open All Office Shades",
|
||||
"color": "37",
|
||||
"icon": "669",
|
||||
"networkNumber": 7383,
|
||||
"roomIds": [
|
||||
55,
|
||||
66
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
[
|
||||
{
|
||||
"id": 234,
|
||||
"name": "T3BlbiBBbGwgT2ZmaWNlIFNoYWRlcwo=",
|
||||
"ptName": "ABC",
|
||||
"color": "37",
|
||||
"icon": "669",
|
||||
"networkNumber": 7383,
|
||||
"roomIds": [
|
||||
55,
|
||||
66
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"evt": "motion-started",
|
||||
"isoDate": "2021-12-06T20:01:11.934Z",
|
||||
"bleName": "PIR:1233",
|
||||
"id": 55,
|
||||
"currentPositions": {
|
||||
"primary": 0.99,
|
||||
"secondary": 0.98,
|
||||
"tilt": 0
|
||||
},
|
||||
"targetPositions": {
|
||||
"etaInSeconds": 7,
|
||||
"primary": 0.99,
|
||||
"secondary": 0.99,
|
||||
"tilt": 0
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
[
|
||||
{
|
||||
"id": 2,
|
||||
"type": 6,
|
||||
"name": "VXBwZXIgTGVmdA==",
|
||||
"ptName": "Upper Left",
|
||||
"motion": null,
|
||||
"capabilities": 0,
|
||||
"powerType": 0,
|
||||
"batteryStatus": 2,
|
||||
"roomId": 1,
|
||||
"firmware": {
|
||||
"revision": 3,
|
||||
"subRevision": 0,
|
||||
"build": 359
|
||||
},
|
||||
"positions": {
|
||||
"primary": 0.5,
|
||||
"secondary": 0,
|
||||
"tilt": 0,
|
||||
"velocity": 0
|
||||
},
|
||||
"signalStrength": -50,
|
||||
"bleName": "DUE:1444",
|
||||
"shadeGroupIds": [
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"type": 23,
|
||||
"name": "VXBwZXIgUmlnaHQ=",
|
||||
"ptName": "Upper Right",
|
||||
"motion": null,
|
||||
"capabilities": 1,
|
||||
"powerType": 1,
|
||||
"batteryStatus": 3,
|
||||
"roomId": 1,
|
||||
"firmware": {
|
||||
"revision": 3,
|
||||
"subRevision": 0,
|
||||
"build": 359
|
||||
},
|
||||
"positions": {
|
||||
"primary": 0,
|
||||
"secondary": 0,
|
||||
"tilt": 0,
|
||||
"velocity": 0
|
||||
},
|
||||
"signalStrength": -51,
|
||||
"bleName": "SIL:5AA5",
|
||||
"shadeGroupIds": [
|
||||
]
|
||||
}
|
||||
]
|
||||
Reference in New Issue
Block a user