added migrated 2.x add-ons
Signed-off-by: Kai Kreuzer <kai@openhab.org>
This commit is contained in:
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<features name="org.openhab.binding.hdpowerview-${project.version}" xmlns="http://karaf.apache.org/xmlns/features/v1.4.0">
|
||||
<repository>mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features</repository>
|
||||
|
||||
<feature name="openhab-binding-hdpowerview" description="HD PowerView Binding" version="${project.version}">
|
||||
<feature>openhab-runtime-base</feature>
|
||||
<bundle dependency="true">mvn:org.samba.jcifs/jcifs/1.3.17</bundle>
|
||||
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.hdpowerview/${project.version}</bundle>
|
||||
</feature>
|
||||
</features>
|
||||
@@ -0,0 +1,55 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.hdpowerview.internal;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.core.thing.ThingTypeUID;
|
||||
|
||||
/**
|
||||
* The {@link HDPowerViewBinding} class defines common constants, which are
|
||||
* used across the whole binding.
|
||||
*
|
||||
* @author Andy Lintner - Initial contribution
|
||||
* @author Andrew Fiddian-Green - Added support for secondary rail positions
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class HDPowerViewBindingConstants {
|
||||
|
||||
public static final String BINDING_ID = "hdpowerview";
|
||||
|
||||
// List of all Thing Type UIDs
|
||||
public static final ThingTypeUID THING_TYPE_HUB = new ThingTypeUID(BINDING_ID, "hub");
|
||||
public static final ThingTypeUID THING_TYPE_SHADE = new ThingTypeUID(BINDING_ID, "shade");
|
||||
|
||||
// List of all Channel ids
|
||||
public static final String CHANNEL_SHADE_POSITION = "position";
|
||||
public static final String CHANNEL_SHADE_VANE = "vane";
|
||||
public static final String CHANNEL_SHADE_LOW_BATTERY = "lowBattery";
|
||||
public static final String CHANNEL_SHADE_SECONDARY_POSITION = "secondary";
|
||||
|
||||
public static final String CHANNELTYPE_SCENE_ACTIVATE = "scene-activate";
|
||||
|
||||
public static final List<String> NETBIOS_NAMES = Arrays.asList("PDBU-Hub3.0", "PowerView-Hub");
|
||||
|
||||
public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = new HashSet<>();
|
||||
|
||||
static {
|
||||
SUPPORTED_THING_TYPES_UIDS.add(THING_TYPE_HUB);
|
||||
SUPPORTED_THING_TYPES_UIDS.add(THING_TYPE_SHADE);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.hdpowerview.internal;
|
||||
|
||||
import java.util.Hashtable;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.hdpowerview.internal.discovery.HDPowerViewShadeDiscoveryService;
|
||||
import org.openhab.binding.hdpowerview.internal.handler.HDPowerViewHubHandler;
|
||||
import org.openhab.binding.hdpowerview.internal.handler.HDPowerViewShadeHandler;
|
||||
import org.openhab.core.config.discovery.DiscoveryService;
|
||||
import org.openhab.core.thing.Bridge;
|
||||
import org.openhab.core.thing.Thing;
|
||||
import org.openhab.core.thing.ThingTypeUID;
|
||||
import org.openhab.core.thing.binding.BaseThingHandlerFactory;
|
||||
import org.openhab.core.thing.binding.ThingHandler;
|
||||
import org.openhab.core.thing.binding.ThingHandlerFactory;
|
||||
import org.osgi.service.component.annotations.Component;
|
||||
|
||||
/**
|
||||
* The {@link HDPowerViewHandlerFactory} is responsible for creating things and thing
|
||||
* handlers.
|
||||
*
|
||||
* @author Andy Lintner - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
@Component(service = ThingHandlerFactory.class, configurationPid = "binding.hdpowerview")
|
||||
public class HDPowerViewHandlerFactory extends BaseThingHandlerFactory {
|
||||
|
||||
@Override
|
||||
public boolean supportsThingType(ThingTypeUID thingTypeUID) {
|
||||
return HDPowerViewBindingConstants.SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected @Nullable ThingHandler createHandler(Thing thing) {
|
||||
ThingTypeUID thingTypeUID = thing.getThingTypeUID();
|
||||
|
||||
if (thingTypeUID.equals(HDPowerViewBindingConstants.THING_TYPE_HUB)) {
|
||||
HDPowerViewHubHandler handler = new HDPowerViewHubHandler((Bridge) thing);
|
||||
registerService(new HDPowerViewShadeDiscoveryService(handler));
|
||||
return handler;
|
||||
} else if (thingTypeUID.equals(HDPowerViewBindingConstants.THING_TYPE_SHADE)) {
|
||||
return new HDPowerViewShadeHandler(thing);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private void registerService(DiscoveryService discoveryService) {
|
||||
bundleContext.registerService(DiscoveryService.class.getName(), discoveryService, new Hashtable<>());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,240 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.hdpowerview.internal;
|
||||
|
||||
import java.time.Instant;
|
||||
|
||||
import javax.ws.rs.ProcessingException;
|
||||
import javax.ws.rs.client.Client;
|
||||
import javax.ws.rs.client.Entity;
|
||||
import javax.ws.rs.client.Invocation;
|
||||
import javax.ws.rs.client.WebTarget;
|
||||
import javax.ws.rs.core.MediaType;
|
||||
import javax.ws.rs.core.Response;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.hdpowerview.internal.api.ShadePosition;
|
||||
import org.openhab.binding.hdpowerview.internal.api.requests.ShadeMove;
|
||||
import org.openhab.binding.hdpowerview.internal.api.requests.ShadeStop;
|
||||
import org.openhab.binding.hdpowerview.internal.api.responses.Scenes;
|
||||
import org.openhab.binding.hdpowerview.internal.api.responses.Shade;
|
||||
import org.openhab.binding.hdpowerview.internal.api.responses.Shades;
|
||||
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 hub
|
||||
*
|
||||
* @author Andy Lintner - Initial contribution
|
||||
* @author Andrew Fiddian-Green - Added support for secondary rail positions
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class HDPowerViewWebTargets {
|
||||
|
||||
private static final String PUT = "PUT";
|
||||
private static final String GET = "GET";
|
||||
private static final String SCENE_ID = "sceneId";
|
||||
private static final String ID = "id";
|
||||
private static final String REFRESH = "refresh";
|
||||
private static final String CONN_HDR = "Connection";
|
||||
private static final String CONN_VAL = "close"; // versus "keep-alive"
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(HDPowerViewWebTargets.class);
|
||||
|
||||
/*
|
||||
* the hub returns a 423 error (resource locked) daily just after midnight;
|
||||
* which means it is temporarily undergoing maintenance; so we use "soft"
|
||||
* exception handling during the five minute maintenance period after a 423
|
||||
* error is received
|
||||
*/
|
||||
private final int maintenancePeriod = 300;
|
||||
private Instant maintenanceScheduledEnd = Instant.now().minusSeconds(2 * maintenancePeriod);
|
||||
|
||||
private WebTarget base;
|
||||
private WebTarget shades;
|
||||
private WebTarget shade;
|
||||
private WebTarget sceneActivate;
|
||||
private WebTarget scenes;
|
||||
|
||||
private final Gson gson = new Gson();
|
||||
|
||||
/**
|
||||
* Initialize the web targets
|
||||
*
|
||||
* @param client the Javax RS client (the binding)
|
||||
* @param ipAddress the IP address of the server (the hub)
|
||||
*/
|
||||
public HDPowerViewWebTargets(Client client, String ipAddress) {
|
||||
base = client.target("http://" + ipAddress + "/api");
|
||||
shades = base.path("shades/");
|
||||
shade = base.path("shades/{id}");
|
||||
sceneActivate = base.path("scenes");
|
||||
scenes = base.path("scenes/");
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches a JSON package that describes all shades in the hub, and wraps it in
|
||||
* a Shades class instance
|
||||
*
|
||||
* @return Shades class instance
|
||||
* @throws JsonParseException if there is a JSON parsing error
|
||||
* @throws ProcessingException if there is any processing error
|
||||
* @throws HubMaintenanceException if the hub is down for maintenance
|
||||
*/
|
||||
public @Nullable Shades getShades() throws JsonParseException, ProcessingException, HubMaintenanceException {
|
||||
String json = invoke(shades.request().header(CONN_HDR, CONN_VAL).buildGet(), shades, null);
|
||||
return gson.fromJson(json, Shades.class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Instructs the hub to move a specific shade
|
||||
*
|
||||
* @param shadeId id of the shade to be moved
|
||||
* @param position instance of ShadePosition containing the new position
|
||||
* @throws ProcessingException if there is any processing error
|
||||
* @throws HubMaintenanceException if the hub is down for maintenance
|
||||
*/
|
||||
public void moveShade(int shadeId, ShadePosition position) throws ProcessingException, HubMaintenanceException {
|
||||
WebTarget target = shade.resolveTemplate(ID, shadeId);
|
||||
String json = gson.toJson(new ShadeMove(shadeId, position));
|
||||
invoke(target.request().header(CONN_HDR, CONN_VAL)
|
||||
.buildPut(Entity.entity(json, MediaType.APPLICATION_JSON_TYPE)), target, json);
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches a JSON package that describes all scenes in the hub, and wraps it in
|
||||
* a Scenes class instance
|
||||
*
|
||||
* @return Scenes class instance
|
||||
* @throws JsonParseException if there is a JSON parsing error
|
||||
* @throws ProcessingException if there is any processing error
|
||||
* @throws HubMaintenanceException if the hub is down for maintenance
|
||||
*/
|
||||
public @Nullable Scenes getScenes() throws JsonParseException, ProcessingException, HubMaintenanceException {
|
||||
String json = invoke(scenes.request().header(CONN_HDR, CONN_VAL).buildGet(), scenes, null);
|
||||
return gson.fromJson(json, Scenes.class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Instructs the hub to execute a specific scene
|
||||
*
|
||||
* @param sceneId id of the scene to be executed
|
||||
* @throws ProcessingException if there is any processing error
|
||||
* @throws HubMaintenanceException if the hub is down for maintenance
|
||||
*/
|
||||
public void activateScene(int sceneId) throws ProcessingException, HubMaintenanceException {
|
||||
WebTarget target = sceneActivate.queryParam(SCENE_ID, sceneId);
|
||||
invoke(target.request().header(CONN_HDR, CONN_VAL).buildGet(), target, null);
|
||||
}
|
||||
|
||||
private synchronized String invoke(Invocation invocation, WebTarget target, @Nullable String jsonCommand)
|
||||
throws ProcessingException, HubMaintenanceException {
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace("API command {} {}", jsonCommand == null ? GET : PUT, target.getUri());
|
||||
if (jsonCommand != null) {
|
||||
logger.trace("JSON command = {}", jsonCommand);
|
||||
}
|
||||
}
|
||||
Response response;
|
||||
try {
|
||||
response = invocation.invoke();
|
||||
} catch (ProcessingException e) {
|
||||
if (Instant.now().isBefore(maintenanceScheduledEnd)) {
|
||||
// throw "softer" exception during maintenance window
|
||||
logger.debug("Hub still undergoing maintenance");
|
||||
throw new HubMaintenanceException("Hub still undergoing maintenance");
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
int statusCode = response.getStatus();
|
||||
if (statusCode == 423) {
|
||||
// set end of maintenance window, and throw a "softer" exception
|
||||
maintenanceScheduledEnd = Instant.now().plusSeconds(maintenancePeriod);
|
||||
logger.debug("Hub undergoing maintenance");
|
||||
if (response.hasEntity()) {
|
||||
response.readEntity(String.class);
|
||||
}
|
||||
response.close();
|
||||
throw new HubMaintenanceException("Hub undergoing maintenance");
|
||||
}
|
||||
if (statusCode != 200) {
|
||||
logger.warn("Hub returned HTTP error '{}'", statusCode);
|
||||
if (response.hasEntity()) {
|
||||
response.readEntity(String.class);
|
||||
}
|
||||
response.close();
|
||||
throw new ProcessingException(String.format("HTTP %d error", statusCode));
|
||||
}
|
||||
if (!response.hasEntity()) {
|
||||
logger.warn("Hub returned no content");
|
||||
response.close();
|
||||
throw new ProcessingException("Missing response entity");
|
||||
}
|
||||
String jsonResponse = response.readEntity(String.class);
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace("JSON response = {}", jsonResponse);
|
||||
}
|
||||
return jsonResponse;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches a JSON package that describes a specific shade in the hub, and wraps it
|
||||
* in a Shade class instance
|
||||
*
|
||||
* @param shadeId id of the shade to be fetched
|
||||
* @return Shade class instance
|
||||
* @throws ProcessingException if there is any processing error
|
||||
* @throws HubMaintenanceException if the hub is down for maintenance
|
||||
*/
|
||||
public @Nullable Shade getShade(int shadeId) throws ProcessingException, HubMaintenanceException {
|
||||
WebTarget target = shade.resolveTemplate(ID, shadeId);
|
||||
String json = invoke(target.request().header(CONN_HDR, CONN_VAL).buildGet(), target, null);
|
||||
return gson.fromJson(json, Shade.class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Instructs the hub to do a hard refresh (discovery on the hubs RF network) on
|
||||
* a specific shade; fetches a JSON package that describes that shade, and wraps
|
||||
* it in a Shade class instance
|
||||
*
|
||||
* @param shadeId id of the shade to be refreshed
|
||||
* @return Shade class instance
|
||||
* @throws ProcessingException if there is any processing error
|
||||
* @throws HubMaintenanceException if the hub is down for maintenance
|
||||
*/
|
||||
public @Nullable Shade refreshShade(int shadeId) throws ProcessingException, HubMaintenanceException {
|
||||
WebTarget target = shade.resolveTemplate(ID, shadeId).queryParam(REFRESH, true);
|
||||
String json = invoke(target.request().header(CONN_HDR, CONN_VAL).buildGet(), target, null);
|
||||
return gson.fromJson(json, Shade.class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tells the hub to stop movement of a specific shade
|
||||
*
|
||||
* @param shadeId id of the shade to be stopped
|
||||
* @throws ProcessingException if there is any processing error
|
||||
* @throws HubMaintenanceException if the hub is down for maintenance
|
||||
*/
|
||||
public void stopShade(int shadeId) throws ProcessingException, HubMaintenanceException {
|
||||
WebTarget target = shade.resolveTemplate(ID, shadeId);
|
||||
String json = gson.toJson(new ShadeStop(shadeId));
|
||||
invoke(target.request().header(CONN_HDR, CONN_VAL)
|
||||
.buildPut(Entity.entity(json, MediaType.APPLICATION_JSON_TYPE)), target, json);
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.hdpowerview.internal;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
/**
|
||||
* The {@link HubMaintenanceException} is a custom exception for the HD PowerView hub
|
||||
*
|
||||
* @author Andrew Fiddian-Green - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class HubMaintenanceException extends Exception {
|
||||
|
||||
private static final long serialVersionUID = -708582495003057343L;
|
||||
|
||||
public HubMaintenanceException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.hdpowerview.internal.api;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
/**
|
||||
* Actuator class; all shades have a PRIMARY class actuator, plus double action
|
||||
* shades also have a SECONDARY class actuator
|
||||
*
|
||||
* @author Andrew Fiddian-Green - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public enum ActuatorClass {
|
||||
PRIMARY_ACTUATOR,
|
||||
SECONDARY_ACTUATOR;
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.hdpowerview.internal.api;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
/**
|
||||
* Shade coordinate system, as returned by the HD PowerView hub
|
||||
*
|
||||
* @param ZERO_IS_CLOSED coordinate value 0 means shade is closed
|
||||
* @param ZERO_IS_OPEN coordinate value 0 means shade is open
|
||||
* @param VANE_COORDS coordinate system for vanes
|
||||
* @param ERROR_UNKNOWN unsupported coordinate system
|
||||
*
|
||||
* @author Andy Lintner - Initial contribution of the original enum called
|
||||
* ShadePositionKind
|
||||
*
|
||||
* @author Andrew Fiddian-Green - Rewritten as a new enum called
|
||||
* CoordinateSystem to support secondary rail positions and be more
|
||||
* explicit on coordinate directions and ranges
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public enum CoordinateSystem {
|
||||
/*-
|
||||
* Specifies the coordinate system used for the position of the shade. Top-down
|
||||
* shades are in the same coordinate space as bottom-up shades. Shade position
|
||||
* values for top-down shades would be reversed for bottom-up shades. For
|
||||
* example, since 65535 is the open value for a bottom-up shade, it is the
|
||||
* closed value for a top-down shade. The top-down/bottom-up shade is different
|
||||
* in that instead of the top and bottom rail operating in one coordinate space
|
||||
* like the top-down and the bottom-up, it operates in two where the top
|
||||
* (middle) rail closed value is 0 and the bottom (primary) rail closed position
|
||||
* is also 0 and fully open for both is 65535
|
||||
*
|
||||
* The position element can take on multiple states depending on the family of
|
||||
* shade under control.
|
||||
*
|
||||
* The ranges of position integer values are
|
||||
* shades: 0..65535
|
||||
* vanes: 0..32767
|
||||
*
|
||||
* Shade fully up: (top-down: open, bottom-up: closed)
|
||||
* posKind: 1 {ZERO_IS_CLOSED}
|
||||
* position: 65535
|
||||
*
|
||||
* Shade and vane fully down: (top-down: closed, bottom-up: open)
|
||||
* posKind: 1 {ZERO_IS_CLOSED}
|
||||
* position1: 0
|
||||
*
|
||||
* ALTERNATE: Shade and vane fully down: (top-down: closed, bottom-up: open)
|
||||
* posKind: 3 {VANE_COORDS}
|
||||
* position: 0
|
||||
*
|
||||
* Shade fully down (closed) and vane fully up (open):
|
||||
* posKind: 3 {VANE_COORDS}
|
||||
* position: 32767
|
||||
*
|
||||
* Dual action, secondary top-down shade fully up (closed):
|
||||
* posKind: 2 {ZERO_IS_OPEN}
|
||||
* position: 0
|
||||
*
|
||||
* Dual action, secondary top-down shade fully down (open):
|
||||
* posKind: 2 {ZERO_IS_OPEN}
|
||||
* position: 65535
|
||||
*
|
||||
*/
|
||||
ZERO_IS_CLOSED,
|
||||
ZERO_IS_OPEN,
|
||||
VANE_COORDS,
|
||||
ERROR_UNKNOWN;
|
||||
|
||||
public static final int MAX_SHADE = 65535;
|
||||
public static final int MAX_VANE = 32767;
|
||||
|
||||
/**
|
||||
* Converts an HD PowerView posKind integer value to a CoordinateSystem enum value
|
||||
*
|
||||
* @param posKind input integer value
|
||||
* @return corresponding CoordinateSystem enum
|
||||
*/
|
||||
public static CoordinateSystem fromPosKind(int posKind) {
|
||||
switch (posKind) {
|
||||
case 1:
|
||||
return ZERO_IS_CLOSED;
|
||||
case 2:
|
||||
return ZERO_IS_OPEN;
|
||||
case 3:
|
||||
return VANE_COORDS;
|
||||
}
|
||||
return ERROR_UNKNOWN;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a CoordinateSystem enum to an HD PowerView posKind integer value
|
||||
*
|
||||
* @return the posKind integer value
|
||||
*/
|
||||
public int toPosKind() {
|
||||
return ordinal() + 1;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,244 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.hdpowerview.internal.api;
|
||||
|
||||
import static org.openhab.binding.hdpowerview.internal.api.CoordinateSystem.*;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.core.library.types.PercentType;
|
||||
import org.openhab.core.types.*;
|
||||
|
||||
/**
|
||||
* The position of a single shade, as returned by the HD PowerView hub
|
||||
*
|
||||
* @author Andy Lintner - Initial contribution
|
||||
* @author Andrew Fiddian-Green - Added support for secondary rail positions
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class ShadePosition {
|
||||
/**
|
||||
* Primary actuator position
|
||||
*/
|
||||
private int posKind1;
|
||||
private int position1;
|
||||
|
||||
/**
|
||||
* Secondary actuator position
|
||||
*
|
||||
* here we have to use Integer objects rather than just int primitives because
|
||||
* these are secondary optional position elements in the JSON payload, so the
|
||||
* GSON de-serializer might leave them as null
|
||||
*/
|
||||
private @Nullable Integer posKind2 = null;
|
||||
private @Nullable Integer position2 = null;
|
||||
|
||||
/**
|
||||
* Create a ShadePosition position instance with just a primary actuator
|
||||
* position
|
||||
*
|
||||
* @param coordSys the Coordinate System to be used
|
||||
* @param percent the percentage position within that Coordinate System
|
||||
* @return the ShadePosition instance
|
||||
*/
|
||||
public static ShadePosition create(CoordinateSystem coordSys, int percent) {
|
||||
return new ShadePosition(coordSys, percent, null, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a ShadePosition position instance with both a primary and a secondary
|
||||
* actuator position
|
||||
*
|
||||
* @param primaryCoordSys the Coordinate System to be used for the primary
|
||||
* position
|
||||
* @param primaryPercent the percentage position for primary position
|
||||
* @param secondaryCoordSys the Coordinate System to be used for the secondary
|
||||
* position
|
||||
* @param secondaryPercent the percentage position for secondary position
|
||||
* @return the ShadePosition instance
|
||||
*/
|
||||
public static ShadePosition create(CoordinateSystem primaryCoordSys, int primaryPercent,
|
||||
@Nullable CoordinateSystem secondaryCoordSys, @Nullable Integer secondaryPercent) {
|
||||
return new ShadePosition(primaryCoordSys, primaryPercent, secondaryCoordSys, secondaryPercent);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor for ShadePosition position with both a primary and a secondary
|
||||
* actuator position
|
||||
*
|
||||
* @param primaryCoordSys the Coordinate System to be used for the primary
|
||||
* position
|
||||
* @param primaryPercent the percentage position for primary position
|
||||
* @param secondaryCoordSys the Coordinate System to be used for the secondary
|
||||
* position
|
||||
* @param secondaryPercent the percentage position for secondary position
|
||||
*/
|
||||
ShadePosition(CoordinateSystem primaryCoordSys, int primaryPercent, @Nullable CoordinateSystem secondaryCoordSys,
|
||||
@Nullable Integer secondaryPercent) {
|
||||
setPosition1(primaryCoordSys, primaryPercent);
|
||||
setPosition2(secondaryCoordSys, secondaryPercent);
|
||||
}
|
||||
|
||||
/**
|
||||
* For a given Actuator Class and Coordinate System, map the ShadePosition's
|
||||
* state to an OpenHAB State
|
||||
*
|
||||
* @param actuatorClass the requested Actuator Class
|
||||
* @param coordSys the requested Coordinate System
|
||||
* @return the corresponding OpenHAB State
|
||||
*/
|
||||
public State getState(ActuatorClass actuatorClass, CoordinateSystem coordSys) {
|
||||
switch (actuatorClass) {
|
||||
case PRIMARY_ACTUATOR:
|
||||
return getPosition1(coordSys);
|
||||
case SECONDARY_ACTUATOR:
|
||||
return getPosition2(coordSys);
|
||||
default:
|
||||
return UnDefType.UNDEF;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine the Coordinate System used for the given Actuator Class (if any)
|
||||
*
|
||||
* @param actuatorClass the requested Actuator Class
|
||||
* @return the Coordinate System used for that Actuator Class, or ERROR_UNKNOWN
|
||||
* if the Actuator Class is not implemented
|
||||
*/
|
||||
public CoordinateSystem getCoordinateSystem(ActuatorClass actuatorClass) {
|
||||
switch (actuatorClass) {
|
||||
case PRIMARY_ACTUATOR:
|
||||
return fromPosKind(posKind1);
|
||||
case SECONDARY_ACTUATOR:
|
||||
Integer posKind2 = this.posKind2;
|
||||
if (posKind2 != null) {
|
||||
return fromPosKind(posKind2.intValue());
|
||||
}
|
||||
default:
|
||||
return ERROR_UNKNOWN;
|
||||
}
|
||||
}
|
||||
|
||||
private void setPosition1(CoordinateSystem coordSys, int percent) {
|
||||
posKind1 = coordSys.toPosKind();
|
||||
switch (coordSys) {
|
||||
case ZERO_IS_CLOSED:
|
||||
/*-
|
||||
* Primary rail of a single action bottom-up shade, or
|
||||
* Primary, lower, bottom-up, rail of a dual action shade
|
||||
*/
|
||||
case ZERO_IS_OPEN:
|
||||
/*-
|
||||
* Primary rail of a single action top-down shade
|
||||
*
|
||||
* All these types use the same coordinate system; which is inverted in relation
|
||||
* to that of OpenHAB
|
||||
*/
|
||||
position1 = MAX_SHADE - (int) Math.round(percent / 100d * MAX_SHADE);
|
||||
break;
|
||||
case VANE_COORDS:
|
||||
/*
|
||||
* Vane angle of the primary rail of a bottom-up single action shade
|
||||
*/
|
||||
position1 = (int) Math.round(percent / 100d * MAX_VANE);
|
||||
break;
|
||||
default:
|
||||
position1 = 0;
|
||||
}
|
||||
}
|
||||
|
||||
private State getPosition1(CoordinateSystem coordSys) {
|
||||
switch (coordSys) {
|
||||
case ZERO_IS_CLOSED:
|
||||
/*-
|
||||
* Primary rail of a single action bottom-up shade, or
|
||||
* Primary, lower, bottom-up, rail of a dual action shade
|
||||
*/
|
||||
case ZERO_IS_OPEN:
|
||||
/*
|
||||
* Primary rail of a single action top-down shade
|
||||
*
|
||||
* All these types use the same coordinate system; which is inverted in relation
|
||||
* to that of OpenHAB
|
||||
*
|
||||
* If the slats have a defined position then the shade position must by
|
||||
* definition be 100%
|
||||
*/
|
||||
return posKind1 == 3 ? PercentType.HUNDRED
|
||||
: new PercentType(100 - (int) Math.round((double) position1 / MAX_SHADE * 100));
|
||||
|
||||
case VANE_COORDS:
|
||||
/*
|
||||
* Vane angle of the primary rail of a bottom-up single action shade
|
||||
*
|
||||
* If the shades are not open, the vane position is undefined; if the the shades
|
||||
* are exactly open then the vanes are at zero; otherwise return the actual vane
|
||||
* position itself
|
||||
*
|
||||
* note: sometimes the hub may return a value of position1 > MAX_VANE (seems to
|
||||
* be a bug in the hub) so we avoid an out of range exception via the Math.min()
|
||||
* function below..
|
||||
*/
|
||||
return posKind1 != 3 ? (position1 != 0 ? UnDefType.UNDEF : PercentType.ZERO)
|
||||
: new PercentType((int) Math.round((double) Math.min(position1, MAX_VANE) / MAX_VANE * 100));
|
||||
|
||||
default:
|
||||
return UnDefType.UNDEF;
|
||||
}
|
||||
}
|
||||
|
||||
private void setPosition2(@Nullable CoordinateSystem coordSys, @Nullable Integer percent) {
|
||||
if (coordSys == null || percent == null) {
|
||||
return;
|
||||
}
|
||||
posKind2 = Integer.valueOf(coordSys.toPosKind());
|
||||
switch (coordSys) {
|
||||
case ZERO_IS_CLOSED:
|
||||
case ZERO_IS_OPEN:
|
||||
/*
|
||||
* Secondary, upper, top-down rail of a dual action shade
|
||||
*
|
||||
* Uses a coordinate system that is NOT inverted in relation to OpenHAB
|
||||
*/
|
||||
position2 = Integer.valueOf((int) Math.round(percent.doubleValue() / 100 * MAX_SHADE));
|
||||
break;
|
||||
default:
|
||||
position2 = Integer.valueOf(0);
|
||||
}
|
||||
}
|
||||
|
||||
private State getPosition2(CoordinateSystem coordSys) {
|
||||
Integer posKind2 = this.posKind2;
|
||||
Integer position2 = this.position2;
|
||||
if (position2 == null || posKind2 == null) {
|
||||
return UnDefType.UNDEF;
|
||||
}
|
||||
switch (coordSys) {
|
||||
case ZERO_IS_CLOSED:
|
||||
/*
|
||||
* This case should never occur; but return a value anyway just in case
|
||||
*/
|
||||
case ZERO_IS_OPEN:
|
||||
/*
|
||||
* Secondary, upper, top-down rail of a dual action shade
|
||||
*
|
||||
* Uses a coordinate system that is NOT inverted in relation to OpenHAB
|
||||
*/
|
||||
if (posKind2.intValue() != 3) {
|
||||
return new PercentType(100 - (int) Math.round(position2.doubleValue() / MAX_SHADE * 100));
|
||||
}
|
||||
default:
|
||||
return UnDefType.UNDEF;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.hdpowerview.internal.api.requests;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.hdpowerview.internal.api.ShadePosition;
|
||||
|
||||
/**
|
||||
* The position of a shade to set
|
||||
*
|
||||
* @author Andy Lintner - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
class ShadeIdPosition {
|
||||
|
||||
int id;
|
||||
public @Nullable ShadePosition positions;
|
||||
|
||||
public ShadeIdPosition(int id, ShadePosition position) {
|
||||
this.id = id;
|
||||
this.positions = position;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.hdpowerview.internal.api.requests;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* The motion "stop" directive for a shade
|
||||
*
|
||||
* @author Andrew Fiddian-Green - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
class ShadeIdStop {
|
||||
|
||||
int id;
|
||||
public @Nullable String motion;
|
||||
|
||||
public ShadeIdStop(int id) {
|
||||
this.id = id;
|
||||
this.motion = "stop";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.hdpowerview.internal.api.requests;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.hdpowerview.internal.api.ShadePosition;
|
||||
|
||||
/**
|
||||
* A request to set the position of a shade
|
||||
*
|
||||
* @author Andy Lintner - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class ShadeMove {
|
||||
|
||||
public @Nullable ShadeIdPosition shade;
|
||||
|
||||
public ShadeMove(int id, ShadePosition position) {
|
||||
this.shade = new ShadeIdPosition(id, position);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.hdpowerview.internal.api.requests;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* A request to stop the movement of a shade
|
||||
*
|
||||
* @author Andrew Fiddian-Green - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class ShadeStop {
|
||||
|
||||
public @Nullable ShadeIdStop shade;
|
||||
|
||||
public ShadeStop(int id) {
|
||||
this.shade = new ShadeIdStop(id);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.hdpowerview.internal.api.responses;
|
||||
|
||||
import java.util.Base64;
|
||||
import java.util.List;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* State of all Scenes in an HD PowerView hub
|
||||
*
|
||||
* @author Andy Lintner - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class Scenes {
|
||||
|
||||
public @Nullable List<Scene> sceneData;
|
||||
public @Nullable List<Integer> sceneIds;
|
||||
|
||||
/*
|
||||
* the following SuppressWarnings annotation is because the Eclipse compiler
|
||||
* does NOT expect a NonNullByDefault annotation on the inner class, since it is
|
||||
* implicitly inherited from the outer class, whereas the Maven compiler always
|
||||
* requires an explicit NonNullByDefault annotation on all classes
|
||||
*/
|
||||
@SuppressWarnings("null")
|
||||
@NonNullByDefault
|
||||
public static class Scene {
|
||||
public int id;
|
||||
public @Nullable String name;
|
||||
public int roomId;
|
||||
public int order;
|
||||
public int colorId;
|
||||
public int iconId;
|
||||
|
||||
public String getName() {
|
||||
return new String(Base64.getDecoder().decode(name));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.hdpowerview.internal.api.responses;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.hdpowerview.internal.api.responses.Shades.ShadeData;
|
||||
|
||||
/**
|
||||
* State of a single Shade, as returned by an HD PowerView hub
|
||||
*
|
||||
* @author Andrew Fiddian-Green - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class Shade {
|
||||
|
||||
public @Nullable ShadeData shade;
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.hdpowerview.internal.api.responses;
|
||||
|
||||
import java.util.Base64;
|
||||
import java.util.List;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.hdpowerview.internal.api.ShadePosition;
|
||||
|
||||
/**
|
||||
* State of all Shades, as returned by an HD PowerView hub
|
||||
*
|
||||
* @author Andy Lintner - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class Shades {
|
||||
|
||||
public @Nullable List<ShadeData> shadeData;
|
||||
public @Nullable List<Integer> shadeIds;
|
||||
|
||||
/*
|
||||
* the following SuppressWarnings annotation is because the Eclipse compiler
|
||||
* does NOT expect a NonNullByDefault annotation on the inner class, since it is
|
||||
* implicitly inherited from the outer class, whereas the Maven compiler always
|
||||
* requires an explicit NonNullByDefault annotation on all classes
|
||||
*/
|
||||
@SuppressWarnings("null")
|
||||
@NonNullByDefault
|
||||
public static class ShadeData {
|
||||
public int id;
|
||||
public @Nullable String name;
|
||||
public int roomId;
|
||||
public int groupId;
|
||||
public int order;
|
||||
public int type;
|
||||
public double batteryStrength;
|
||||
public int batteryStatus;
|
||||
public boolean batteryIsLow;
|
||||
public @Nullable ShadePosition positions;
|
||||
public @Nullable Boolean timedOut;
|
||||
|
||||
public String getName() {
|
||||
return new String(Base64.getDecoder().decode(name));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.hdpowerview.internal.config;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* Basic configuration for the HD PowerView hub
|
||||
*
|
||||
* @author Andy Lintner - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class HDPowerViewHubConfiguration {
|
||||
|
||||
public static final String HOST = "host";
|
||||
|
||||
public @Nullable String host;
|
||||
|
||||
public long refresh;
|
||||
public long hardRefresh;
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.hdpowerview.internal.config;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
/**
|
||||
* Basic configuration for an HD PowerView Scene
|
||||
*
|
||||
* @author Andy Lintner - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class HDPowerViewSceneConfiguration {
|
||||
|
||||
public static final String ID = "id";
|
||||
|
||||
public int id;
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.hdpowerview.internal.config;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* Basic configuration for an HD PowerView Shade
|
||||
*
|
||||
* @author Andy Lintner - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class HDPowerViewShadeConfiguration {
|
||||
|
||||
public static final String ID = "id";
|
||||
|
||||
public @Nullable String id;
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.hdpowerview.internal.discovery;
|
||||
|
||||
import static org.openhab.binding.hdpowerview.internal.HDPowerViewBindingConstants.*;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Set;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
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 hubs by means of mDNS
|
||||
*
|
||||
* @author Andrew Fiddian-Green - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
@Component(immediate = true)
|
||||
public class HDPowerViewHubDiscoveryParticipant implements MDNSDiscoveryParticipant {
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(HDPowerViewHubDiscoveryParticipant.class);
|
||||
|
||||
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");
|
||||
|
||||
@Override
|
||||
public Set<ThingTypeUID> getSupportedThingTypeUIDs() {
|
||||
return Collections.singleton(THING_TYPE_HUB);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getServiceType() {
|
||||
return "_powerview._tcp.local.";
|
||||
}
|
||||
|
||||
@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_HUB, host.replace('.', '_'));
|
||||
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);
|
||||
return hub;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable ThingUID getThingUID(ServiceInfo service) {
|
||||
for (String host : service.getHostAddresses()) {
|
||||
return new ThingUID(THING_TYPE_HUB, host.replace('.', '_'));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.hdpowerview.internal.discovery;
|
||||
|
||||
import static org.openhab.binding.hdpowerview.internal.HDPowerViewBindingConstants.*;
|
||||
|
||||
import java.net.UnknownHostException;
|
||||
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.config.HDPowerViewHubConfiguration;
|
||||
import org.openhab.core.config.discovery.AbstractDiscoveryService;
|
||||
import org.openhab.core.config.discovery.DiscoveryResult;
|
||||
import org.openhab.core.config.discovery.DiscoveryResultBuilder;
|
||||
import org.openhab.core.config.discovery.DiscoveryService;
|
||||
import org.openhab.core.thing.ThingUID;
|
||||
import org.osgi.service.component.annotations.Component;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import jcifs.netbios.NbtAddress;
|
||||
|
||||
/**
|
||||
* Discovers HD PowerView hubs by means of NetBios
|
||||
*
|
||||
* @author Andy Lintner - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
@Component(service = DiscoveryService.class, immediate = true, configurationPid = "discovery.hdpowerview")
|
||||
public class HDPowerViewHubDiscoveryService extends AbstractDiscoveryService {
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(HDPowerViewHubDiscoveryService.class);
|
||||
|
||||
private final Runnable scanner;
|
||||
private @Nullable ScheduledFuture<?> backgroundFuture;
|
||||
|
||||
public HDPowerViewHubDiscoveryService() {
|
||||
super(Collections.singleton(THING_TYPE_HUB), 600, true);
|
||||
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 () -> {
|
||||
for (String netBiosName : NETBIOS_NAMES) {
|
||||
try {
|
||||
NbtAddress address = NbtAddress.getByName(netBiosName);
|
||||
if (address != null) {
|
||||
String host = address.getInetAddress().getHostAddress();
|
||||
ThingUID thingUID = new ThingUID(THING_TYPE_HUB, host.replace('.', '_'));
|
||||
DiscoveryResult hub = DiscoveryResultBuilder.create(thingUID)
|
||||
.withProperty(HDPowerViewHubConfiguration.HOST, host)
|
||||
.withRepresentationProperty(HDPowerViewHubConfiguration.HOST)
|
||||
.withLabel("PowerView Hub (" + host + ")").build();
|
||||
logger.debug("NetBios discovered hub on host '{}'", host);
|
||||
thingDiscovered(hub);
|
||||
}
|
||||
} catch (UnknownHostException e) {
|
||||
// Nothing to do here - the host couldn't be found, likely because it doesn't
|
||||
// exist
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.hdpowerview.internal.discovery;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import javax.ws.rs.ProcessingException;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.hdpowerview.internal.HDPowerViewBindingConstants;
|
||||
import org.openhab.binding.hdpowerview.internal.HDPowerViewWebTargets;
|
||||
import org.openhab.binding.hdpowerview.internal.HubMaintenanceException;
|
||||
import org.openhab.binding.hdpowerview.internal.api.responses.Shades;
|
||||
import org.openhab.binding.hdpowerview.internal.api.responses.Shades.ShadeData;
|
||||
import org.openhab.binding.hdpowerview.internal.config.HDPowerViewShadeConfiguration;
|
||||
import org.openhab.binding.hdpowerview.internal.handler.HDPowerViewHubHandler;
|
||||
import org.openhab.core.config.discovery.AbstractDiscoveryService;
|
||||
import org.openhab.core.config.discovery.DiscoveryResult;
|
||||
import org.openhab.core.config.discovery.DiscoveryResultBuilder;
|
||||
import org.openhab.core.thing.ThingUID;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import com.google.gson.JsonParseException;
|
||||
|
||||
/**
|
||||
* Discovers an HD PowerView Shade from an existing hub
|
||||
*
|
||||
* @author Andy Lintner - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class HDPowerViewShadeDiscoveryService extends AbstractDiscoveryService {
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(HDPowerViewShadeDiscoveryService.class);
|
||||
private final HDPowerViewHubHandler hub;
|
||||
private final Runnable scanner;
|
||||
private @Nullable ScheduledFuture<?> backgroundFuture;
|
||||
|
||||
public HDPowerViewShadeDiscoveryService(HDPowerViewHubHandler hub) {
|
||||
super(Collections.singleton(HDPowerViewBindingConstants.THING_TYPE_SHADE), 600, 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 {
|
||||
HDPowerViewWebTargets webTargets = hub.getWebTargets();
|
||||
if (webTargets == null) {
|
||||
throw new ProcessingException("Web targets not initialized");
|
||||
}
|
||||
Shades shades = webTargets.getShades();
|
||||
if (shades != null && shades.shadeData != null) {
|
||||
ThingUID bridgeUID = hub.getThing().getUID();
|
||||
List<ShadeData> shadesData = shades.shadeData;
|
||||
if (shadesData != null) {
|
||||
for (ShadeData shadeData : shadesData) {
|
||||
if (shadeData.id != 0) {
|
||||
String id = Integer.toString(shadeData.id);
|
||||
ThingUID thingUID = new ThingUID(HDPowerViewBindingConstants.THING_TYPE_SHADE,
|
||||
bridgeUID, id);
|
||||
DiscoveryResult result = DiscoveryResultBuilder.create(thingUID)
|
||||
.withProperty(HDPowerViewShadeConfiguration.ID, id)
|
||||
.withRepresentationProperty(HDPowerViewShadeConfiguration.ID)
|
||||
.withLabel(shadeData.getName()).withBridge(bridgeUID).build();
|
||||
logger.debug("Hub discovered shade '{}'", id);
|
||||
thingDiscovered(result);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (ProcessingException | JsonParseException e) {
|
||||
logger.warn("Unexpected error: {}", e.getMessage());
|
||||
} catch (HubMaintenanceException e) {
|
||||
// exceptions are logged in HDPowerViewWebTargets
|
||||
}
|
||||
stopScan();
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.hdpowerview.internal.handler;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.core.thing.Bridge;
|
||||
import org.openhab.core.thing.Thing;
|
||||
import org.openhab.core.thing.binding.BaseThingHandler;
|
||||
import org.openhab.core.thing.binding.ThingHandler;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* Abstract class for Things that are managed through an HD PowerView hub
|
||||
*
|
||||
* @author Andy Lintner - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
abstract class AbstractHubbedThingHandler extends BaseThingHandler {
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(AbstractHubbedThingHandler.class);
|
||||
|
||||
public AbstractHubbedThingHandler(Thing thing) {
|
||||
super(thing);
|
||||
}
|
||||
|
||||
protected @Nullable HDPowerViewHubHandler getBridgeHandler() {
|
||||
Bridge bridge = getBridge();
|
||||
if (bridge == null) {
|
||||
logger.error("Thing {} must belong to a hub", getThing().getThingTypeUID().getId());
|
||||
return null;
|
||||
}
|
||||
ThingHandler handler = bridge.getHandler();
|
||||
if (!(handler instanceof HDPowerViewHubHandler)) {
|
||||
logger.debug("Thing {} belongs to the wrong hub type", getThing().getThingTypeUID().getId());
|
||||
return null;
|
||||
}
|
||||
return (HDPowerViewHubHandler) handler;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,319 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.hdpowerview.internal.handler;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import javax.ws.rs.ProcessingException;
|
||||
import javax.ws.rs.client.Client;
|
||||
import javax.ws.rs.client.ClientBuilder;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.hdpowerview.internal.HDPowerViewBindingConstants;
|
||||
import org.openhab.binding.hdpowerview.internal.HDPowerViewWebTargets;
|
||||
import org.openhab.binding.hdpowerview.internal.HubMaintenanceException;
|
||||
import org.openhab.binding.hdpowerview.internal.api.responses.Scenes;
|
||||
import org.openhab.binding.hdpowerview.internal.api.responses.Scenes.Scene;
|
||||
import org.openhab.binding.hdpowerview.internal.api.responses.Shades;
|
||||
import org.openhab.binding.hdpowerview.internal.api.responses.Shades.ShadeData;
|
||||
import org.openhab.binding.hdpowerview.internal.config.HDPowerViewHubConfiguration;
|
||||
import org.openhab.binding.hdpowerview.internal.config.HDPowerViewShadeConfiguration;
|
||||
import org.openhab.core.library.types.OnOffType;
|
||||
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.BaseBridgeHandler;
|
||||
import org.openhab.core.thing.binding.ThingHandler;
|
||||
import org.openhab.core.thing.binding.builder.ChannelBuilder;
|
||||
import org.openhab.core.thing.type.ChannelTypeUID;
|
||||
import org.openhab.core.types.Command;
|
||||
import org.openhab.core.types.RefreshType;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import com.google.gson.JsonParseException;
|
||||
|
||||
/**
|
||||
* The {@link HDPowerViewHubHandler} is responsible for handling commands, which
|
||||
* are sent to one of the channels.
|
||||
*
|
||||
* @author Andy Lintner - Initial contribution
|
||||
* @author Andrew Fiddian-Green - Added support for secondary rail positions
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class HDPowerViewHubHandler extends BaseBridgeHandler {
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(HDPowerViewHubHandler.class);
|
||||
|
||||
private long refreshInterval;
|
||||
private long hardRefreshInterval;
|
||||
|
||||
private final Client client = ClientBuilder.newClient();
|
||||
private @Nullable HDPowerViewWebTargets webTargets;
|
||||
private @Nullable ScheduledFuture<?> pollFuture;
|
||||
private @Nullable ScheduledFuture<?> hardRefreshFuture;
|
||||
|
||||
private final ChannelTypeUID sceneChannelTypeUID = new ChannelTypeUID(HDPowerViewBindingConstants.BINDING_ID,
|
||||
HDPowerViewBindingConstants.CHANNELTYPE_SCENE_ACTIVATE);
|
||||
|
||||
public HDPowerViewHubHandler(Bridge bridge) {
|
||||
super(bridge);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleCommand(ChannelUID channelUID, Command command) {
|
||||
if (RefreshType.REFRESH.equals(command)) {
|
||||
requestRefreshShades();
|
||||
return;
|
||||
}
|
||||
|
||||
Channel channel = getThing().getChannel(channelUID.getId());
|
||||
if (channel != null && sceneChannelTypeUID.equals(channel.getChannelTypeUID())) {
|
||||
if (OnOffType.ON.equals(command)) {
|
||||
try {
|
||||
HDPowerViewWebTargets webTargets = this.webTargets;
|
||||
if (webTargets == null) {
|
||||
throw new ProcessingException("Web targets not initialized");
|
||||
}
|
||||
webTargets.activateScene(Integer.parseInt(channelUID.getId()));
|
||||
} catch (HubMaintenanceException e) {
|
||||
// exceptions are logged in HDPowerViewWebTargets
|
||||
} catch (NumberFormatException | ProcessingException e) {
|
||||
logger.debug("Unexpected error {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initialize() {
|
||||
logger.debug("Initializing hub");
|
||||
HDPowerViewHubConfiguration config = getConfigAs(HDPowerViewHubConfiguration.class);
|
||||
String host = config.host;
|
||||
|
||||
if (host == null || host.isEmpty()) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Host address must be set");
|
||||
return;
|
||||
}
|
||||
|
||||
webTargets = new HDPowerViewWebTargets(client, host);
|
||||
refreshInterval = config.refresh;
|
||||
hardRefreshInterval = config.hardRefresh;
|
||||
schedulePoll();
|
||||
}
|
||||
|
||||
public @Nullable HDPowerViewWebTargets getWebTargets() {
|
||||
return webTargets;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleRemoval() {
|
||||
super.handleRemoval();
|
||||
stopPoll();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose() {
|
||||
super.dispose();
|
||||
stopPoll();
|
||||
}
|
||||
|
||||
private void schedulePoll() {
|
||||
ScheduledFuture<?> future = this.pollFuture;
|
||||
if (future != null) {
|
||||
future.cancel(false);
|
||||
}
|
||||
logger.debug("Scheduling poll for 5000ms out, then every {}ms", refreshInterval);
|
||||
this.pollFuture = scheduler.scheduleWithFixedDelay(this::poll, 5000, refreshInterval, TimeUnit.MILLISECONDS);
|
||||
|
||||
future = this.hardRefreshFuture;
|
||||
if (future != null) {
|
||||
future.cancel(false);
|
||||
}
|
||||
if (hardRefreshInterval > 0) {
|
||||
logger.debug("Scheduling hard refresh every {}minutes", hardRefreshInterval);
|
||||
this.hardRefreshFuture = scheduler.scheduleWithFixedDelay(this::requestRefreshShades, 1,
|
||||
hardRefreshInterval, TimeUnit.MINUTES);
|
||||
}
|
||||
}
|
||||
|
||||
private synchronized void stopPoll() {
|
||||
ScheduledFuture<?> future = this.pollFuture;
|
||||
if (future != null) {
|
||||
future.cancel(true);
|
||||
}
|
||||
this.pollFuture = null;
|
||||
|
||||
future = this.hardRefreshFuture;
|
||||
if (future != null) {
|
||||
future.cancel(true);
|
||||
}
|
||||
this.hardRefreshFuture = null;
|
||||
}
|
||||
|
||||
private synchronized void poll() {
|
||||
try {
|
||||
logger.debug("Polling for state");
|
||||
pollShades();
|
||||
pollScenes();
|
||||
} catch (JsonParseException e) {
|
||||
logger.warn("Bridge returned a bad JSON response: {}", e.getMessage());
|
||||
} catch (ProcessingException e) {
|
||||
logger.warn("Error connecting to bridge: {}", e.getMessage());
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE, e.getMessage());
|
||||
} catch (HubMaintenanceException e) {
|
||||
// exceptions are logged in HDPowerViewWebTargets
|
||||
}
|
||||
}
|
||||
|
||||
private void pollShades() throws JsonParseException, ProcessingException, HubMaintenanceException {
|
||||
HDPowerViewWebTargets webTargets = this.webTargets;
|
||||
if (webTargets == null) {
|
||||
throw new ProcessingException("Web targets not initialized");
|
||||
}
|
||||
|
||||
Shades shades = webTargets.getShades();
|
||||
if (shades == null) {
|
||||
throw new JsonParseException("Missing 'shades' element");
|
||||
}
|
||||
|
||||
List<ShadeData> shadesData = shades.shadeData;
|
||||
if (shadesData == null) {
|
||||
throw new JsonParseException("Missing 'shades.shadeData' element");
|
||||
}
|
||||
|
||||
updateStatus(ThingStatus.ONLINE);
|
||||
logger.debug("Received data for {} shades", shadesData.size());
|
||||
|
||||
Map<String, ShadeData> idShadeDataMap = getIdShadeDataMap(shadesData);
|
||||
Map<Thing, String> thingIdMap = getThingIdMap();
|
||||
for (Entry<Thing, String> item : thingIdMap.entrySet()) {
|
||||
Thing thing = item.getKey();
|
||||
String shadeId = item.getValue();
|
||||
ShadeData shadeData = idShadeDataMap.get(shadeId);
|
||||
updateShadeThing(shadeId, thing, shadeData);
|
||||
}
|
||||
}
|
||||
|
||||
private void updateShadeThing(String shadeId, Thing thing, @Nullable ShadeData shadeData) {
|
||||
HDPowerViewShadeHandler thingHandler = ((HDPowerViewShadeHandler) thing.getHandler());
|
||||
if (thingHandler == null) {
|
||||
logger.debug("Shade '{}' handler not initialized", shadeId);
|
||||
return;
|
||||
}
|
||||
if (shadeData == null) {
|
||||
logger.debug("Shade '{}' has no data in hub", shadeId);
|
||||
} else {
|
||||
logger.debug("Updating shade '{}'", shadeId);
|
||||
}
|
||||
thingHandler.onReceiveUpdate(shadeData);
|
||||
}
|
||||
|
||||
private void pollScenes() throws JsonParseException, ProcessingException, HubMaintenanceException {
|
||||
HDPowerViewWebTargets webTargets = this.webTargets;
|
||||
if (webTargets == null) {
|
||||
throw new ProcessingException("Web targets not initialized");
|
||||
}
|
||||
|
||||
Scenes scenes = webTargets.getScenes();
|
||||
if (scenes == null) {
|
||||
throw new JsonParseException("Missing 'scenes' element");
|
||||
}
|
||||
|
||||
List<Scene> sceneData = scenes.sceneData;
|
||||
if (sceneData == null) {
|
||||
throw new JsonParseException("Missing 'scenes.sceneData' element");
|
||||
}
|
||||
logger.debug("Received data for {} scenes", sceneData.size());
|
||||
|
||||
Map<String, Channel> idChannelMap = getIdChannelMap();
|
||||
for (Scene scene : sceneData) {
|
||||
// remove existing scene channel from the map
|
||||
String sceneId = Integer.toString(scene.id);
|
||||
if (idChannelMap.containsKey(sceneId)) {
|
||||
idChannelMap.remove(sceneId);
|
||||
logger.debug("Keeping channel for existing scene '{}'", sceneId);
|
||||
} else {
|
||||
// create a new scene channel
|
||||
ChannelUID channelUID = new ChannelUID(getThing().getUID(), sceneId);
|
||||
Channel channel = ChannelBuilder.create(channelUID, "Switch").withType(sceneChannelTypeUID)
|
||||
.withLabel(scene.getName()).withDescription("Activates the scene " + scene.getName()).build();
|
||||
updateThing(editThing().withChannel(channel).build());
|
||||
logger.debug("Creating new channel for scene '{}'", sceneId);
|
||||
}
|
||||
}
|
||||
|
||||
// remove any previously created channels that no longer exist
|
||||
if (!idChannelMap.isEmpty()) {
|
||||
logger.debug("Removing {} orphan scene channels", idChannelMap.size());
|
||||
List<Channel> allChannels = new ArrayList<>(getThing().getChannels());
|
||||
allChannels.removeAll(idChannelMap.values());
|
||||
updateThing(editThing().withChannels(allChannels).build());
|
||||
}
|
||||
}
|
||||
|
||||
private Map<Thing, String> getThingIdMap() {
|
||||
Map<Thing, String> ret = new HashMap<>();
|
||||
for (Thing thing : getThing().getThings()) {
|
||||
String id = thing.getConfiguration().as(HDPowerViewShadeConfiguration.class).id;
|
||||
if (id != null && !id.isEmpty()) {
|
||||
ret.put(thing, id);
|
||||
}
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
private Map<String, ShadeData> getIdShadeDataMap(List<ShadeData> shadeData) {
|
||||
Map<String, ShadeData> ret = new HashMap<>();
|
||||
for (ShadeData shade : shadeData) {
|
||||
if (shade.id != 0) {
|
||||
ret.put(Integer.toString(shade.id), shade);
|
||||
}
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
private Map<String, Channel> getIdChannelMap() {
|
||||
Map<String, Channel> ret = new HashMap<>();
|
||||
for (Channel channel : getThing().getChannels()) {
|
||||
if (sceneChannelTypeUID.equals(channel.getChannelTypeUID())) {
|
||||
ret.put(channel.getUID().getId(), channel);
|
||||
}
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
private void requestRefreshShades() {
|
||||
Map<Thing, String> thingIdMap = getThingIdMap();
|
||||
for (Entry<Thing, String> item : thingIdMap.entrySet()) {
|
||||
Thing thing = item.getKey();
|
||||
ThingHandler handler = thing.getHandler();
|
||||
if (handler instanceof HDPowerViewShadeHandler) {
|
||||
((HDPowerViewShadeHandler) handler).requestRefreshShade();
|
||||
} else {
|
||||
String shadeId = item.getValue();
|
||||
logger.debug("Shade '{}' handler not initialized", shadeId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,264 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.hdpowerview.internal.handler;
|
||||
|
||||
import static org.openhab.binding.hdpowerview.internal.HDPowerViewBindingConstants.*;
|
||||
import static org.openhab.binding.hdpowerview.internal.api.ActuatorClass.*;
|
||||
import static org.openhab.binding.hdpowerview.internal.api.CoordinateSystem.*;
|
||||
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import javax.ws.rs.ProcessingException;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.hdpowerview.internal.HDPowerViewWebTargets;
|
||||
import org.openhab.binding.hdpowerview.internal.HubMaintenanceException;
|
||||
import org.openhab.binding.hdpowerview.internal.api.ActuatorClass;
|
||||
import org.openhab.binding.hdpowerview.internal.api.CoordinateSystem;
|
||||
import org.openhab.binding.hdpowerview.internal.api.ShadePosition;
|
||||
import org.openhab.binding.hdpowerview.internal.api.responses.Shade;
|
||||
import org.openhab.binding.hdpowerview.internal.api.responses.Shades.ShadeData;
|
||||
import org.openhab.binding.hdpowerview.internal.config.HDPowerViewShadeConfiguration;
|
||||
import org.openhab.core.library.types.OnOffType;
|
||||
import org.openhab.core.library.types.PercentType;
|
||||
import org.openhab.core.library.types.StopMoveType;
|
||||
import org.openhab.core.library.types.UpDownType;
|
||||
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.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;
|
||||
|
||||
/**
|
||||
* Handles commands for an HD PowerView Shade
|
||||
*
|
||||
* @author Andy Lintner - Initial contribution
|
||||
* @author Andrew Fiddian-Green - Added support for secondary rail positions
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class HDPowerViewShadeHandler extends AbstractHubbedThingHandler {
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(HDPowerViewShadeHandler.class);
|
||||
|
||||
private static final int REFRESH_DELAY_SEC = 10;
|
||||
private @Nullable ScheduledFuture<?> refreshFuture = null;
|
||||
|
||||
public HDPowerViewShadeHandler(Thing thing) {
|
||||
super(thing);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initialize() {
|
||||
try {
|
||||
getShadeId();
|
||||
} catch (NumberFormatException e) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
|
||||
"Configuration 'id' not a valid integer");
|
||||
return;
|
||||
}
|
||||
if (getBridgeHandler() == null) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Hub not configured");
|
||||
return;
|
||||
}
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleCommand(ChannelUID channelUID, Command command) {
|
||||
if (RefreshType.REFRESH.equals(command)) {
|
||||
requestRefreshShade();
|
||||
return;
|
||||
}
|
||||
|
||||
switch (channelUID.getId()) {
|
||||
case CHANNEL_SHADE_POSITION:
|
||||
if (command instanceof PercentType) {
|
||||
moveShade(PRIMARY_ACTUATOR, ZERO_IS_CLOSED, ((PercentType) command).intValue());
|
||||
} else if (command instanceof UpDownType) {
|
||||
moveShade(PRIMARY_ACTUATOR, ZERO_IS_CLOSED, UpDownType.UP.equals(command) ? 0 : 100);
|
||||
} else if (command instanceof StopMoveType) {
|
||||
if (StopMoveType.STOP.equals(command)) {
|
||||
stopShade();
|
||||
} else {
|
||||
logger.warn("Unexpected StopMoveType command");
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case CHANNEL_SHADE_VANE:
|
||||
if (command instanceof PercentType) {
|
||||
moveShade(PRIMARY_ACTUATOR, VANE_COORDS, ((PercentType) command).intValue());
|
||||
} else if (command instanceof OnOffType) {
|
||||
moveShade(PRIMARY_ACTUATOR, VANE_COORDS, OnOffType.ON.equals(command) ? 100 : 0);
|
||||
}
|
||||
break;
|
||||
|
||||
case CHANNEL_SHADE_SECONDARY_POSITION:
|
||||
if (command instanceof PercentType) {
|
||||
moveShade(SECONDARY_ACTUATOR, ZERO_IS_OPEN, ((PercentType) command).intValue());
|
||||
} else if (command instanceof UpDownType) {
|
||||
moveShade(SECONDARY_ACTUATOR, ZERO_IS_OPEN, UpDownType.UP.equals(command) ? 0 : 100);
|
||||
} else if (command instanceof StopMoveType) {
|
||||
if (StopMoveType.STOP.equals(command)) {
|
||||
stopShade();
|
||||
} else {
|
||||
logger.warn("Unexpected StopMoveType command");
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the state of the channels based on the ShadeData provided
|
||||
*
|
||||
* @param shadeData the ShadeData to be used; may be null
|
||||
*/
|
||||
protected void onReceiveUpdate(@Nullable ShadeData shadeData) {
|
||||
if (shadeData != null) {
|
||||
updateStatus(ThingStatus.ONLINE);
|
||||
updateBindingStates(shadeData.positions);
|
||||
updateState(CHANNEL_SHADE_LOW_BATTERY, shadeData.batteryStatus < 2 ? OnOffType.ON : OnOffType.OFF);
|
||||
} else {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
private void updateBindingStates(@Nullable ShadePosition shadePos) {
|
||||
if (shadePos != null) {
|
||||
updateState(CHANNEL_SHADE_POSITION, shadePos.getState(PRIMARY_ACTUATOR, ZERO_IS_CLOSED));
|
||||
updateState(CHANNEL_SHADE_VANE, shadePos.getState(PRIMARY_ACTUATOR, VANE_COORDS));
|
||||
updateState(CHANNEL_SHADE_SECONDARY_POSITION, shadePos.getState(SECONDARY_ACTUATOR, ZERO_IS_OPEN));
|
||||
} else {
|
||||
updateState(CHANNEL_SHADE_POSITION, UnDefType.UNDEF);
|
||||
updateState(CHANNEL_SHADE_VANE, UnDefType.UNDEF);
|
||||
updateState(CHANNEL_SHADE_SECONDARY_POSITION, UnDefType.UNDEF);
|
||||
}
|
||||
}
|
||||
|
||||
private void moveShade(ActuatorClass actuatorClass, CoordinateSystem coordSys, int newPercent) {
|
||||
try {
|
||||
HDPowerViewHubHandler bridge;
|
||||
if ((bridge = getBridgeHandler()) == null) {
|
||||
throw new ProcessingException("Missing bridge handler");
|
||||
}
|
||||
HDPowerViewWebTargets webTargets = bridge.getWebTargets();
|
||||
if (webTargets == null) {
|
||||
throw new ProcessingException("Web targets not initialized");
|
||||
}
|
||||
int shadeId = getShadeId();
|
||||
|
||||
switch (actuatorClass) {
|
||||
case PRIMARY_ACTUATOR:
|
||||
// write the new primary position
|
||||
webTargets.moveShade(shadeId, ShadePosition.create(coordSys, newPercent));
|
||||
break;
|
||||
case SECONDARY_ACTUATOR:
|
||||
// read the current primary position; default value 100%
|
||||
int primaryPercent = 100;
|
||||
Shade shade = webTargets.getShade(shadeId);
|
||||
if (shade != null) {
|
||||
ShadeData shadeData = shade.shade;
|
||||
if (shadeData != null) {
|
||||
ShadePosition shadePos = shadeData.positions;
|
||||
if (shadePos != null) {
|
||||
State primaryState = shadePos.getState(PRIMARY_ACTUATOR, ZERO_IS_CLOSED);
|
||||
if (primaryState instanceof PercentType) {
|
||||
primaryPercent = ((PercentType) primaryState).intValue();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// write the current primary position, plus the new secondary position
|
||||
webTargets.moveShade(shadeId,
|
||||
ShadePosition.create(ZERO_IS_CLOSED, primaryPercent, ZERO_IS_OPEN, newPercent));
|
||||
}
|
||||
} catch (ProcessingException | NumberFormatException e) {
|
||||
logger.warn("Unexpected error: {}", e.getMessage());
|
||||
return;
|
||||
} catch (HubMaintenanceException e) {
|
||||
// exceptions are logged in HDPowerViewWebTargets
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private int getShadeId() throws NumberFormatException {
|
||||
return Integer.parseInt(getConfigAs(HDPowerViewShadeConfiguration.class).id);
|
||||
}
|
||||
|
||||
private void stopShade() {
|
||||
try {
|
||||
HDPowerViewHubHandler bridge;
|
||||
if ((bridge = getBridgeHandler()) == null) {
|
||||
throw new ProcessingException("Missing bridge handler");
|
||||
}
|
||||
HDPowerViewWebTargets webTargets = bridge.getWebTargets();
|
||||
if (webTargets == null) {
|
||||
throw new ProcessingException("Web targets not initialized");
|
||||
}
|
||||
int shadeId = getShadeId();
|
||||
webTargets.stopShade(shadeId);
|
||||
requestRefreshShade();
|
||||
} catch (ProcessingException | NumberFormatException e) {
|
||||
logger.warn("Unexpected error: {}", e.getMessage());
|
||||
return;
|
||||
} catch (HubMaintenanceException e) {
|
||||
// exceptions are logged in HDPowerViewWebTargets
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Request that the shade shall undergo a 'hard' refresh
|
||||
*/
|
||||
protected synchronized void requestRefreshShade() {
|
||||
if (refreshFuture == null) {
|
||||
refreshFuture = scheduler.schedule(this::doRefreshShade, REFRESH_DELAY_SEC, TimeUnit.SECONDS);
|
||||
}
|
||||
}
|
||||
|
||||
private void doRefreshShade() {
|
||||
try {
|
||||
HDPowerViewHubHandler bridge;
|
||||
if ((bridge = getBridgeHandler()) == null) {
|
||||
throw new ProcessingException("Missing bridge handler");
|
||||
}
|
||||
HDPowerViewWebTargets webTargets = bridge.getWebTargets();
|
||||
if (webTargets == null) {
|
||||
throw new ProcessingException("Web targets not initialized");
|
||||
}
|
||||
int shadeId = getShadeId();
|
||||
Shade shade = webTargets.refreshShade(shadeId);
|
||||
if (shade != null) {
|
||||
ShadeData shadeData = shade.shade;
|
||||
if (shadeData != null) {
|
||||
if (Boolean.TRUE.equals(shadeData.timedOut)) {
|
||||
logger.warn("Shade {} wireless refresh time out", shadeId);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (ProcessingException | NumberFormatException e) {
|
||||
logger.warn("Unexpected error: {}", e.getMessage());
|
||||
} catch (HubMaintenanceException e) {
|
||||
// exceptions are logged in HDPowerViewWebTargets
|
||||
}
|
||||
refreshFuture = null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<binding:binding id="hdpowerview" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:binding="https://openhab.org/schemas/binding/v1.0.0"
|
||||
xsi:schemaLocation="https://openhab.org/schemas/binding/v1.0.0 https://openhab.org/schemas/binding-1.0.0.xsd">
|
||||
|
||||
<name>Hunter Douglas PowerView Binding</name>
|
||||
<description>The Hunter Douglas PowerView binding provides access to the Hunter Douglas line of PowerView shades.</description>
|
||||
<author>Andy Lintner</author>
|
||||
|
||||
</binding:binding>
|
||||
@@ -0,0 +1,85 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<thing:thing-descriptions bindingId="hdpowerview"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
|
||||
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
|
||||
|
||||
<bridge-type id="hub">
|
||||
<label>PowerView Hub</label>
|
||||
<description>Hunter Douglas (Luxaflex) PowerView Hub</description>
|
||||
|
||||
<properties>
|
||||
<property name="vendor">Hunter Douglas (Luxaflex)</property>
|
||||
<property name="modelId">PowerView Hub</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 Hub</description>
|
||||
<context>network-address</context>
|
||||
</parameter>
|
||||
<parameter name="refresh" type="integer" required="false">
|
||||
<label>Refresh Interval</label>
|
||||
<description>The number of milliseconds between fetches of the PowerView Hub shade state</description>
|
||||
<default>60000</default>
|
||||
</parameter>
|
||||
<parameter name="hardRefresh" type="integer" required="false">
|
||||
<label>Hard Refresh Interval</label>
|
||||
<description>The number of minutes between hard refreshes of the PowerView Hub (or 0 to disable)</description>
|
||||
<default>180</default>
|
||||
</parameter>
|
||||
</config-description>
|
||||
</bridge-type>
|
||||
|
||||
<thing-type id="shade">
|
||||
<supported-bridge-type-refs>
|
||||
<bridge-type-ref id="hub"/>
|
||||
</supported-bridge-type-refs>
|
||||
<label>PowerView Shade</label>
|
||||
<description>Hunter Douglas (Luxaflex) PowerView 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="lowBattery" typeId="system.low-battery"/>
|
||||
</channels>
|
||||
|
||||
<properties>
|
||||
<property name="vendor">Hunter Douglas (Luxaflex)</property>
|
||||
<property name="modelId">PowerView Motorized Shade</property>
|
||||
</properties>
|
||||
<representation-property>id</representation-property>
|
||||
|
||||
<config-description>
|
||||
<parameter name="id" type="text" required="true">
|
||||
<label>ID</label>
|
||||
<description>The numeric ID of the PowerView Shade in the Hub</description>
|
||||
</parameter>
|
||||
</config-description>
|
||||
</thing-type>
|
||||
|
||||
<channel-type id="shade-position">
|
||||
<item-type>Rollershutter</item-type>
|
||||
<label>Position</label>
|
||||
<description>The vertical position of the shade</description>
|
||||
</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>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="scene-activate">
|
||||
<item-type>Switch</item-type>
|
||||
<label>Activate</label>
|
||||
<description>Activates the scene</description>
|
||||
</channel-type>
|
||||
|
||||
</thing:thing-descriptions>
|
||||
@@ -0,0 +1,337 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.hdpowerview;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
import static org.openhab.binding.hdpowerview.internal.api.ActuatorClass.*;
|
||||
import static org.openhab.binding.hdpowerview.internal.api.CoordinateSystem.*;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.FileReader;
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import javax.ws.rs.ProcessingException;
|
||||
import javax.ws.rs.client.Client;
|
||||
import javax.ws.rs.client.ClientBuilder;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.junit.Test;
|
||||
import org.openhab.binding.hdpowerview.internal.HDPowerViewWebTargets;
|
||||
import org.openhab.binding.hdpowerview.internal.HubMaintenanceException;
|
||||
import org.openhab.binding.hdpowerview.internal.api.CoordinateSystem;
|
||||
import org.openhab.binding.hdpowerview.internal.api.ShadePosition;
|
||||
import org.openhab.binding.hdpowerview.internal.api.responses.Scenes;
|
||||
import org.openhab.binding.hdpowerview.internal.api.responses.Scenes.Scene;
|
||||
import org.openhab.binding.hdpowerview.internal.api.responses.Shade;
|
||||
import org.openhab.binding.hdpowerview.internal.api.responses.Shades;
|
||||
import org.openhab.binding.hdpowerview.internal.api.responses.Shades.ShadeData;
|
||||
import org.openhab.core.library.types.PercentType;
|
||||
import org.openhab.core.types.State;
|
||||
import org.openhab.core.types.UnDefType;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.JsonParseException;
|
||||
|
||||
/**
|
||||
* Unit tests for HD PowerView binding
|
||||
*
|
||||
* @author Andrew Fiddian-Green - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class HDPowerViewJUnitTests {
|
||||
|
||||
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");
|
||||
|
||||
/*
|
||||
* load a test JSON string from a file
|
||||
*/
|
||||
private String loadJson(String fileName) {
|
||||
try (FileReader file = new FileReader(String.format("src/test/resources/%s.json", fileName));
|
||||
BufferedReader reader = new BufferedReader(file)) {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
String line;
|
||||
while ((line = reader.readLine()) != null) {
|
||||
builder.append(line).append("\n");
|
||||
}
|
||||
return builder.toString();
|
||||
} catch (IOException e) {
|
||||
fail(e.getMessage());
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
/**
|
||||
* Run a series of ONLINE tests on the communication with a hub
|
||||
*
|
||||
* @param hubIPAddress must be a valid hub IP address to run the
|
||||
* tests on; or an INVALID IP address to
|
||||
* suppress the tests
|
||||
* @param allowShadeMovementCommands set to true if you accept that the tests
|
||||
* shall physically move the shades
|
||||
*/
|
||||
@Test
|
||||
public void testOnlineCommunication() {
|
||||
/*
|
||||
* NOTE: in order to actually run these tests you must have a hub physically
|
||||
* available, and its IP address must be correctly configured in the
|
||||
* "hubIPAddress" string constant e.g. "192.168.1.123"
|
||||
*/
|
||||
String hubIPAddress = "192.168.1.xxx";
|
||||
|
||||
/*
|
||||
* NOTE: set allowShadeMovementCommands = true if you accept physically moving
|
||||
* the shades during these tests
|
||||
*/
|
||||
boolean allowShadeMovementCommands = false;
|
||||
|
||||
if (VALID_IP_V4_ADDRESS.matcher(hubIPAddress).matches()) {
|
||||
// initialize stuff
|
||||
Client client = ClientBuilder.newClient();
|
||||
assertNotNull(client);
|
||||
// client.register(new Logger());
|
||||
HDPowerViewWebTargets webTargets = new HDPowerViewWebTargets(client, hubIPAddress);
|
||||
assertNotNull(webTargets);
|
||||
|
||||
// ==== exercise some code ====
|
||||
ShadePosition test;
|
||||
State pos;
|
||||
|
||||
// shade fully up
|
||||
test = ShadePosition.create(ZERO_IS_CLOSED, 0);
|
||||
assertNotNull(test);
|
||||
pos = test.getState(PRIMARY_ACTUATOR, ZERO_IS_CLOSED);
|
||||
assertEquals(PercentType.class, pos.getClass());
|
||||
assertEquals(0, ((PercentType) pos).intValue());
|
||||
pos = test.getState(PRIMARY_ACTUATOR, VANE_COORDS);
|
||||
assertTrue(UnDefType.UNDEF.equals(pos));
|
||||
|
||||
// shade fully down (method 1)
|
||||
test = ShadePosition.create(ZERO_IS_CLOSED, 100);
|
||||
assertNotNull(test);
|
||||
pos = test.getState(PRIMARY_ACTUATOR, ZERO_IS_CLOSED);
|
||||
assertEquals(PercentType.class, pos.getClass());
|
||||
assertEquals(100, ((PercentType) pos).intValue());
|
||||
pos = test.getState(PRIMARY_ACTUATOR, VANE_COORDS);
|
||||
assertEquals(PercentType.class, pos.getClass());
|
||||
assertEquals(0, ((PercentType) pos).intValue());
|
||||
|
||||
// shade fully down (method 2)
|
||||
test = ShadePosition.create(VANE_COORDS, 0);
|
||||
assertNotNull(test);
|
||||
pos = test.getState(PRIMARY_ACTUATOR, ZERO_IS_CLOSED);
|
||||
assertEquals(PercentType.class, pos.getClass());
|
||||
assertEquals(100, ((PercentType) pos).intValue());
|
||||
pos = test.getState(PRIMARY_ACTUATOR, VANE_COORDS);
|
||||
assertEquals(PercentType.class, pos.getClass());
|
||||
assertEquals(0, ((PercentType) pos).intValue());
|
||||
|
||||
// shade fully down (method 2) and vane fully open
|
||||
test = ShadePosition.create(VANE_COORDS, 100);
|
||||
assertNotNull(test);
|
||||
pos = test.getState(PRIMARY_ACTUATOR, ZERO_IS_CLOSED);
|
||||
assertEquals(PercentType.class, pos.getClass());
|
||||
assertEquals(100, ((PercentType) pos).intValue());
|
||||
pos = test.getState(PRIMARY_ACTUATOR, VANE_COORDS);
|
||||
assertEquals(PercentType.class, pos.getClass());
|
||||
assertEquals(100, ((PercentType) pos).intValue());
|
||||
|
||||
int shadeId = 0;
|
||||
@Nullable
|
||||
ShadePosition shadePos = null;
|
||||
@Nullable
|
||||
Shades shadesX = null;
|
||||
|
||||
// ==== get all shades ====
|
||||
try {
|
||||
shadesX = webTargets.getShades();
|
||||
assertNotNull(shadesX);
|
||||
@Nullable
|
||||
List<ShadeData> shadesData = shadesX.shadeData;
|
||||
assertNotNull(shadesData);
|
||||
assertTrue(shadesData.size() > 0);
|
||||
@Nullable
|
||||
ShadeData shadeData;
|
||||
shadeData = shadesData.get(0);
|
||||
assertNotNull(shadeData);
|
||||
assertTrue(shadeData.getName().length() > 0);
|
||||
shadePos = shadeData.positions;
|
||||
assertNotNull(shadePos);
|
||||
@Nullable
|
||||
ShadeData shadeZero = shadesData.get(0);
|
||||
assertNotNull(shadeZero);
|
||||
shadeId = shadeZero.id;
|
||||
assertNotEquals(0, shadeId);
|
||||
|
||||
for (ShadeData shadexData : shadesData) {
|
||||
String shadeName = shadexData.getName();
|
||||
assertNotNull(shadeName);
|
||||
}
|
||||
} catch (JsonParseException | ProcessingException | HubMaintenanceException e) {
|
||||
fail(e.getMessage());
|
||||
}
|
||||
|
||||
// ==== get all scenes ====
|
||||
int sceneId = 0;
|
||||
try {
|
||||
Scenes scenes = webTargets.getScenes();
|
||||
assertNotNull(scenes);
|
||||
@Nullable
|
||||
List<Scene> scenesData = scenes.sceneData;
|
||||
assertNotNull(scenesData);
|
||||
assertTrue(scenesData.size() > 0);
|
||||
@Nullable
|
||||
Scene sceneZero = scenesData.get(0);
|
||||
assertNotNull(sceneZero);
|
||||
sceneId = sceneZero.id;
|
||||
assertTrue(sceneId > 0);
|
||||
|
||||
for (Scene scene : scenesData) {
|
||||
String sceneName = scene.getName();
|
||||
assertNotNull(sceneName);
|
||||
}
|
||||
} catch (JsonParseException | ProcessingException | HubMaintenanceException e) {
|
||||
fail(e.getMessage());
|
||||
}
|
||||
|
||||
// ==== refresh a specific shade ====
|
||||
@Nullable
|
||||
Shade shade = null;
|
||||
try {
|
||||
assertNotEquals(0, shadeId);
|
||||
shade = webTargets.refreshShade(shadeId);
|
||||
assertNotNull(shade);
|
||||
} catch (ProcessingException | HubMaintenanceException e) {
|
||||
fail(e.getMessage());
|
||||
}
|
||||
|
||||
// ==== move a specific shade ====
|
||||
try {
|
||||
assertNotEquals(0, shadeId);
|
||||
assertNotNull(shade);
|
||||
@Nullable
|
||||
ShadeData shadeData = shade.shade;
|
||||
assertNotNull(shadeData);
|
||||
ShadePosition positions = shadeData.positions;
|
||||
assertNotNull(positions);
|
||||
CoordinateSystem coordSys = positions.getCoordinateSystem(PRIMARY_ACTUATOR);
|
||||
assertNotNull(coordSys);
|
||||
|
||||
pos = positions.getState(PRIMARY_ACTUATOR, coordSys);
|
||||
assertEquals(PercentType.class, pos.getClass());
|
||||
|
||||
pos = positions.getState(PRIMARY_ACTUATOR, ZERO_IS_CLOSED);
|
||||
assertEquals(PercentType.class, pos.getClass());
|
||||
|
||||
int position = ((PercentType) pos).intValue();
|
||||
position = position + ((position <= 10) ? 5 : -5);
|
||||
|
||||
ShadePosition newPos = ShadePosition.create(ZERO_IS_CLOSED, position);
|
||||
assertNotNull(newPos);
|
||||
|
||||
if (allowShadeMovementCommands) {
|
||||
webTargets.moveShade(shadeId, newPos);
|
||||
}
|
||||
} catch (ProcessingException | HubMaintenanceException e) {
|
||||
fail(e.getMessage());
|
||||
}
|
||||
|
||||
// ==== activate a specific scene ====
|
||||
if (allowShadeMovementCommands) {
|
||||
try {
|
||||
assertNotNull(sceneId);
|
||||
webTargets.activateScene(sceneId);
|
||||
} catch (ProcessingException | HubMaintenanceException e) {
|
||||
fail(e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Run a series of OFFLINE tests on the JSON parsing machinery
|
||||
*/
|
||||
@Test
|
||||
public void testOfflineJsonParsing() {
|
||||
final Gson gson = new Gson();
|
||||
|
||||
@Nullable
|
||||
Shades shades;
|
||||
// test generic JSON shades response
|
||||
try {
|
||||
@Nullable
|
||||
String json = loadJson("shades");
|
||||
assertNotNull(json);
|
||||
assertNotEquals("", json);
|
||||
shades = gson.fromJson(json, Shades.class);
|
||||
assertNotNull(shades);
|
||||
} catch (JsonParseException e) {
|
||||
fail(e.getMessage());
|
||||
}
|
||||
|
||||
// test generic JSON scenes response
|
||||
try {
|
||||
@Nullable
|
||||
String json = loadJson("scenes");
|
||||
assertNotNull(json);
|
||||
assertNotEquals("", json);
|
||||
@Nullable
|
||||
Scenes scenes = gson.fromJson(json, Scenes.class);
|
||||
assertNotNull(scenes);
|
||||
} catch (JsonParseException e) {
|
||||
fail(e.getMessage());
|
||||
}
|
||||
|
||||
// test the JSON parsing for a duette top down bottom up shade
|
||||
try {
|
||||
@Nullable
|
||||
ShadeData shadeData = null;
|
||||
String json = loadJson("duette");
|
||||
assertNotNull(json);
|
||||
assertNotEquals("", json);
|
||||
|
||||
shades = gson.fromJson(json, Shades.class);
|
||||
assertNotNull(shades);
|
||||
@Nullable
|
||||
List<ShadeData> shadesData = shades.shadeData;
|
||||
assertNotNull(shadesData);
|
||||
|
||||
assertEquals(1, shadesData.size());
|
||||
shadeData = shadesData.get(0);
|
||||
assertNotNull(shadeData);
|
||||
|
||||
assertEquals("Gardin 1", shadeData.getName());
|
||||
assertEquals(63778, shadeData.id);
|
||||
|
||||
ShadePosition shadePos = shadeData.positions;
|
||||
assertNotNull(shadePos);
|
||||
assertEquals(ZERO_IS_CLOSED, shadePos.getCoordinateSystem(PRIMARY_ACTUATOR));
|
||||
|
||||
State pos = shadePos.getState(PRIMARY_ACTUATOR, ZERO_IS_CLOSED);
|
||||
assertEquals(PercentType.class, pos.getClass());
|
||||
assertEquals(59, ((PercentType) pos).intValue());
|
||||
|
||||
pos = shadePos.getState(SECONDARY_ACTUATOR, ZERO_IS_OPEN);
|
||||
assertEquals(PercentType.class, pos.getClass());
|
||||
assertEquals(65, ((PercentType) pos).intValue());
|
||||
|
||||
pos = shadePos.getState(PRIMARY_ACTUATOR, VANE_COORDS);
|
||||
assertEquals(UnDefType.class, pos.getClass());
|
||||
} catch (JsonParseException e) {
|
||||
fail(e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"shadeIds": [
|
||||
63778
|
||||
],
|
||||
"shadeData": [
|
||||
{
|
||||
"id": 63778,
|
||||
"type": 8,
|
||||
"batteryStatus": 0,
|
||||
"batteryStrength": 0,
|
||||
"roomId": 891,
|
||||
"firmware": {
|
||||
"revision": 1,
|
||||
"subRevision": 8,
|
||||
"build": 1944
|
||||
},
|
||||
"motor": {
|
||||
"revision": 0,
|
||||
"subRevision": 0,
|
||||
"build": 267
|
||||
},
|
||||
"name": "R2FyZGluIDE=",
|
||||
"groupId": 18108,
|
||||
"positions": {
|
||||
"posKind2": 2,
|
||||
"position2": 23194,
|
||||
"posKind1": 1,
|
||||
"position1": 26566
|
||||
},
|
||||
"signalStrength": 4,
|
||||
"aid": 2,
|
||||
"capabilities": 7,
|
||||
"batteryKind": "unassigned"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
{
|
||||
"sceneIds": [
|
||||
18097,
|
||||
22663,
|
||||
35821,
|
||||
6551
|
||||
],
|
||||
"sceneData": [
|
||||
{
|
||||
"roomId": 59611,
|
||||
"name": "RG9vciBPcGVu",
|
||||
"colorId": 6,
|
||||
"iconId": 160,
|
||||
"networkNumber": 19,
|
||||
"id": 18097,
|
||||
"order": 3,
|
||||
"hkAssist": false
|
||||
},
|
||||
{
|
||||
"roomId": 59611,
|
||||
"name": "SGVhcnQ=",
|
||||
"colorId": 15,
|
||||
"iconId": 49,
|
||||
"networkNumber": 3,
|
||||
"id": 22663,
|
||||
"order": 0,
|
||||
"hkAssist": false
|
||||
},
|
||||
{
|
||||
"roomId": 59611,
|
||||
"name": "Q2xvc2U=",
|
||||
"colorId": 5,
|
||||
"iconId": 31,
|
||||
"networkNumber": 8,
|
||||
"id": 35821,
|
||||
"order": 2,
|
||||
"hkAssist": false
|
||||
},
|
||||
{
|
||||
"roomId": 59611,
|
||||
"name": "T3Blbg==",
|
||||
"colorId": 10,
|
||||
"iconId": 95,
|
||||
"networkNumber": 20,
|
||||
"id": 6551,
|
||||
"order": 1,
|
||||
"hkAssist": false
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
{
|
||||
"shadeIds": [
|
||||
25206,
|
||||
55854,
|
||||
50150
|
||||
],
|
||||
"shadeData": [
|
||||
{
|
||||
"id": 25206,
|
||||
"type": 44,
|
||||
"batteryStatus": 3,
|
||||
"batteryStrength": 179,
|
||||
"roomId": 59611,
|
||||
"firmware": {
|
||||
"revision": 1,
|
||||
"subRevision": 8,
|
||||
"build": 1944
|
||||
},
|
||||
"name": "U2hhZGUgMg==",
|
||||
"motor": {
|
||||
"revision": 51,
|
||||
"subRevision": 51,
|
||||
"build": 11825
|
||||
},
|
||||
"groupId": 64003,
|
||||
"aid": 2,
|
||||
"signalStrength": 4,
|
||||
"capabilities": 0,
|
||||
"batteryKind": "unassigned",
|
||||
"positions": {
|
||||
"posKind1": 3,
|
||||
"position1": 32579
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 55854,
|
||||
"type": 44,
|
||||
"batteryStatus": 3,
|
||||
"batteryStrength": 187,
|
||||
"roomId": 59611,
|
||||
"firmware": {
|
||||
"revision": 1,
|
||||
"subRevision": 8,
|
||||
"build": 1944
|
||||
},
|
||||
"motor": {
|
||||
"revision": 51,
|
||||
"subRevision": 51,
|
||||
"build": 11825
|
||||
},
|
||||
"name": "U2hhZGUgMw==",
|
||||
"groupId": 64003,
|
||||
"aid": 3,
|
||||
"signalStrength": 4,
|
||||
"capabilities": 0,
|
||||
"positions": {
|
||||
"posKind1": 1,
|
||||
"position1": 65534
|
||||
},
|
||||
"batteryKind": "unassigned"
|
||||
},
|
||||
{
|
||||
"id": 50150,
|
||||
"type": 44,
|
||||
"batteryStatus": 3,
|
||||
"batteryStrength": 181,
|
||||
"roomId": 59611,
|
||||
"name": "U2hhZGUgMQ==",
|
||||
"firmware": {
|
||||
"revision": 1,
|
||||
"subRevision": 8,
|
||||
"build": 1944
|
||||
},
|
||||
"motor": {
|
||||
"revision": 51,
|
||||
"subRevision": 51,
|
||||
"build": 11825
|
||||
},
|
||||
"groupId": 64003,
|
||||
"aid": 4,
|
||||
"signalStrength": 4,
|
||||
"capabilities": 0,
|
||||
"batteryKind": "unassigned",
|
||||
"positions": {
|
||||
"posKind1": 1,
|
||||
"position1": 1040
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user