added migrated 2.x add-ons

Signed-off-by: Kai Kreuzer <kai@openhab.org>
This commit is contained in:
Kai Kreuzer
2020-09-21 01:58:32 +02:00
parent bbf1a7fd29
commit 6df6783b60
11662 changed files with 1302875 additions and 11 deletions

View File

@@ -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>

View File

@@ -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);
}
}

View File

@@ -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<>());
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View File

@@ -0,0 +1,27 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.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;
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}
}

View File

@@ -0,0 +1,34 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.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;
}
}

View File

@@ -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";
}
}

View File

@@ -0,0 +1,32 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.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);
}
}

View File

@@ -0,0 +1,31 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.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);
}
}

View File

@@ -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));
}
}
}

View File

@@ -0,0 +1,28 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.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;
}

View File

@@ -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));
}
}
}

View File

@@ -0,0 +1,32 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.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;
}

View File

@@ -0,0 +1,28 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.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;
}

View File

@@ -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;
}

View File

@@ -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;
}
}

View File

@@ -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
}
}
};
}
}

View File

@@ -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();
};
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}
}
}

View File

@@ -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;
}
}

View File

@@ -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>

View File

@@ -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>

View File

@@ -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());
}
}
}

View File

@@ -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"
}
]
}

View File

@@ -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
}
]
}

View File

@@ -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
}
}
]
}