[lutron] Add LEAP protocol support (#8650)

* [lutron] Add LEAP protocol support

Signed-off-by: Bob Adair <bob.github@att.net>
This commit is contained in:
Bob A
2020-10-15 18:59:24 -04:00
committed by GitHub
parent 25826854b4
commit 6f659f2308
65 changed files with 4313 additions and 352 deletions

View File

@@ -28,6 +28,7 @@ public class LutronBindingConstants {
// Bridge Type UIDs
public static final ThingTypeUID THING_TYPE_IPBRIDGE = new ThingTypeUID(BINDING_ID, "ipbridge");
public static final ThingTypeUID THING_TYPE_LEAPBRIDGE = new ThingTypeUID(BINDING_ID, "leapbridge");
// List of all Thing Type UIDs
public static final ThingTypeUID THING_TYPE_DIMMER = new ThingTypeUID(BINDING_ID, "dimmer");
@@ -51,12 +52,15 @@ public class LutronBindingConstants {
public static final ThingTypeUID THING_TYPE_PALLADIOMKEYPAD = new ThingTypeUID(BINDING_ID, "palladiomkeypad");
public static final ThingTypeUID THING_TYPE_WCI = new ThingTypeUID(BINDING_ID, "wci");
public static final ThingTypeUID THING_TYPE_SYSVAR = new ThingTypeUID(BINDING_ID, "sysvar");
public static final ThingTypeUID THING_TYPE_OGROUP = new ThingTypeUID(BINDING_ID, "ogroup");
public static final ThingTypeUID THING_TYPE_FAN = new ThingTypeUID(BINDING_ID, "fan");
// List of all Channel ids
public static final String CHANNEL_LIGHTLEVEL = "lightlevel";
public static final String CHANNEL_SHADELEVEL = "shadelevel";
public static final String CHANNEL_SWITCH = "switchstatus";
public static final String CHANNEL_OCCUPANCYSTATUS = "occupancystatus";
public static final String CHANNEL_GROUPSTATE = "groupstate";
public static final String CHANNEL_CLOCKMODE = "clockmode";
public static final String CHANNEL_SUNRISE = "sunrise";
public static final String CHANNEL_SUNSET = "sunset";
@@ -67,6 +71,9 @@ public class LutronBindingConstants {
public static final String CHANNEL_BLINDLIFTLEVEL = "blindliftlevel";
public static final String CHANNEL_BLINDTILTLEVEL = "blindtiltlevel";
public static final String CHANNEL_VARSTATE = "varstate";
public static final String CHANNEL_FANSPEED = "fanspeed";
public static final String CHANNEL_FANLEVEL = "fanlevel";
public static final String CHANNEL_COMMAND = "command"; // For LEAP bridge debugging
// Bridge config properties (used by discovery service)
public static final String HOST = "ipAddress";

View File

@@ -31,12 +31,15 @@ import org.openhab.binding.lutron.internal.grxprg.PrgConstants;
import org.openhab.binding.lutron.internal.handler.BlindHandler;
import org.openhab.binding.lutron.internal.handler.CcoHandler;
import org.openhab.binding.lutron.internal.handler.DimmerHandler;
import org.openhab.binding.lutron.internal.handler.FanHandler;
import org.openhab.binding.lutron.internal.handler.GrafikEyeKeypadHandler;
import org.openhab.binding.lutron.internal.handler.GreenModeHandler;
import org.openhab.binding.lutron.internal.handler.IPBridgeHandler;
import org.openhab.binding.lutron.internal.handler.IntlKeypadHandler;
import org.openhab.binding.lutron.internal.handler.KeypadHandler;
import org.openhab.binding.lutron.internal.handler.LeapBridgeHandler;
import org.openhab.binding.lutron.internal.handler.MaintainedCcoHandler;
import org.openhab.binding.lutron.internal.handler.OGroupHandler;
import org.openhab.binding.lutron.internal.handler.OccupancySensorHandler;
import org.openhab.binding.lutron.internal.handler.PalladiomKeypadHandler;
import org.openhab.binding.lutron.internal.handler.PicoKeypadHandler;
@@ -87,11 +90,13 @@ public class LutronHandlerFactory extends BaseThingHandlerFactory {
// Used by LutronDeviceDiscoveryService to discover these types
public static final Set<ThingTypeUID> DISCOVERABLE_DEVICE_TYPES_UIDS = Collections
.unmodifiableSet(Stream.of(THING_TYPE_DIMMER, THING_TYPE_SWITCH, THING_TYPE_OCCUPANCYSENSOR,
THING_TYPE_KEYPAD, THING_TYPE_TTKEYPAD, THING_TYPE_INTLKEYPAD, THING_TYPE_PICO,
THING_TYPE_VIRTUALKEYPAD, THING_TYPE_VCRX, THING_TYPE_CCO, THING_TYPE_SHADE, THING_TYPE_TIMECLOCK,
THING_TYPE_GREENMODE, THING_TYPE_QSIO, THING_TYPE_GRAFIKEYEKEYPAD, THING_TYPE_BLIND,
THING_TYPE_PALLADIOMKEYPAD, THING_TYPE_WCI).collect(Collectors.toSet()));
.unmodifiableSet(Stream
.of(THING_TYPE_DIMMER, THING_TYPE_SWITCH, THING_TYPE_OCCUPANCYSENSOR, THING_TYPE_KEYPAD,
THING_TYPE_TTKEYPAD, THING_TYPE_INTLKEYPAD, THING_TYPE_PICO, THING_TYPE_VIRTUALKEYPAD,
THING_TYPE_VCRX, THING_TYPE_CCO, THING_TYPE_SHADE, THING_TYPE_TIMECLOCK,
THING_TYPE_GREENMODE, THING_TYPE_QSIO, THING_TYPE_GRAFIKEYEKEYPAD, THING_TYPE_BLIND,
THING_TYPE_PALLADIOMKEYPAD, THING_TYPE_WCI, THING_TYPE_OGROUP, THING_TYPE_FAN)
.collect(Collectors.toSet()));
// Used by the HwDiscoveryService
public static final Set<ThingTypeUID> HW_DISCOVERABLE_DEVICE_TYPES_UIDS = Collections
@@ -99,11 +104,13 @@ public class LutronHandlerFactory extends BaseThingHandlerFactory {
// Other types that can be initiated but not discovered
private static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Collections
.unmodifiableSet(Stream.of(THING_TYPE_IPBRIDGE, PrgConstants.THING_TYPE_PRGBRIDGE,
PrgConstants.THING_TYPE_GRAFIKEYE, RadioRAConstants.THING_TYPE_RS232,
RadioRAConstants.THING_TYPE_DIMMER, RadioRAConstants.THING_TYPE_SWITCH,
RadioRAConstants.THING_TYPE_PHANTOM, HwConstants.THING_TYPE_HWSERIALBRIDGE, THING_TYPE_CCO_PULSED,
THING_TYPE_CCO_MAINTAINED, THING_TYPE_SYSVAR).collect(Collectors.toSet()));
.unmodifiableSet(Stream
.of(THING_TYPE_IPBRIDGE, THING_TYPE_LEAPBRIDGE, PrgConstants.THING_TYPE_PRGBRIDGE,
PrgConstants.THING_TYPE_GRAFIKEYE, RadioRAConstants.THING_TYPE_RS232,
RadioRAConstants.THING_TYPE_DIMMER, RadioRAConstants.THING_TYPE_SWITCH,
RadioRAConstants.THING_TYPE_PHANTOM, HwConstants.THING_TYPE_HWSERIALBRIDGE,
THING_TYPE_CCO_PULSED, THING_TYPE_CCO_MAINTAINED, THING_TYPE_SYSVAR)
.collect(Collectors.toSet()));
private final Logger logger = LoggerFactory.getLogger(LutronHandlerFactory.class);
@@ -135,6 +142,9 @@ public class LutronHandlerFactory extends BaseThingHandlerFactory {
IPBridgeHandler bridgeHandler = new IPBridgeHandler((Bridge) thing);
registerDiscoveryService(bridgeHandler);
return bridgeHandler;
} else if (thingTypeUID.equals(THING_TYPE_LEAPBRIDGE)) {
LeapBridgeHandler bridgeHandler = new LeapBridgeHandler((Bridge) thing);
return bridgeHandler;
} else if (thingTypeUID.equals(THING_TYPE_DIMMER)) {
return new DimmerHandler(thing);
} else if (thingTypeUID.equals(THING_TYPE_SHADE)) {
@@ -177,6 +187,10 @@ public class LutronHandlerFactory extends BaseThingHandlerFactory {
return new BlindHandler(thing);
} else if (thingTypeUID.equals(THING_TYPE_SYSVAR)) {
return new SysvarHandler(thing);
} else if (thingTypeUID.equals(THING_TYPE_OGROUP)) {
return new OGroupHandler(thing);
} else if (thingTypeUID.equals(THING_TYPE_FAN)) {
return new FanHandler(thing);
} else if (thingTypeUID.equals(PrgConstants.THING_TYPE_PRGBRIDGE)) {
return new PrgBridgeHandler((Bridge) thing);
} else if (thingTypeUID.equals(PrgConstants.THING_TYPE_GRAFIKEYE)) {
@@ -203,7 +217,7 @@ public class LutronHandlerFactory extends BaseThingHandlerFactory {
if (thingHandler instanceof IPBridgeHandler) {
ServiceRegistration<?> serviceReg = discoveryServiceRegMap.remove(thingHandler.getThing().getUID());
if (serviceReg != null) {
logger.debug("Unregistering discovery service.");
logger.debug("Unregistering device discovery service.");
serviceReg.unregister();
}
}
@@ -212,10 +226,10 @@ public class LutronHandlerFactory extends BaseThingHandlerFactory {
/**
* Register a discovery service for an IP bridge handler.
*
* @param bridgeHandler bridge handler for which to register the discovery service
* @param bridgeHandler IP bridge handler for which to register the discovery service
*/
private synchronized void registerDiscoveryService(IPBridgeHandler bridgeHandler) {
logger.debug("Registering discovery service.");
logger.debug("Registering XML device discovery service.");
LutronDeviceDiscoveryService discoveryService = new LutronDeviceDiscoveryService(bridgeHandler, httpClient);
bridgeHandler.setDiscoveryService(discoveryService);
discoveryServiceRegMap.put(bridgeHandler.getThing().getUID(),

View File

@@ -0,0 +1,25 @@
/**
* 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.lutron.internal.config;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Configuration settings for a {@link org.openhab.binding.lutron.internal.handler.FanHandler}.
*
* @author Bob Adair - Initial contribution
*/
@NonNullByDefault
public class FanConfig {
public int integrationId = 0; // Initialize to invalid value
}

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.lutron.internal.config;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* Configuration settings for an {@link org.openhab.binding.lutron.internal.handler.LeapBridgeHandler}.
*
* @author Bob Adair - Initial contribution
*/
@NonNullByDefault
public class LeapBridgeConfig {
public @Nullable String ipAddress;
public int port = 8081;
public @Nullable String keystore;
public @Nullable String keystorePassword;
public boolean certValidate = false;
public int reconnect;
public int heartbeat;
public int delay = 0;
}

View File

@@ -0,0 +1,25 @@
/**
* 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.lutron.internal.config;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Configuration settings for a {@link org.openhab.binding.lutron.internal.handler.OGroupHandler}.
*
* @author Bob Adair - Initial contribution
*/
@NonNullByDefault
public class OGroupConfig {
public int integrationId;
}

View File

@@ -0,0 +1,222 @@
/**
* 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.lutron.internal.discovery;
import static org.openhab.binding.lutron.internal.LutronBindingConstants.*;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.lutron.internal.LutronHandlerFactory;
import org.openhab.binding.lutron.internal.handler.LeapBridgeHandler;
import org.openhab.binding.lutron.internal.protocol.leap.dto.Area;
import org.openhab.binding.lutron.internal.protocol.leap.dto.Device;
import org.openhab.binding.lutron.internal.protocol.leap.dto.OccupancyGroup;
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.ThingTypeUID;
import org.openhab.core.thing.ThingUID;
import org.openhab.core.thing.binding.ThingHandler;
import org.openhab.core.thing.binding.ThingHandlerService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link LeapDeviceDiscoveryService} discovers devices paired with Lutron bridges using the LEAP protocol.
*
* @author Bob Adair - Initial contribution
*/
@NonNullByDefault
public class LeapDeviceDiscoveryService extends AbstractDiscoveryService
implements DiscoveryService, ThingHandlerService {
private static final int DISCOVERY_SERVICE_TIMEOUT = 0; // seconds
private final Logger logger = LoggerFactory.getLogger(LeapDeviceDiscoveryService.class);
/** Area number to name map **/
private @Nullable Map<Integer, String> areaMap;
private @Nullable List<OccupancyGroup> oGroupList;
private @NonNullByDefault({}) LeapBridgeHandler bridgeHandler;
public LeapDeviceDiscoveryService() {
super(LutronHandlerFactory.DISCOVERABLE_DEVICE_TYPES_UIDS, DISCOVERY_SERVICE_TIMEOUT);
}
@Override
public void setThingHandler(ThingHandler handler) {
if (handler instanceof LeapBridgeHandler) {
bridgeHandler = (LeapBridgeHandler) handler;
bridgeHandler.setDiscoveryService(this);
}
}
@Override
public @Nullable ThingHandler getThingHandler() {
return bridgeHandler;
}
@Override
protected void startScan() {
logger.debug("Active discovery scan started");
bridgeHandler.queryDiscoveryData();
}
public void processDeviceDefinitions(List<Device> deviceList) {
for (Device device : deviceList) {
// Integer zoneid = device.getZone();
Integer deviceId = device.getDevice();
String label = device.getFullyQualifiedName();
if (deviceId > 0) {
logger.debug("Discovered device: {} type: {} id: {}", label, device.deviceType, deviceId);
if (device.deviceType != null) {
switch (device.deviceType) {
case "SmartBridge":
case "RA2SelectMainRepeater":
notifyDiscovery(THING_TYPE_VIRTUALKEYPAD, deviceId, label, "model", "Caseta");
break;
case "WallDimmer":
case "PlugInDimmer":
notifyDiscovery(THING_TYPE_DIMMER, deviceId, label);
break;
case "WallSwitch":
case "PlugInSwitch":
notifyDiscovery(THING_TYPE_SWITCH, deviceId, label);
break;
case "CasetaFanSpeedController":
case "MaestroFanSpeedController":
notifyDiscovery(THING_TYPE_FAN, deviceId, label);
break;
case "Pico2Button":
notifyDiscovery(THING_TYPE_PICO, deviceId, label, "model", "2B");
break;
case "Pico2ButtonRaiseLower":
notifyDiscovery(THING_TYPE_PICO, deviceId, label, "model", "2BRL");
break;
case "Pico3ButtonRaiseLower":
notifyDiscovery(THING_TYPE_PICO, deviceId, label, "model", "3BRL");
break;
case "SerenaRollerShade":
case "SerenaHoneycombShade":
case "TriathlonRollerShade":
case "TriathlonHoneycombShade":
case "QsWirelessShade":
notifyDiscovery(THING_TYPE_SHADE, deviceId, label);
break;
case "RPSOccupancySensor":
// Don't discover sensors. Using occupancy groups instead.
break;
default:
logger.info("Unrecognized device type: {}", device.deviceType);
break;
}
}
}
}
}
private void processOccupancyGroups() {
Map<Integer, String> areaMap = this.areaMap;
List<OccupancyGroup> oGroupList = this.oGroupList;
if (areaMap != null && oGroupList != null) {
logger.trace("Processing occupancy groups");
for (OccupancyGroup oGroup : oGroupList) {
logger.trace("Processing OccupancyGroup: {}", oGroup.href);
int groupNum = oGroup.getOccupancyGroup();
// Only process occupancy groups with associated occupancy sensors
if (groupNum > 0 && oGroup.associatedSensors != null) {
String areaName;
if (oGroup.associatedAreas.length > 0) {
// If multiple associated areas are listed, use only the first
areaName = areaMap.get(oGroup.associatedAreas[0].getAreaNumber());
} else {
areaName = "Occupancy Group";
}
logger.debug("Discovered occupancy group: {} areas: {} area name: {}", groupNum,
oGroup.associatedAreas.length, areaName);
notifyDiscovery(THING_TYPE_OGROUP, groupNum, areaName);
}
}
this.areaMap = null;
this.oGroupList = null;
}
}
public void setOccupancyGroups(List<OccupancyGroup> oGroupList) {
logger.trace("Setting occupancy groups list");
this.oGroupList = oGroupList;
if (areaMap != null) {
processOccupancyGroups();
}
}
public void setAreas(List<Area> areaList) {
Map<Integer, String> areaMap = new HashMap<>();
logger.trace("Setting areas map");
for (Area area : areaList) {
int areaNum = area.getArea();
logger.trace("Inserting area into map - num: {} name: {}", areaNum, area.name);
if (areaNum > 0) {
areaMap.put(areaNum, area.name);
} else {
logger.debug("Ignoring area with unparsable href {}", area.href);
}
}
this.areaMap = areaMap;
if (oGroupList != null) {
processOccupancyGroups();
}
}
private void notifyDiscovery(ThingTypeUID thingTypeUID, @Nullable Integer integrationId, String label,
@Nullable String propName, @Nullable Object propValue) {
if (integrationId == null) {
logger.debug("Discovered {} with no integration ID", label);
return;
}
ThingUID bridgeUID = this.bridgeHandler.getThing().getUID();
ThingUID uid = new ThingUID(thingTypeUID, bridgeUID, integrationId.toString());
Map<String, Object> properties = new HashMap<>();
properties.put(INTEGRATION_ID, integrationId);
if (propName != null && propValue != null) {
properties.put(propName, propValue);
}
DiscoveryResult result = DiscoveryResultBuilder.create(uid).withBridge(bridgeUID).withLabel(label)
.withProperties(properties).withRepresentationProperty(INTEGRATION_ID).build();
thingDiscovered(result);
logger.trace("Discovered {}", uid);
}
private void notifyDiscovery(ThingTypeUID thingTypeUID, Integer integrationId, String label) {
notifyDiscovery(thingTypeUID, integrationId, label, null, null);
}
@Override
public void deactivate() {
super.deactivate();
}
}

View File

@@ -94,7 +94,7 @@ public class LutronDeviceDiscoveryService extends AbstractDiscoveryService {
private final Logger logger = LoggerFactory.getLogger(LutronDeviceDiscoveryService.class);
private IPBridgeHandler bridgeHandler;
private final IPBridgeHandler bridgeHandler;
private DbXmlInfoReader dbXmlInfoReader = new DbXmlInfoReader();
private final HttpClient httpClient;
@@ -111,8 +111,9 @@ public class LutronDeviceDiscoveryService extends AbstractDiscoveryService {
@Override
protected synchronized void startScan() {
Future<?> scanTask = this.scanTask;
if (scanTask == null || scanTask.isDone()) {
scanTask = scheduler.submit(this::asyncDiscoveryTask);
this.scanTask = scheduler.submit(this::asyncDiscoveryTask);
}
}
@@ -131,7 +132,7 @@ public class LutronDeviceDiscoveryService extends AbstractDiscoveryService {
private void readDeviceDatabase() {
Project project = null;
if (bridgeHandler == null || bridgeHandler.getIPBridgeConfig() == null) {
if (bridgeHandler.getIPBridgeConfig() == null) {
logger.debug("Unable to get bridge config. Exiting.");
return;
}
@@ -240,9 +241,9 @@ public class LutronDeviceDiscoveryService extends AbstractDiscoveryService {
for (DeviceNode deviceNode : area.getDeviceNodes()) {
if (deviceNode instanceof DeviceGroup) {
processDeviceGroup((DeviceGroup) deviceNode, context);
processDeviceGroup(area, (DeviceGroup) deviceNode, context);
} else if (deviceNode instanceof Device) {
processDevice((Device) deviceNode, context);
processDevice(area, (Device) deviceNode, context);
}
}
@@ -257,17 +258,17 @@ public class LutronDeviceDiscoveryService extends AbstractDiscoveryService {
context.pop();
}
private void processDeviceGroup(DeviceGroup deviceGroup, Stack<String> context) {
private void processDeviceGroup(Area area, DeviceGroup deviceGroup, Stack<String> context) {
context.push(deviceGroup.getName());
for (Device device : deviceGroup.getDevices()) {
processDevice(device, context);
processDevice(area, device, context);
}
context.pop();
}
private void processDevice(Device device, Stack<String> context) {
private void processDevice(Area area, Device device, Stack<String> context) {
List<Integer> buttons;
KeypadConfig kpConfig;
String kpModel;
@@ -280,6 +281,7 @@ public class LutronDeviceDiscoveryService extends AbstractDiscoveryService {
switch (type) {
case MOTION_SENSOR:
notifyDiscovery(THING_TYPE_OCCUPANCYSENSOR, device.getIntegrationId(), label);
notifyDiscovery(THING_TYPE_OGROUP, area.getIntegrationId(), area.getName());
break;
case SEETOUCH_KEYPAD:
@@ -389,10 +391,13 @@ public class LutronDeviceDiscoveryService extends AbstractDiscoveryService {
case FLUORESCENT_DB:
case ZERO_TO_TEN:
case AUTO_DETECT:
case CEILING_FAN_TYPE:
notifyDiscovery(THING_TYPE_DIMMER, output.getIntegrationId(), label);
break;
case CEILING_FAN_TYPE:
notifyDiscovery(THING_TYPE_FAN, output.getIntegrationId(), label);
break;
case NON_DIM:
case NON_DIM_INC:
case NON_DIM_ELV:

View File

@@ -38,8 +38,8 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link LutronMdnsBridgeDiscoveryService} discovers Lutron Caseta Smart Bridge Pro and eventually RA2 Select Main
* Repeater and other Lutron devices on the network using mDNS.
* The {@link LutronMdnsBridgeDiscoveryService} discovers Lutron Caseta Smart Bridge, Caseta Smart Bridge Pro, RA2
* Select Main Repeater, and other Lutron devices on the network using mDNS.
*
* @author Bob Adair - Initial contribution
*/
@@ -51,11 +51,15 @@ public class LutronMdnsBridgeDiscoveryService implements MDNSDiscoveryParticipan
private static final String LUTRON_MDNS_SERVICE_TYPE = "_lutron._tcp.local.";
private static final String PRODFAM_CASETA = "Caseta";
private static final String PRODTYP_CASETA_SB = "Smart Bridge";
private static final String DEVCLASS_CASETA_SB = "08040100";
private static final String PRODTYP_CASETA_SBP2 = "Smart Bridge Pro 2";
private static final String DEVCLASS_CASETA_SBP2 = "08050100";
private static final String PRODFAM_RA2_SELECT = "RA2 Select";
private static final String PRODTYP_RA2_SELECT = "Main Repeater";
private static final String DEVCLASS_RA2_SELECT = "080E0401";
private static final String DEVCLASS_CONNECT_BRIDGE = "08090301";
private static final String DEFAULT_LABEL = "Unknown Lutron bridge";
@@ -108,7 +112,11 @@ public class LutronMdnsBridgeDiscoveryService implements MDNSDiscoveryParticipan
String bridgeHostName = ipAddresses[0].getHostName();
logger.debug("Lutron mDNS bridge hostname: {}", bridgeHostName);
if (DEVCLASS_CASETA_SBP2.equals(devclass)) {
if (DEVCLASS_CASETA_SB.equals(devclass)) {
properties.put(PROPERTY_PRODFAM, PRODFAM_CASETA);
properties.put(PROPERTY_PRODTYP, PRODTYP_CASETA_SB);
label = PRODFAM_CASETA + " " + PRODTYP_CASETA_SB;
} else if (DEVCLASS_CASETA_SBP2.equals(devclass)) {
properties.put(PROPERTY_PRODFAM, PRODFAM_CASETA);
properties.put(PROPERTY_PRODTYP, PRODTYP_CASETA_SBP2);
label = PRODFAM_CASETA + " " + PRODTYP_CASETA_SBP2;
@@ -122,7 +130,7 @@ public class LutronMdnsBridgeDiscoveryService implements MDNSDiscoveryParticipan
} else {
logger.info("Lutron device with unknown DEVCLASS discovered via mDNS: {}. Configure device manually.",
devclass);
return null; // For now, exit if service has unknown DEVCLASS
return null; // Exit if service has unknown DEVCLASS
}
if (!bridgeHostName.equals(ipAddresses[0].getHostAddress())) {
@@ -161,10 +169,15 @@ public class LutronMdnsBridgeDiscoveryService implements MDNSDiscoveryParticipan
@Override
public @Nullable ThingUID getThingUID(ServiceInfo service) {
String serial = getSerial(service);
String devclass = service.getPropertyString("DEVCLASS");
if (serial == null) {
return null;
} else {
return new ThingUID(THING_TYPE_IPBRIDGE, serial);
if (DEVCLASS_CASETA_SB.equals(devclass)) {
return new ThingUID(THING_TYPE_LEAPBRIDGE, serial);
} else {
return new ThingUID(THING_TYPE_IPBRIDGE, serial);
}
}
}

View File

@@ -24,6 +24,7 @@ import java.util.List;
*/
public class Area {
private String name;
private Integer integrationId;
private List<DeviceNode> deviceNodes;
private List<Output> outputs;
private List<Area> areas;
@@ -32,6 +33,10 @@ public class Area {
return name;
}
public Integer getIntegrationId() {
return integrationId;
}
public List<DeviceNode> getDeviceNodes() {
return deviceNodes != null ? deviceNodes : Collections.<DeviceNode> emptyList();
}

View File

@@ -23,7 +23,9 @@ import java.util.concurrent.TimeUnit;
import org.openhab.binding.lutron.internal.KeypadComponent;
import org.openhab.binding.lutron.internal.keypadconfig.KeypadConfig;
import org.openhab.binding.lutron.internal.protocol.LutronCommandType;
import org.openhab.binding.lutron.internal.protocol.DeviceCommand;
import org.openhab.binding.lutron.internal.protocol.lip.LutronCommandType;
import org.openhab.binding.lutron.internal.protocol.lip.TargetType;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.OpenClosedType;
import org.openhab.core.thing.Bridge;
@@ -46,23 +48,14 @@ import org.slf4j.LoggerFactory;
* @author Bob Adair - Initial contribution, based partly on Allan Tong's KeypadHandler class
*/
public abstract class BaseKeypadHandler extends LutronHandler {
protected static final Integer ACTION_PRESS = 3;
protected static final Integer ACTION_RELEASE = 4;
protected static final Integer ACTION_HOLD = 5;
protected static final Integer ACTION_LED_STATE = 9;
protected static final Integer LED_OFF = 0;
protected static final Integer LED_ON = 1;
protected static final Integer LED_FLASH = 2; // Same as 1 on RA2 keypads
protected static final Integer LED_RAPIDFLASH = 3; // Same as 1 on RA2 keypads
private final Logger logger = LoggerFactory.getLogger(BaseKeypadHandler.class);
protected List<KeypadComponent> buttonList = new ArrayList<>();
protected List<KeypadComponent> ledList = new ArrayList<>();
protected List<KeypadComponent> cciList = new ArrayList<>();
Map<Integer, Integer> leapButtonMap;
protected int integrationId;
protected String model;
protected Boolean autoRelease;
@@ -75,6 +68,7 @@ public abstract class BaseKeypadHandler extends LutronHandler {
private final Object asyncInitLock = new Object();
protected KeypadConfig kp;
protected TargetType commandTargetType = TargetType.KEYPAD; // For LEAP bridge
public BaseKeypadHandler(Thing thing) {
super(thing);
@@ -123,7 +117,7 @@ public abstract class BaseKeypadHandler extends LutronHandler {
List<Channel> channelList = new ArrayList<>();
List<Channel> existingChannels = getThing().getChannels();
if (existingChannels != null && !existingChannels.isEmpty()) {
if (!existingChannels.isEmpty()) {
// Clear existing channels
logger.debug("Clearing existing channels for keypad {}", integrationId);
ThingBuilder thingBuilder = editThing();
@@ -261,7 +255,7 @@ public abstract class BaseKeypadHandler extends LutronHandler {
// To reduce query volume, query only 1st LED and LEDs with linked channels.
for (KeypadComponent component : ledList) {
if (component.id() == ledList.get(0).id() || isLinked(channelFromComponent(component.id()))) {
queryDevice(component.id(), ACTION_LED_STATE);
queryDevice(commandTargetType, component.id(), DeviceCommand.ACTION_LED_STATE);
}
}
}
@@ -290,12 +284,12 @@ public abstract class BaseKeypadHandler extends LutronHandler {
// For LEDs, handle RefreshType and OnOffType commands
if (isLed(componentID)) {
if (command instanceof RefreshType) {
queryDevice(componentID, ACTION_LED_STATE);
queryDevice(commandTargetType, componentID, DeviceCommand.ACTION_LED_STATE);
} else if (command instanceof OnOffType) {
if (command == OnOffType.ON) {
device(componentID, ACTION_LED_STATE, LED_ON);
device(commandTargetType, componentID, null, DeviceCommand.ACTION_LED_STATE, DeviceCommand.LED_ON);
} else if (command == OnOffType.OFF) {
device(componentID, ACTION_LED_STATE, LED_OFF);
device(commandTargetType, componentID, null, DeviceCommand.ACTION_LED_STATE, DeviceCommand.LED_OFF);
}
} else {
logger.warn("Invalid command {} received for channel {} device {}", command, channelUID,
@@ -307,13 +301,15 @@ public abstract class BaseKeypadHandler extends LutronHandler {
// For buttons, handle OnOffType commands
if (isButton(componentID)) {
if (command instanceof OnOffType) {
// Annotate commands with LEAP button number for LEAP bridge
Integer leapComponent = (this.leapButtonMap == null) ? null : leapButtonMap.get(componentID);
if (command == OnOffType.ON) {
device(componentID, ACTION_PRESS);
device(commandTargetType, componentID, leapComponent, DeviceCommand.ACTION_PRESS, null);
if (autoRelease) {
device(componentID, ACTION_RELEASE);
device(commandTargetType, componentID, leapComponent, DeviceCommand.ACTION_RELEASE, null);
}
} else if (command == OnOffType.OFF) {
device(componentID, ACTION_RELEASE);
device(commandTargetType, componentID, leapComponent, DeviceCommand.ACTION_RELEASE, null);
}
} else {
logger.warn("Invalid command type {} received for channel {} device {}", command, channelUID,
@@ -342,7 +338,7 @@ public abstract class BaseKeypadHandler extends LutronHandler {
// if this channel is for an LED, query the Lutron controller for the current state
if (isLed(id)) {
queryDevice(id, ACTION_LED_STATE);
queryDevice(commandTargetType, id, DeviceCommand.ACTION_LED_STATE);
}
// Button and CCI state can't be queried, only monitored for updates.
// Init button state to OFF on channel init.
@@ -368,16 +364,16 @@ public abstract class BaseKeypadHandler extends LutronHandler {
ChannelUID channelUID = channelFromComponent(component);
if (channelUID != null) {
if (ACTION_LED_STATE.toString().equals(parameters[1]) && parameters.length >= 3) {
if (DeviceCommand.ACTION_LED_STATE.toString().equals(parameters[1]) && parameters.length >= 3) {
if (getThing().getStatus() == ThingStatus.UNKNOWN) {
updateStatus(ThingStatus.ONLINE); // set thing status online if this is an initial response
}
if (LED_ON.toString().equals(parameters[2])) {
if (DeviceCommand.LED_ON.toString().equals(parameters[2])) {
updateState(channelUID, OnOffType.ON);
} else if (LED_OFF.toString().equals(parameters[2])) {
} else if (DeviceCommand.LED_OFF.toString().equals(parameters[2])) {
updateState(channelUID, OnOffType.OFF);
}
} else if (ACTION_PRESS.toString().equals(parameters[1])) {
} else if (DeviceCommand.ACTION_PRESS.toString().equals(parameters[1])) {
if (isButton(component)) {
updateState(channelUID, OnOffType.ON);
if (autoRelease) {
@@ -386,13 +382,13 @@ public abstract class BaseKeypadHandler extends LutronHandler {
} else { // component is CCI
updateState(channelUID, OpenClosedType.CLOSED);
}
} else if (ACTION_RELEASE.toString().equals(parameters[1])) {
} else if (DeviceCommand.ACTION_RELEASE.toString().equals(parameters[1])) {
if (isButton(component)) {
updateState(channelUID, OnOffType.OFF);
} else { // component is CCI
updateState(channelUID, OpenClosedType.OPEN);
}
} else if (ACTION_HOLD.toString().equals(parameters[1])) {
} else if (DeviceCommand.ACTION_HOLD.toString().equals(parameters[1])) {
updateState(channelUID, OnOffType.OFF); // Signal a release if we receive a hold code as we will not
// get a subsequent release.
}

View File

@@ -17,7 +17,9 @@ import static org.openhab.binding.lutron.internal.LutronBindingConstants.*;
import java.math.BigDecimal;
import org.openhab.binding.lutron.internal.config.BlindConfig;
import org.openhab.binding.lutron.internal.protocol.LutronCommandType;
import org.openhab.binding.lutron.internal.protocol.OutputCommand;
import org.openhab.binding.lutron.internal.protocol.lip.LutronCommandType;
import org.openhab.binding.lutron.internal.protocol.lip.TargetType;
import org.openhab.core.library.types.PercentType;
import org.openhab.core.library.types.StopMoveType;
import org.openhab.core.library.types.UpDownType;
@@ -37,16 +39,6 @@ import org.slf4j.LoggerFactory;
* @author Bob Adair - Initial contribution based on Alan Tong's DimmerHandler
*/
public class BlindHandler extends LutronHandler {
private static final Integer ACTION_LIFTLEVEL = 1;
private static final Integer ACTION_TILTLEVEL = 9;
private static final Integer ACTION_LIFTTILTLEVEL = 10;
private static final Integer ACTION_STARTRAISINGTILT = 11;
private static final Integer ACTION_STARTLOWERINGTILT = 12;
private static final Integer ACTION_STOPTILT = 13;
private static final Integer ACTION_STARTRAISINGLIFT = 14;
private static final Integer ACTION_STARTLOWERINGLIFT = 15;
private static final Integer ACTION_STOPLIFT = 16;
private static final Integer ACTION_POSITION_UPDATE = 32; // undocumented in integration protocol guide
private static final Integer PARAMETER_POSITION_UPDATE = 2; // undocumented in integration protocol guide
private int tiltMax = 100; // max 50 for horizontal sheer, 100 for venetian
@@ -96,8 +88,9 @@ public class BlindHandler extends LutronHandler {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "No bridge configured");
} else if (bridge.getStatus() == ThingStatus.ONLINE) {
updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.NONE, "Awaiting initial response");
queryOutput(ACTION_LIFTLEVEL); // handleUpdate() will set thing status to online when response arrives
queryOutput(ACTION_TILTLEVEL);
queryOutput(TargetType.BLIND, OutputCommand.ACTION_LIFTLEVEL);
// handleUpdate() will set thing status to online when response arrives
queryOutput(TargetType.BLIND, OutputCommand.ACTION_TILTLEVEL);
} else {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
}
@@ -107,9 +100,9 @@ public class BlindHandler extends LutronHandler {
public void channelLinked(ChannelUID channelUID) {
// Refresh state when new item is linked.
if (channelUID.getId().equals(CHANNEL_BLINDLIFTLEVEL)) {
queryOutput(ACTION_LIFTLEVEL);
queryOutput(TargetType.BLIND, OutputCommand.ACTION_LIFTLEVEL);
} else if (channelUID.getId().equals(CHANNEL_BLINDTILTLEVEL)) {
queryOutput(ACTION_TILTLEVEL);
queryOutput(TargetType.BLIND, OutputCommand.ACTION_TILTLEVEL);
}
}
@@ -125,30 +118,30 @@ public class BlindHandler extends LutronHandler {
private void handleLiftCommand(Command command) {
if (command instanceof PercentType) {
int level = ((PercentType) command).intValue();
output(ACTION_LIFTLEVEL, level, 0);
output(TargetType.BLIND, OutputCommand.ACTION_LIFTLEVEL, level, null, null);
} else if (command.equals(UpDownType.UP)) {
output(ACTION_STARTRAISINGLIFT);
output(TargetType.BLIND, OutputCommand.ACTION_STARTRAISINGLIFT, null, null, null);
} else if (command.equals(UpDownType.DOWN)) {
output(ACTION_STARTLOWERINGLIFT);
output(TargetType.BLIND, OutputCommand.ACTION_STARTLOWERINGLIFT, null, null, null);
} else if (command.equals(StopMoveType.STOP)) {
output(ACTION_STOPLIFT);
output(TargetType.BLIND, OutputCommand.ACTION_STOPLIFT, null, null, null);
} else if (command instanceof RefreshType) {
queryOutput(ACTION_LIFTLEVEL);
queryOutput(TargetType.BLIND, OutputCommand.ACTION_LIFTLEVEL);
}
}
private void handleTiltCommand(Command command) {
if (command instanceof PercentType) {
int level = ((PercentType) command).intValue();
output(ACTION_TILTLEVEL, Math.min(level, tiltMax), 0);
output(TargetType.BLIND, OutputCommand.ACTION_TILTLEVEL, Math.min(level, tiltMax), null, null);
} else if (command.equals(UpDownType.UP)) {
output(ACTION_STARTRAISINGTILT);
output(TargetType.BLIND, OutputCommand.ACTION_STARTRAISINGTILT, null, null, null);
} else if (command.equals(UpDownType.DOWN)) {
output(ACTION_STARTLOWERINGTILT);
output(TargetType.BLIND, OutputCommand.ACTION_STARTLOWERINGTILT, null, null, null);
} else if (command.equals(StopMoveType.STOP)) {
output(ACTION_STOPTILT);
output(TargetType.BLIND, OutputCommand.ACTION_STOPTILT, null, null, null);
} else if (command instanceof RefreshType) {
queryOutput(ACTION_TILTLEVEL);
queryOutput(TargetType.BLIND, OutputCommand.ACTION_TILTLEVEL);
}
}
@@ -159,21 +152,21 @@ public class BlindHandler extends LutronHandler {
updateStatus(ThingStatus.ONLINE);
}
if (ACTION_LIFTLEVEL.toString().equals(parameters[0])) {
if (OutputCommand.ACTION_LIFTLEVEL.toString().equals(parameters[0])) {
BigDecimal liftLevel = new BigDecimal(parameters[1]);
logger.trace("Blind {} received lift level: {}", getIntegrationId(), liftLevel);
updateState(CHANNEL_BLINDLIFTLEVEL, new PercentType(liftLevel));
} else if (ACTION_TILTLEVEL.toString().equals(parameters[0])) {
} else if (OutputCommand.ACTION_TILTLEVEL.toString().equals(parameters[0])) {
BigDecimal tiltLevel = new BigDecimal(parameters[1]);
logger.trace("Blind {} received tilt level: {}", getIntegrationId(), tiltLevel);
updateState(CHANNEL_BLINDTILTLEVEL, new PercentType(tiltLevel));
} else if (ACTION_LIFTTILTLEVEL.toString().equals(parameters[0]) && parameters.length > 2) {
} else if (OutputCommand.ACTION_LIFTTILTLEVEL.toString().equals(parameters[0]) && parameters.length > 2) {
BigDecimal liftLevel = new BigDecimal(parameters[1]);
BigDecimal tiltLevel = new BigDecimal(parameters[2]);
logger.trace("Blind {} received lift/tilt level: {} {}", getIntegrationId(), liftLevel, tiltLevel);
updateState(CHANNEL_BLINDLIFTLEVEL, new PercentType(liftLevel));
updateState(CHANNEL_BLINDTILTLEVEL, new PercentType(tiltLevel));
} else if (ACTION_POSITION_UPDATE.toString().equals(parameters[0])
} else if (OutputCommand.ACTION_POSITION_UPDATE.toString().equals(parameters[0])
&& PARAMETER_POSITION_UPDATE.toString().equals(parameters[1]) && parameters.length >= 3) {
BigDecimal level = new BigDecimal(parameters[2]);
logger.trace("Blind {} received lift level position update: {}", getIntegrationId(), level);

View File

@@ -15,12 +15,13 @@ package org.openhab.binding.lutron.internal.handler;
import static org.openhab.binding.lutron.internal.LutronBindingConstants.*;
import java.math.BigDecimal;
import java.util.Locale;
import org.apache.commons.lang.StringUtils;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.lutron.internal.protocol.LutronCommandType;
import org.openhab.binding.lutron.internal.protocol.OutputCommand;
import org.openhab.binding.lutron.internal.protocol.lip.LutronCommandType;
import org.openhab.binding.lutron.internal.protocol.lip.TargetType;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.ChannelUID;
@@ -45,9 +46,6 @@ import org.slf4j.LoggerFactory;
*/
@NonNullByDefault
public class CcoHandler extends LutronHandler {
private static final Integer ACTION_PULSE = 6;
private static final Integer ACTION_STATE = 1;
private final Logger logger = LoggerFactory.getLogger(CcoHandler.class);
private int integrationId;
@@ -123,7 +121,8 @@ public class CcoHandler extends LutronHandler {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "No bridge configured");
} else if (bridge.getStatus() == ThingStatus.ONLINE) {
updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.NONE, "Awaiting initial response");
queryOutput(ACTION_STATE); // handleUpdate() will set thing status to online when response arrives
queryOutput(TargetType.CCO, OutputCommand.ACTION_STATE);
// handleUpdate() will set thing status to online when response arrives
} else {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
}
@@ -139,7 +138,7 @@ public class CcoHandler extends LutronHandler {
updateState(channelUID, OnOffType.OFF);
} else if (outputType == CcoOutputType.MAINTAINED) {
// Query the device state and let the service routine update the channel state
queryOutput(ACTION_STATE);
queryOutput(TargetType.CCO, OutputCommand.ACTION_STATE);
} else {
logger.warn("invalid output type defined for CCO {}", integrationId);
}
@@ -153,22 +152,22 @@ public class CcoHandler extends LutronHandler {
if (channelUID.getId().equals(CHANNEL_SWITCH)) {
if (command instanceof OnOffType && command == OnOffType.ON) {
if (outputType == CcoOutputType.PULSED) {
output(ACTION_PULSE, String.format(Locale.ROOT, "%.2f", defaultPulse));
output(TargetType.CCO, OutputCommand.ACTION_PULSE, Double.valueOf(defaultPulse), null, null);
updateState(channelUID, OnOffType.OFF);
} else {
output(ACTION_STATE, 100);
output(TargetType.CCO, OutputCommand.ACTION_STATE, 100, null, null);
}
}
else if (command instanceof OnOffType && command == OnOffType.OFF) {
if (outputType == CcoOutputType.MAINTAINED) {
output(ACTION_STATE, 0);
output(TargetType.CCO, OutputCommand.ACTION_STATE, 0, null, null);
}
}
else if (command instanceof RefreshType) {
if (outputType == CcoOutputType.MAINTAINED) {
queryOutput(ACTION_STATE);
queryOutput(TargetType.CCO, OutputCommand.ACTION_STATE);
} else {
updateState(CHANNEL_SWITCH, OnOffType.OFF);
}
@@ -186,7 +185,7 @@ public class CcoHandler extends LutronHandler {
if (outputType == CcoOutputType.MAINTAINED) {
if (type == LutronCommandType.OUTPUT && parameters.length > 1
&& ACTION_STATE.toString().equals(parameters[0])) {
&& OutputCommand.ACTION_STATE.toString().equals(parameters[0])) {
if (getThing().getStatus() == ThingStatus.UNKNOWN) {
updateStatus(ThingStatus.ONLINE);
}

View File

@@ -21,8 +21,10 @@ import java.util.concurrent.atomic.AtomicReference;
import org.openhab.binding.lutron.internal.action.DimmerActions;
import org.openhab.binding.lutron.internal.config.DimmerConfig;
import org.openhab.binding.lutron.internal.protocol.LutronCommandType;
import org.openhab.binding.lutron.internal.protocol.LutronDuration;
import org.openhab.binding.lutron.internal.protocol.OutputCommand;
import org.openhab.binding.lutron.internal.protocol.lip.LutronCommandType;
import org.openhab.binding.lutron.internal.protocol.lip.TargetType;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.PercentType;
import org.openhab.core.thing.Bridge;
@@ -42,8 +44,6 @@ import org.slf4j.LoggerFactory;
* @author Bob Adair - Added initDeviceState method, and onLevel and onToLast parameters
*/
public class DimmerHandler extends LutronHandler {
private static final Integer ACTION_ZONELEVEL = 1;
private final Logger logger = LoggerFactory.getLogger(DimmerHandler.class);
private DimmerConfig config;
private LutronDuration fadeInTime;
@@ -90,7 +90,8 @@ public class DimmerHandler extends LutronHandler {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "No bridge configured");
} else if (bridge.getStatus() == ThingStatus.ONLINE) {
updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.NONE, "Awaiting initial response");
queryOutput(ACTION_ZONELEVEL); // handleUpdate() will set thing status to online when response arrives
queryOutput(TargetType.DIMMER, OutputCommand.ACTION_ZONELEVEL);
// handleUpdate() will set thing status to online when response arrives
lastLightLevel.set(config.onLevel);
} else {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
@@ -101,7 +102,7 @@ public class DimmerHandler extends LutronHandler {
public void channelLinked(ChannelUID channelUID) {
if (channelUID.getId().equals(CHANNEL_LIGHTLEVEL)) {
// Refresh state when new item is linked.
queryOutput(ACTION_ZONELEVEL);
queryOutput(TargetType.DIMMER, OutputCommand.ACTION_ZONELEVEL);
}
}
@@ -110,28 +111,28 @@ public class DimmerHandler extends LutronHandler {
if (channelUID.getId().equals(CHANNEL_LIGHTLEVEL)) {
if (command instanceof Number) {
int level = ((Number) command).intValue();
output(ACTION_ZONELEVEL, level, 0.25);
output(TargetType.DIMMER, OutputCommand.ACTION_ZONELEVEL, level, new LutronDuration("0.25"), null);
} else if (command.equals(OnOffType.ON)) {
if (config.onToLast) {
output(ACTION_ZONELEVEL, lastLightLevel.get(), fadeInTime);
output(TargetType.DIMMER, OutputCommand.ACTION_ZONELEVEL, lastLightLevel.get(), fadeInTime, null);
} else {
output(ACTION_ZONELEVEL, config.onLevel, fadeInTime);
output(TargetType.DIMMER, OutputCommand.ACTION_ZONELEVEL, config.onLevel, fadeInTime, null);
}
} else if (command.equals(OnOffType.OFF)) {
output(ACTION_ZONELEVEL, 0, fadeOutTime);
output(TargetType.DIMMER, OutputCommand.ACTION_ZONELEVEL, 0, fadeOutTime, null);
}
}
}
public void setLightLevel(BigDecimal level, LutronDuration fade, LutronDuration delay) {
int intLevel = level.intValue();
output(ACTION_ZONELEVEL, intLevel, fade, delay);
output(TargetType.DIMMER, OutputCommand.ACTION_ZONELEVEL, intLevel, fade, delay);
}
@Override
public void handleUpdate(LutronCommandType type, String... parameters) {
if (type == LutronCommandType.OUTPUT && parameters.length > 1
&& ACTION_ZONELEVEL.toString().equals(parameters[0])) {
&& OutputCommand.ACTION_ZONELEVEL.toString().equals(parameters[0])) {
BigDecimal level = new BigDecimal(parameters[1]);
if (getThing().getStatus() == ThingStatus.UNKNOWN) {
updateStatus(ThingStatus.ONLINE);

View File

@@ -0,0 +1,137 @@
/**
* 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.lutron.internal.handler;
import static org.openhab.binding.lutron.internal.LutronBindingConstants.*;
import java.math.BigDecimal;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.lutron.internal.config.FanConfig;
import org.openhab.binding.lutron.internal.protocol.FanSpeedType;
import org.openhab.binding.lutron.internal.protocol.OutputCommand;
import org.openhab.binding.lutron.internal.protocol.lip.LutronCommandType;
import org.openhab.binding.lutron.internal.protocol.lip.LutronOperation;
import org.openhab.binding.lutron.internal.protocol.lip.TargetType;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.PercentType;
import org.openhab.core.library.types.StringType;
import org.openhab.core.thing.Bridge;
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.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Handler responsible for communicating with ceiling fan speed controllers.
*
* @author Bob Adair - Initial contribution
*/
@NonNullByDefault
public class FanHandler extends LutronHandler {
private final Logger logger = LoggerFactory.getLogger(FanHandler.class);
private FanConfig config = new FanConfig();
public FanHandler(Thing thing) {
super(thing);
}
@Override
public int getIntegrationId() {
if (config.integrationId <= 0) {
throw new IllegalStateException("handler not initialized");
} else {
return config.integrationId;
}
}
@Override
public void initialize() {
config = getConfigAs(FanConfig.class);
if (config.integrationId <= 0) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "No integrationId configured");
return;
}
logger.debug("Initializing Fan handler for integration ID {}", getIntegrationId());
initDeviceState();
}
@Override
protected void initDeviceState() {
logger.debug("Initializing device state for Fan {}", getIntegrationId());
Bridge bridge = getBridge();
if (bridge == null) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "No bridge configured");
} else if (bridge.getStatus() == ThingStatus.ONLINE) {
updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.NONE, "Awaiting initial response");
queryOutput(TargetType.FAN, OutputCommand.ACTION_ZONELEVEL);
// handleUpdate() will set thing status to online when response arrives
} else {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
}
}
@Override
public void channelLinked(ChannelUID channelUID) {
if (channelUID.getId().equals(CHANNEL_FANSPEED) || channelUID.getId().equals(CHANNEL_FANLEVEL)) {
// Refresh state when new item is linked.
queryOutput(TargetType.FAN, OutputCommand.ACTION_ZONELEVEL);
}
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
if (channelUID.getId().equals(CHANNEL_FANLEVEL)) {
if (command instanceof Number) {
int level = ((Number) command).intValue();
FanSpeedType speed = FanSpeedType.toFanSpeedType(level);
// output(TargetType.FAN, LutronCommand.ACTION_ZONELEVEL, level, null, null);
sendCommand(new OutputCommand(TargetType.FAN, LutronOperation.EXECUTE, getIntegrationId(),
OutputCommand.ACTION_ZONELEVEL, speed, null, null));
} else if (command.equals(OnOffType.ON)) {
// output(TargetType.FAN, LutronCommand.ACTION_ZONELEVEL, 100, null, null);
sendCommand(new OutputCommand(TargetType.FAN, LutronOperation.EXECUTE, getIntegrationId(),
OutputCommand.ACTION_ZONELEVEL, FanSpeedType.HIGH, null, null));
} else if (command.equals(OnOffType.OFF)) {
// output(TargetType.FAN, LutronCommand.ACTION_ZONELEVEL, 0, null, null);
sendCommand(new OutputCommand(TargetType.FAN, LutronOperation.EXECUTE, getIntegrationId(),
OutputCommand.ACTION_ZONELEVEL, FanSpeedType.OFF, null, null));
}
} else if (channelUID.getId().equals(CHANNEL_FANSPEED)) {
if (command instanceof StringType) {
FanSpeedType speed = FanSpeedType.toFanSpeedType(command.toString());
sendCommand(new OutputCommand(TargetType.FAN, LutronOperation.EXECUTE, getIntegrationId(),
OutputCommand.ACTION_ZONELEVEL, speed, null, null));
}
}
}
@Override
public void handleUpdate(LutronCommandType type, String... parameters) {
if (type == LutronCommandType.OUTPUT && parameters.length > 1
&& OutputCommand.ACTION_ZONELEVEL.toString().equals(parameters[0])) {
BigDecimal level = new BigDecimal(parameters[1]);
if (getThing().getStatus() == ThingStatus.UNKNOWN) {
updateStatus(ThingStatus.ONLINE);
}
updateState(CHANNEL_FANLEVEL, new PercentType(level));
FanSpeedType fanSpeed = FanSpeedType.toFanSpeedType(level.intValue());
updateState(CHANNEL_FANSPEED, new StringType(fanSpeed.toString()));
}
}
}

View File

@@ -19,7 +19,8 @@ import java.util.concurrent.TimeUnit;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.lutron.internal.protocol.LutronCommandType;
import org.openhab.binding.lutron.internal.protocol.ModeCommand;
import org.openhab.binding.lutron.internal.protocol.lip.LutronCommandType;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.thing.Bridge;
@@ -39,8 +40,7 @@ import org.slf4j.LoggerFactory;
*/
@NonNullByDefault
public class GreenModeHandler extends LutronHandler {
private static final Integer ACTION_STEP = 1;
public static final int GREENSTEP_MIN = 1;
private static final int GREENSTEP_MIN = 1;
// poll interval parameters are in minutes
private static final int POLL_INTERVAL_DEFAULT = 15;
@@ -94,7 +94,8 @@ public class GreenModeHandler extends LutronHandler {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "No bridge configured");
} else if (bridge.getStatus() == ThingStatus.ONLINE) {
updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.NONE, "Awaiting initial response");
queryGreenMode(ACTION_STEP); // handleUpdate() will set thing status to online when response arrives
queryGreenMode(ModeCommand.ACTION_STEP);
// handleUpdate() will set thing status to online when response arrives
} else {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
}
@@ -123,13 +124,13 @@ public class GreenModeHandler extends LutronHandler {
private synchronized void pollState() {
logger.trace("Executing green mode polling job for integration ID {}", integrationId);
queryGreenMode(ACTION_STEP);
queryGreenMode(ModeCommand.ACTION_STEP);
}
@Override
public void channelLinked(ChannelUID channelUID) {
if (channelUID.getId().equals(CHANNEL_STEP)) {
queryGreenMode(ACTION_STEP);
queryGreenMode(ModeCommand.ACTION_STEP);
}
}
@@ -137,16 +138,16 @@ public class GreenModeHandler extends LutronHandler {
public void handleCommand(ChannelUID channelUID, Command command) {
if (channelUID.getId().equals(CHANNEL_STEP)) {
if (command == OnOffType.ON) {
greenMode(ACTION_STEP, 2);
greenMode(ModeCommand.ACTION_STEP, 2);
} else if (command == OnOffType.OFF) {
greenMode(ACTION_STEP, 1);
greenMode(ModeCommand.ACTION_STEP, 1);
} else if (command instanceof Number) {
Integer step = ((Number) command).intValue();
if (step.intValue() >= GREENSTEP_MIN) {
greenMode(ACTION_STEP, step);
greenMode(ModeCommand.ACTION_STEP, step);
}
} else if (command instanceof RefreshType) {
queryGreenMode(ACTION_STEP);
queryGreenMode(ModeCommand.ACTION_STEP);
} else {
logger.debug("Ignoring invalid command {} for id {}", command, integrationId);
}
@@ -159,7 +160,7 @@ public class GreenModeHandler extends LutronHandler {
public void handleUpdate(LutronCommandType type, String... parameters) {
try {
if (type == LutronCommandType.MODE && parameters.length > 1
&& ACTION_STEP.toString().equals(parameters[0])) {
&& ModeCommand.ACTION_STEP.toString().equals(parameters[0])) {
Long step = Long.valueOf(parameters[1]);
if (getThing().getStatus() == ThingStatus.UNKNOWN) {
updateStatus(ThingStatus.ONLINE);

View File

@@ -30,15 +30,16 @@ import org.openhab.binding.lutron.internal.config.IPBridgeConfig;
import org.openhab.binding.lutron.internal.discovery.LutronDeviceDiscoveryService;
import org.openhab.binding.lutron.internal.net.TelnetSession;
import org.openhab.binding.lutron.internal.net.TelnetSessionListener;
import org.openhab.binding.lutron.internal.protocol.LutronCommand;
import org.openhab.binding.lutron.internal.protocol.LutronCommandType;
import org.openhab.binding.lutron.internal.protocol.LutronOperation;
import org.openhab.binding.lutron.internal.protocol.LIPCommand;
import org.openhab.binding.lutron.internal.protocol.LutronCommandNew;
import org.openhab.binding.lutron.internal.protocol.lip.LutronCommandType;
import org.openhab.binding.lutron.internal.protocol.lip.LutronOperation;
import org.openhab.binding.lutron.internal.protocol.lip.TargetType;
import org.openhab.core.thing.Bridge;
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.types.Command;
import org.slf4j.Logger;
@@ -51,9 +52,9 @@ import org.slf4j.LoggerFactory;
* @author Bob Adair - Added reconnect and heartbeat config parameters, moved discovery service registration to
* LutronHandlerFactory
*/
public class IPBridgeHandler extends BaseBridgeHandler {
public class IPBridgeHandler extends LutronBridgeHandler {
private static final Pattern RESPONSE_REGEX = Pattern
.compile("~(OUTPUT|DEVICE|SYSTEM|TIMECLOCK|MODE|SYSVAR),([0-9\\.:/]+),([0-9,\\.:/]*)\\Z");
.compile("~(OUTPUT|DEVICE|SYSTEM|TIMECLOCK|MODE|SYSVAR|GROUP),([0-9\\.:/]+),([0-9,\\.:/]*)\\Z");
private static final String DB_UPDATE_DATE_FORMAT = "MM/dd/yyyy HH:mm:ss";
@@ -85,7 +86,7 @@ public class IPBridgeHandler extends BaseBridgeHandler {
private int sendDelay;
private TelnetSession session;
private BlockingQueue<LutronCommand> sendQueue = new LinkedBlockingQueue<>();
private BlockingQueue<LutronCommandNew> sendQueue = new LinkedBlockingQueue<>();
private Thread messageSender;
private ScheduledFuture<?> keepAlive;
@@ -206,8 +207,8 @@ public class IPBridgeHandler extends BaseBridgeHandler {
updateStatus(ThingStatus.ONLINE);
// Disable prompts
sendCommand(new LutronCommand(LutronOperation.EXECUTE, LutronCommandType.MONITORING, -1, MONITOR_PROMPT,
MONITOR_DISABLE));
sendCommand(new LIPCommand(TargetType.BRIDGE, LutronOperation.EXECUTE, LutronCommandType.MONITORING, null,
MONITOR_PROMPT, MONITOR_DISABLE));
if (requireSysvarMonitoring.get()) {
setSysvarMonitoring(true);
@@ -215,7 +216,8 @@ public class IPBridgeHandler extends BaseBridgeHandler {
// Check the time device database was last updated. On the initial connect, this will trigger
// a scan for paired devices.
sendCommand(new LutronCommand(LutronOperation.QUERY, LutronCommandType.SYSTEM, -1, SYSTEM_DBEXPORTDATETIME));
sendCommand(new LIPCommand(TargetType.BRIDGE, LutronOperation.QUERY, LutronCommandType.SYSTEM, null,
SYSTEM_DBEXPORTDATETIME));
messageSender = new Thread(this::sendCommandsThread, "Lutron sender");
messageSender.start();
@@ -228,7 +230,7 @@ public class IPBridgeHandler extends BaseBridgeHandler {
private void sendCommandsThread() {
try {
while (!Thread.currentThread().isInterrupted()) {
LutronCommand command = sendQueue.take();
LutronCommandNew command = sendQueue.take();
logger.debug("Sending command {}", command);
@@ -317,8 +319,9 @@ public class IPBridgeHandler extends BaseBridgeHandler {
return false;
}
void sendCommand(LutronCommand command) {
this.sendQueue.add(command);
@Override
public void sendCommand(LutronCommandNew command) {
sendQueue.add(command);
}
private LutronHandler findThingHandler(int integrationId) {
@@ -423,7 +426,8 @@ public class IPBridgeHandler extends BaseBridgeHandler {
keepAliveReconnect = scheduler.schedule(this::reconnect, KEEPALIVE_TIMEOUT_SECONDS, TimeUnit.SECONDS);
logger.trace("Sending keepalive query");
sendCommand(new LutronCommand(LutronOperation.QUERY, LutronCommandType.SYSTEM, -1, SYSTEM_DBEXPORTDATETIME));
sendCommand(new LIPCommand(TargetType.BRIDGE, LutronOperation.QUERY, LutronCommandType.SYSTEM, null,
SYSTEM_DBEXPORTDATETIME));
}
private void setDbUpdateDate(String dateString, String timeString) {
@@ -455,8 +459,8 @@ public class IPBridgeHandler extends BaseBridgeHandler {
private void setSysvarMonitoring(boolean enable) {
Integer setting = (enable) ? MONITOR_ENABLE : MONITOR_DISABLE;
sendCommand(
new LutronCommand(LutronOperation.EXECUTE, LutronCommandType.MONITORING, -1, MONITOR_SYSVAR, setting));
sendCommand(new LIPCommand(TargetType.BRIDGE, LutronOperation.EXECUTE, LutronCommandType.MONITORING, null,
MONITOR_SYSVAR, setting));
}
@Override

View File

@@ -0,0 +1,802 @@
/**
* 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.lutron.internal.handler;
import static org.openhab.binding.lutron.internal.LutronBindingConstants.*;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.InterruptedIOException;
import java.io.OutputStreamWriter;
import java.net.UnknownHostException;
import java.security.KeyManagementException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.UnrecoverableKeyException;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Future;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocket;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactory;
import javax.net.ssl.X509TrustManager;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.lutron.internal.config.LeapBridgeConfig;
import org.openhab.binding.lutron.internal.discovery.LeapDeviceDiscoveryService;
import org.openhab.binding.lutron.internal.protocol.FanSpeedType;
import org.openhab.binding.lutron.internal.protocol.GroupCommand;
import org.openhab.binding.lutron.internal.protocol.LutronCommandNew;
import org.openhab.binding.lutron.internal.protocol.OutputCommand;
import org.openhab.binding.lutron.internal.protocol.leap.LeapCommand;
import org.openhab.binding.lutron.internal.protocol.leap.LeapMessageParser;
import org.openhab.binding.lutron.internal.protocol.leap.LeapMessageParserCallbacks;
import org.openhab.binding.lutron.internal.protocol.leap.Request;
import org.openhab.binding.lutron.internal.protocol.leap.dto.Area;
import org.openhab.binding.lutron.internal.protocol.leap.dto.ButtonGroup;
import org.openhab.binding.lutron.internal.protocol.leap.dto.Device;
import org.openhab.binding.lutron.internal.protocol.leap.dto.OccupancyGroup;
import org.openhab.binding.lutron.internal.protocol.leap.dto.ZoneStatus;
import org.openhab.binding.lutron.internal.protocol.lip.LutronCommandType;
import org.openhab.core.library.types.StringType;
import org.openhab.core.thing.Bridge;
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.ThingStatusInfo;
import org.openhab.core.thing.binding.ThingHandler;
import org.openhab.core.thing.binding.ThingHandlerService;
import org.openhab.core.types.Command;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Bridge handler responsible for communicating with Lutron hubs that support the LEAP protocol, such as Caseta and
* RA2 Select.
*
* @author Bob Adair - Initial contribution
*/
@NonNullByDefault
public class LeapBridgeHandler extends LutronBridgeHandler implements LeapMessageParserCallbacks {
private static final int DEFAULT_RECONNECT_MINUTES = 5;
private static final int DEFAULT_HEARTBEAT_MINUTES = 5;
private static final long KEEPALIVE_TIMEOUT_SECONDS = 30;
private static final String STATUS_INITIALIZING = "Initializing";
private final Logger logger = LoggerFactory.getLogger(LeapBridgeHandler.class);
private @NonNullByDefault({}) LeapBridgeConfig config;
private int reconnectInterval;
private int heartbeatInterval;
private int sendDelay;
private @NonNullByDefault({}) SSLSocketFactory sslsocketfactory;
private @Nullable SSLSocket sslsocket;
private @Nullable BufferedWriter writer;
private @Nullable BufferedReader reader;
private @NonNullByDefault({}) LeapMessageParser leapMessageParser;
private final BlockingQueue<LeapCommand> sendQueue = new LinkedBlockingQueue<>();
private @Nullable Future<?> asyncInitializeTask;
private @Nullable Thread senderThread;
private @Nullable Thread readerThread;
private @Nullable ScheduledFuture<?> keepAliveJob;
private @Nullable ScheduledFuture<?> keepAliveReconnectJob;
private @Nullable ScheduledFuture<?> connectRetryJob;
private final Object keepAliveReconnectLock = new Object();
private final Map<Integer, Integer> zoneToDevice = new HashMap<>();
private final Map<Integer, Integer> deviceToZone = new HashMap<>();
private final Object zoneMapsLock = new Object();
private @Nullable Map<Integer, List<Integer>> deviceButtonMap;
private final Object deviceButtonMapLock = new Object();
private volatile boolean deviceDataLoaded = false;
private volatile boolean buttonDataLoaded = false;
private final Map<Integer, LutronHandler> childHandlerMap = new ConcurrentHashMap<>();
private final Map<Integer, OGroupHandler> groupHandlerMap = new ConcurrentHashMap<>();
private @Nullable LeapDeviceDiscoveryService discoveryService;
public void setDiscoveryService(LeapDeviceDiscoveryService discoveryService) {
this.discoveryService = discoveryService;
}
public LeapBridgeHandler(Bridge bridge) {
super(bridge);
leapMessageParser = new LeapMessageParser(this);
}
@Override
public Collection<Class<? extends ThingHandlerService>> getServices() {
return Collections.singleton(LeapDeviceDiscoveryService.class);
}
@Override
public void initialize() {
SSLContext sslContext;
childHandlerMap.clear();
groupHandlerMap.clear();
config = getConfigAs(LeapBridgeConfig.class);
String keystorePassword = (config.keystorePassword == null) ? "" : config.keystorePassword;
String ipAddress = config.ipAddress;
if (ipAddress == null || ipAddress.isEmpty()) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "bridge address not specified");
return;
}
reconnectInterval = (config.reconnect > 0) ? config.reconnect : DEFAULT_RECONNECT_MINUTES;
heartbeatInterval = (config.heartbeat > 0) ? config.heartbeat : DEFAULT_HEARTBEAT_MINUTES;
sendDelay = (config.delay < 0) ? 0 : config.delay;
if (config.keystore == null || keystorePassword == null) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
"Keystore/keystore password not configured");
return;
} else {
try (FileInputStream keystoreInputStream = new FileInputStream(config.keystore)) {
logger.trace("Initializing keystore");
KeyStore keystore = KeyStore.getInstance(KeyStore.getDefaultType());
keystore.load(keystoreInputStream, keystorePassword.toCharArray());
logger.trace("Initializing SSL Context");
KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
kmf.init(keystore, keystorePassword.toCharArray());
TrustManager[] trustManagers;
if (config.certValidate) {
// Use default trust manager which will attempt to validate server certificate from hub
TrustManagerFactory tmf = TrustManagerFactory
.getInstance(TrustManagerFactory.getDefaultAlgorithm());
tmf.init(keystore);
trustManagers = tmf.getTrustManagers();
} else {
// Use no-op trust manager which will not verify certificates
trustManagers = defineNoOpTrustManager();
}
sslContext = SSLContext.getInstance("TLS");
sslContext.init(kmf.getKeyManagers(), trustManagers, null);
sslsocketfactory = sslContext.getSocketFactory();
} catch (FileNotFoundException e) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Keystore file not found");
return;
} catch (CertificateException e) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Certificate exception");
return;
} catch (UnrecoverableKeyException e) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
"Key unrecoverable with supplied password");
return;
} catch (KeyManagementException e) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE, "Key management exception");
logger.debug("Key management exception", e);
return;
} catch (KeyStoreException | NoSuchAlgorithmException | IOException e) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE, "Error initializing keystore");
logger.debug("Error initializing keystore", e);
return;
}
}
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE, "Connecting");
asyncInitializeTask = scheduler.submit(this::connect); // start the async connect task
}
/**
* Return a no-op SSL trust manager which will not verify server or client certificates.
*/
private TrustManager[] defineNoOpTrustManager() {
return new TrustManager[] { new X509TrustManager() {
@Override
public void checkClientTrusted(final X509Certificate @Nullable [] chain, final @Nullable String authType) {
logger.debug("Assuming client certificate is valid");
if (chain != null && logger.isTraceEnabled()) {
for (int cert = 0; cert < chain.length; cert++) {
logger.trace("Subject DN: {}", chain[cert].getSubjectDN());
logger.trace("Issuer DN: {}", chain[cert].getIssuerDN());
logger.trace("Serial number {}:", chain[cert].getSerialNumber());
}
}
}
@Override
public void checkServerTrusted(final X509Certificate @Nullable [] chain, final @Nullable String authType) {
logger.debug("Assuming server certificate is valid");
if (chain != null && logger.isTraceEnabled()) {
for (int cert = 0; cert < chain.length; cert++) {
logger.trace("Subject DN: {}", chain[cert].getSubjectDN());
logger.trace("Issuer DN: {}", chain[cert].getIssuerDN());
logger.trace("Serial number: {}", chain[cert].getSerialNumber());
}
}
}
@Override
public X509Certificate @Nullable [] getAcceptedIssuers() {
return null;
}
} };
}
private synchronized void connect() {
deviceDataLoaded = false;
buttonDataLoaded = false;
try {
logger.debug("Opening SSL connection to {}:{}", config.ipAddress, config.port);
SSLSocket sslsocket = (SSLSocket) sslsocketfactory.createSocket(config.ipAddress, config.port);
sslsocket.startHandshake();
writer = new BufferedWriter(new OutputStreamWriter(sslsocket.getOutputStream()));
reader = new BufferedReader(new InputStreamReader(sslsocket.getInputStream()));
this.sslsocket = sslsocket;
} catch (UnknownHostException e) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Unknown host");
return;
} catch (IllegalArgumentException e) {
// port out of valid range
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Invalid port number");
return;
} catch (InterruptedIOException e) {
Thread.currentThread().interrupt();
logger.debug("Interrupted while establishing connection");
return;
} catch (IOException e) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
"Error opening SSL connection. Check log.");
logger.info("Error opening SSL connection: {}", e.getMessage());
disconnect(false);
scheduleConnectRetry(reconnectInterval); // Possibly a temporary problem. Try again later.
return;
}
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE, STATUS_INITIALIZING);
Thread readerThread = new Thread(this::readerThreadJob, "Lutron reader");
readerThread.setDaemon(true);
readerThread.start();
this.readerThread = readerThread;
Thread senderThread = new Thread(this::senderThreadJob, "Lutron sender");
senderThread.setDaemon(true);
senderThread.start();
this.senderThread = senderThread;
sendCommand(new LeapCommand(Request.getButtonGroups()));
queryDiscoveryData();
sendCommand(new LeapCommand(Request.subscribeOccupancyGroupStatus()));
logger.debug("Starting keepalive job with interval {}", heartbeatInterval);
keepAliveJob = scheduler.scheduleWithFixedDelay(this::sendKeepAlive, heartbeatInterval, heartbeatInterval,
TimeUnit.MINUTES);
}
/**
* Called by connect() and discovery service to request fresh discovery data
*/
public void queryDiscoveryData() {
sendCommand(new LeapCommand(Request.getDevices()));
sendCommand(new LeapCommand(Request.getAreas()));
sendCommand(new LeapCommand(Request.getOccupancyGroups()));
}
private void scheduleConnectRetry(long waitMinutes) {
logger.debug("Scheduling connection retry in {} minutes", waitMinutes);
connectRetryJob = scheduler.schedule(this::connect, waitMinutes, TimeUnit.MINUTES);
}
/**
* Disconnect from bridge, cancel retry and keepalive jobs, stop reader and writer threads, and clean up.
*
* @param interruptAll Set if reconnect task should be interrupted if running. Should be false when calling from
* connect or reconnect, and true when calling from dispose.
*/
private synchronized void disconnect(boolean interruptAll) {
logger.debug("Disconnecting");
Thread senderThread = this.senderThread;
Thread readerThread = this.readerThread;
ScheduledFuture<?> connectRetryJob = this.connectRetryJob;
if (connectRetryJob != null) {
connectRetryJob.cancel(true);
}
ScheduledFuture<?> keepAliveJob = this.keepAliveJob;
if (keepAliveJob != null) {
keepAliveJob.cancel(true);
}
reconnectTaskCancel(interruptAll); // May be called from keepAliveReconnectJob thread
if (senderThread != null && senderThread.isAlive()) {
senderThread.interrupt();
}
if (readerThread != null && readerThread.isAlive()) {
readerThread.interrupt();
}
SSLSocket sslsocket = this.sslsocket;
if (sslsocket != null) {
try {
sslsocket.close();
} catch (IOException e) {
logger.debug("Error closing SSL socket: {}", e.getMessage());
}
this.sslsocket = null;
}
BufferedReader reader = this.reader;
if (reader != null) {
try {
reader.close();
} catch (IOException e) {
logger.debug("Error closing reader: {}", e.getMessage());
}
}
BufferedWriter writer = this.writer;
if (writer != null) {
try {
writer.close();
} catch (IOException e) {
logger.debug("Error closing writer: {}", e.getMessage());
}
}
deviceDataLoaded = false;
buttonDataLoaded = false;
}
private synchronized void reconnect() {
logger.debug("Attempting to reconnect to the bridge");
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "reconnecting");
disconnect(false);
connect();
}
/**
* Method executed by the message sender thread (senderThread)
*/
private void senderThreadJob() {
logger.debug("Command sender thread started");
try {
while (!Thread.currentThread().isInterrupted() && writer != null) {
LeapCommand command = sendQueue.take();
logger.trace("Sending command {}", command);
try {
BufferedWriter writer = this.writer;
if (writer != null) {
writer.write(command.toString() + "\n");
writer.flush();
}
} catch (InterruptedIOException e) {
logger.debug("Interrupted while sending");
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Interrupted");
break; // exit loop and terminate thread
} catch (IOException e) {
logger.warn("Communication error, will try to reconnect. Error: {}", e.getMessage());
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
sendQueue.add(command); // Requeue command
reconnect();
break; // reconnect() will start a new thread; terminate this one
}
if (sendDelay > 0) {
Thread.sleep(sendDelay); // introduce delay to throttle send rate
}
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
logger.debug("Command sender thread exiting");
}
}
/**
* Method executed by the message reader thread (readerThread)
*/
private void readerThreadJob() {
logger.debug("Message reader thread started");
String msg = null;
try {
BufferedReader reader = this.reader;
while (!Thread.interrupted() && reader != null && (msg = reader.readLine()) != null) {
leapMessageParser.handleMessage(msg);
}
if (msg == null) {
logger.debug("End of input stream detected");
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Connection lost");
}
} catch (InterruptedIOException e) {
logger.debug("Interrupted while reading");
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Interrupted");
} catch (IOException e) {
logger.debug("I/O error while reading from stream: {}", e.getMessage());
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
} catch (RuntimeException e) {
logger.warn("Runtime exception in reader thread", e);
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
} finally {
logger.debug("Message reader thread exiting");
}
}
/**
* Called if NoContent response received for a buttongroup read request. Creates empty deviceButtonMap.
*/
@Override
public void handleEmptyButtonGroupDefinition() {
logger.debug("No content in button group definition. Creating empty deviceButtonMap.");
Map<Integer, List<Integer>> deviceButtonMap = new HashMap<>();
synchronized (deviceButtonMapLock) {
this.deviceButtonMap = deviceButtonMap;
buttonDataLoaded = true;
}
checkInitialized();
}
/**
* Set state to online if offline/initializing and all required initialization info is loaded.
* Currently this means device (zone) and button group data.
*/
private void checkInitialized() {
ThingStatusInfo statusInfo = getThing().getStatusInfo();
if (statusInfo.getStatus() == ThingStatus.OFFLINE && STATUS_INITIALIZING.equals(statusInfo.getDescription())) {
if (deviceDataLoaded && buttonDataLoaded) {
updateStatus(ThingStatus.ONLINE);
}
}
}
/**
* Notify child thing handler of a zonelevel update from a received zone status message.
*/
@Override
public void handleZoneUpdate(ZoneStatus zoneStatus) {
logger.trace("Zone: {} level: {}", zoneStatus.getZone(), zoneStatus.level);
Integer integrationId = zoneToDevice(zoneStatus.getZone());
if (integrationId == null) {
logger.debug("Unable to map zone {} to device", zoneStatus.getZone());
return;
}
logger.trace("Zone {} mapped to device id {}", zoneStatus.getZone(), integrationId);
// dispatch update to proper thing handler
LutronHandler handler = findThingHandler(integrationId);
if (handler != null) {
if (zoneStatus.fanSpeed != null) {
// handle fan controller
FanSpeedType fanSpeed = zoneStatus.fanSpeed;
try {
handler.handleUpdate(LutronCommandType.OUTPUT, OutputCommand.ACTION_ZONELEVEL.toString(),
Integer.valueOf(fanSpeed.speed()).toString());
} catch (NumberFormatException e) {
logger.warn("Number format exception parsing update");
} catch (RuntimeException e) {
logger.warn("Runtime exception while processing update");
}
} else {
// handle switch/dimmer/shade
try {
handler.handleUpdate(LutronCommandType.OUTPUT, OutputCommand.ACTION_ZONELEVEL.toString(),
Integer.valueOf(zoneStatus.level).toString());
} catch (NumberFormatException e) {
logger.warn("Number format exception parsing update");
} catch (RuntimeException e) {
logger.warn("Runtime exception while processing update");
}
}
} else {
logger.debug("No thing configured for integration ID {}", integrationId);
}
}
/**
* Notify child group handler of a received occupancy group update.
*
* @param occupancyStatus
* @param groupNumber
*/
@Override
public void handleGroupUpdate(int groupNumber, String occupancyStatus) {
logger.trace("Group {} state update: {}", groupNumber, occupancyStatus);
// dispatch update to proper handler
OGroupHandler handler = findGroupHandler(groupNumber);
if (handler != null) {
try {
switch (occupancyStatus) {
case "Occupied":
handler.handleUpdate(LutronCommandType.GROUP, GroupCommand.ACTION_GROUPSTATE.toString(),
GroupCommand.STATE_GRP_OCCUPIED.toString());
break;
case "Unoccupied":
handler.handleUpdate(LutronCommandType.GROUP, GroupCommand.ACTION_GROUPSTATE.toString(),
GroupCommand.STATE_GRP_UNOCCUPIED.toString());
break;
case "Unknown":
handler.handleUpdate(LutronCommandType.GROUP, GroupCommand.ACTION_GROUPSTATE.toString(),
GroupCommand.STATE_GRP_UNKNOWN.toString());
break;
default:
logger.debug("Unexpected occupancy status: {}", occupancyStatus);
return;
}
} catch (NumberFormatException e) {
logger.warn("Number format exception parsing update");
} catch (RuntimeException e) {
logger.warn("Runtime exception while processing update");
}
} else {
logger.debug("No group thing configured for group ID {}", groupNumber);
}
}
@Override
public void handleMultipleButtonGroupDefinition(List<ButtonGroup> buttonGroupList) {
Map<Integer, List<Integer>> deviceButtonMap = new HashMap<>();
for (ButtonGroup buttonGroup : buttonGroupList) {
int parentDevice = buttonGroup.getParentDevice();
logger.trace("Found ButtonGroup: {} parent device: {}", buttonGroup.getButtonGroup(), parentDevice);
List<Integer> buttonList = buttonGroup.getButtonList();
deviceButtonMap.put(parentDevice, buttonList);
}
synchronized (deviceButtonMapLock) {
this.deviceButtonMap = deviceButtonMap;
buttonDataLoaded = true;
}
checkInitialized();
}
@Override
public void handleMultipleDeviceDefintion(List<Device> deviceList) {
synchronized (zoneMapsLock) {
zoneToDevice.clear();
deviceToZone.clear();
for (Device device : deviceList) {
Integer zoneid = device.getZone();
Integer deviceid = device.getDevice();
logger.trace("Found device: {} id: {} zone: {}", device.name, deviceid, zoneid);
if (zoneid > 0 && deviceid > 0) {
zoneToDevice.put(zoneid, deviceid);
deviceToZone.put(deviceid, zoneid);
}
if (deviceid == 1) { // ID 1 is the bridge
setBridgeProperties(device);
}
}
}
deviceDataLoaded = true;
checkInitialized();
LeapDeviceDiscoveryService discoveryService = this.discoveryService;
if (discoveryService != null) {
discoveryService.processDeviceDefinitions(deviceList);
}
}
@Override
public void handleMultipleAreaDefinition(List<Area> areaList) {
LeapDeviceDiscoveryService discoveryService = this.discoveryService;
if (discoveryService != null) {
discoveryService.setAreas(areaList);
}
}
@Override
public void handleMultipleOccupancyGroupDefinition(List<OccupancyGroup> oGroupList) {
LeapDeviceDiscoveryService discoveryService = this.discoveryService;
if (discoveryService != null) {
discoveryService.setOccupancyGroups(oGroupList);
}
}
@Override
public void validMessageReceived(String communiqueType) {
reconnectTaskCancel(true); // Got a good message, so cancel reconnect task.
}
/**
* Set informational bridge properties from the Device entry for the hub/repeater
*/
private void setBridgeProperties(Device device) {
if (device.getDevice() == 1 && device.repeaterProperties != null) {
Map<String, String> properties = editProperties();
if (device.name != null) {
properties.put(PROPERTY_PRODTYP, device.name);
}
if (device.modelNumber != null) {
properties.put(Thing.PROPERTY_MODEL_ID, device.modelNumber);
}
if (device.serialNumber != null) {
properties.put(Thing.PROPERTY_SERIAL_NUMBER, device.serialNumber);
}
if (device.firmwareImage != null && device.firmwareImage.firmware != null
&& device.firmwareImage.firmware.displayName != null) {
properties.put(Thing.PROPERTY_FIRMWARE_VERSION, device.firmwareImage.firmware.displayName);
}
updateProperties(properties);
}
}
/**
* Queue a LeapCommand for transmission by the sender thread.
*/
public void sendCommand(@Nullable LeapCommand command) {
if (command != null) {
sendQueue.add(command);
}
}
/**
* Convert a LutronCommand into a LeapCommand and queue it for transmission by the sender thread.
*/
@Override
public void sendCommand(LutronCommandNew command) {
logger.trace("Received request to send Lutron command: {}", command);
sendCommand(command.leapCommand(this, deviceToZone(command.getIntegrationId())));
}
/**
* Returns LEAP button number for given integrationID and component. Returns 0 if button number cannot be
* determined.
*/
public int getButton(int integrationID, int component) {
synchronized (deviceButtonMapLock) {
if (deviceButtonMap != null) {
List<Integer> buttonList = deviceButtonMap.get(integrationID);
if (buttonList != null && component <= buttonList.size()) {
return buttonList.get(component - 1);
} else {
logger.debug("Could not find button component {} for id {}", component, integrationID);
return 0;
}
} else {
logger.debug("Device to button map not populated");
return 0;
}
}
}
private @Nullable LutronHandler findThingHandler(@Nullable Integer integrationId) {
if (integrationId != null) {
return childHandlerMap.get(integrationId);
} else {
return null;
}
}
private @Nullable OGroupHandler findGroupHandler(int integrationId) {
return groupHandlerMap.get(integrationId);
}
private @Nullable Integer zoneToDevice(int zone) {
synchronized (zoneMapsLock) {
return zoneToDevice.get(zone);
}
}
private @Nullable Integer deviceToZone(@Nullable Integer device) {
if (device == null) {
return null;
}
synchronized (zoneMapsLock) {
return deviceToZone.get(device);
}
}
private void sendKeepAlive() {
logger.trace("Sending keepalive query");
sendCommand(new LeapCommand(Request.ping()));
// Reconnect if no response is received within KEEPALIVE_TIMEOUT_SECONDS.
reconnectTaskSchedule();
}
private void reconnectTaskSchedule() {
synchronized (keepAliveReconnectLock) {
keepAliveReconnectJob = scheduler.schedule(this::reconnect, KEEPALIVE_TIMEOUT_SECONDS, TimeUnit.SECONDS);
}
}
private void reconnectTaskCancel(boolean interrupt) {
synchronized (keepAliveReconnectLock) {
ScheduledFuture<?> keepAliveReconnectJob = this.keepAliveReconnectJob;
if (keepAliveReconnectJob != null) {
logger.trace("Canceling scheduled reconnect job.");
keepAliveReconnectJob.cancel(interrupt);
this.keepAliveReconnectJob = null;
}
}
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
if (channelUID.getId().equals(CHANNEL_COMMAND)) {
if (command instanceof StringType) {
sendCommand(new LeapCommand(command.toString()));
}
}
}
@Override
public void childHandlerInitialized(ThingHandler childHandler, Thing childThing) {
if (childHandler instanceof OGroupHandler) {
// We need a different map for group things because the numbering is separate
OGroupHandler handler = (OGroupHandler) childHandler;
int groupId = handler.getIntegrationId();
groupHandlerMap.put(groupId, handler);
logger.trace("Registered group handler for ID {}", groupId);
} else {
LutronHandler handler = (LutronHandler) childHandler;
int intId = handler.getIntegrationId();
childHandlerMap.put(intId, handler);
logger.trace("Registered child handler for ID {}", intId);
}
}
@Override
public void childHandlerDisposed(ThingHandler childHandler, Thing childThing) {
if (childHandler instanceof OGroupHandler) {
OGroupHandler handler = (OGroupHandler) childHandler;
int groupId = handler.getIntegrationId();
groupHandlerMap.remove(groupId);
logger.trace("Unregistered group handler for ID {}", groupId);
} else {
LutronHandler handler = (LutronHandler) childHandler;
int intId = handler.getIntegrationId();
childHandlerMap.remove(intId);
logger.trace("Unregistered child handler for ID {}", intId);
}
}
@Override
public void dispose() {
Future<?> asyncInitializeTask = this.asyncInitializeTask;
if (asyncInitializeTask != null && !asyncInitializeTask.isDone()) {
asyncInitializeTask.cancel(true); // Interrupt async init task if it isn't done yet
}
disconnect(true);
}
}

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.lutron.internal.handler;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.lutron.internal.protocol.LutronCommandNew;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.binding.BaseBridgeHandler;
/**
* Abstract base class for Lutron bridge handlers
*
* @author Bob Adair - Initial contribution
*/
@NonNullByDefault
public abstract class LutronBridgeHandler extends BaseBridgeHandler {
public LutronBridgeHandler(Bridge bridge) {
super(bridge);
}
public abstract void sendCommand(LutronCommandNew command);
}

View File

@@ -14,9 +14,17 @@ package org.openhab.binding.lutron.internal.handler;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.lutron.internal.protocol.LutronCommand;
import org.openhab.binding.lutron.internal.protocol.LutronCommandType;
import org.openhab.binding.lutron.internal.protocol.LutronOperation;
import org.openhab.binding.lutron.internal.protocol.DeviceCommand;
import org.openhab.binding.lutron.internal.protocol.GroupCommand;
import org.openhab.binding.lutron.internal.protocol.LutronCommandNew;
import org.openhab.binding.lutron.internal.protocol.LutronDuration;
import org.openhab.binding.lutron.internal.protocol.ModeCommand;
import org.openhab.binding.lutron.internal.protocol.OutputCommand;
import org.openhab.binding.lutron.internal.protocol.SysvarCommand;
import org.openhab.binding.lutron.internal.protocol.TimeclockCommand;
import org.openhab.binding.lutron.internal.protocol.lip.LutronCommandType;
import org.openhab.binding.lutron.internal.protocol.lip.LutronOperation;
import org.openhab.binding.lutron.internal.protocol.lip.TargetType;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingStatus;
@@ -30,8 +38,8 @@ import org.slf4j.LoggerFactory;
* Base type for all Lutron thing handlers.
*
* @author Allan Tong - Initial contribution
* @author Bob Adair - Added additional commands and methods for status and state management
*
* @author Bob Adair - Added additional commands and methods for status and state management. Added TargetType to
* LutronCommand for LEAP bridge.
*/
@NonNullByDefault
public abstract class LutronHandler extends BaseThingHandler {
@@ -58,10 +66,10 @@ public abstract class LutronHandler extends BaseThingHandler {
protected void thingOfflineNotify() {
}
protected @Nullable IPBridgeHandler getBridgeHandler() {
protected @Nullable LutronBridgeHandler getBridgeHandler() {
Bridge bridge = getBridge();
return bridge == null ? null : (IPBridgeHandler) bridge.getHandler();
return bridge == null ? null : (LutronBridgeHandler) bridge.getHandler();
}
@Override
@@ -79,8 +87,8 @@ public abstract class LutronHandler extends BaseThingHandler {
}
}
private void sendCommand(LutronCommand command) {
IPBridgeHandler bridgeHandler = getBridgeHandler();
protected void sendCommand(LutronCommandNew command) {
LutronBridgeHandler bridgeHandler = getBridgeHandler();
if (bridgeHandler == null) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.HANDLER_MISSING_ERROR, "No bridge associated");
@@ -90,58 +98,52 @@ public abstract class LutronHandler extends BaseThingHandler {
}
}
protected void output(Object... parameters) {
protected void output(TargetType type, int action, @Nullable Number parameter, @Nullable LutronDuration fade,
@Nullable LutronDuration delay) {
sendCommand(
new LutronCommand(LutronOperation.EXECUTE, LutronCommandType.OUTPUT, getIntegrationId(), parameters));
new OutputCommand(type, LutronOperation.EXECUTE, getIntegrationId(), action, parameter, fade, delay));
}
protected void device(Object... parameters) {
protected void queryOutput(TargetType type, int action) {
sendCommand(
new LutronCommand(LutronOperation.EXECUTE, LutronCommandType.DEVICE, getIntegrationId(), parameters));
new OutputCommand(type, LutronOperation.QUERY, getIntegrationId(), action, (Integer) null, null, null));
}
protected void timeclock(Object... parameters) {
sendCommand(new LutronCommand(LutronOperation.EXECUTE, LutronCommandType.TIMECLOCK, getIntegrationId(),
parameters));
protected void device(TargetType type, Integer component, @Nullable Integer leapComponent, Integer action,
@Nullable Object parameter) {
sendCommand(new DeviceCommand(type, LutronOperation.EXECUTE, getIntegrationId(), component, leapComponent,
action, parameter));
}
protected void greenMode(Object... parameters) {
sendCommand(new LutronCommand(LutronOperation.EXECUTE, LutronCommandType.MODE, getIntegrationId(), parameters));
protected void queryDevice(TargetType type, Integer component, Integer action) {
sendCommand(new DeviceCommand(type, LutronOperation.QUERY, getIntegrationId(), component, null, action, null));
}
protected void sysvar(Object... parameters) {
sendCommand(
new LutronCommand(LutronOperation.EXECUTE, LutronCommandType.SYSVAR, getIntegrationId(), parameters));
protected void timeclock(Integer action, @Nullable Object parameter, @Nullable Boolean enable) {
sendCommand(new TimeclockCommand(LutronOperation.EXECUTE, getIntegrationId(), action, parameter, enable));
}
protected void shadegrp(Object... parameters) {
sendCommand(
new LutronCommand(LutronOperation.EXECUTE, LutronCommandType.SHADEGRP, getIntegrationId(), parameters));
protected void queryTimeclock(Integer action) {
sendCommand(new TimeclockCommand(LutronOperation.QUERY, getIntegrationId(), action, null, null));
}
protected void queryOutput(Object... parameters) {
sendCommand(new LutronCommand(LutronOperation.QUERY, LutronCommandType.OUTPUT, getIntegrationId(), parameters));
protected void sysvar(Integer action, Object parameter) {
sendCommand(new SysvarCommand(LutronOperation.EXECUTE, getIntegrationId(), action, parameter));
}
protected void queryDevice(Object... parameters) {
sendCommand(new LutronCommand(LutronOperation.QUERY, LutronCommandType.DEVICE, getIntegrationId(), parameters));
protected void querySysvar(Integer action) {
sendCommand(new SysvarCommand(LutronOperation.QUERY, getIntegrationId(), action, null));
}
protected void queryTimeclock(Object... parameters) {
sendCommand(
new LutronCommand(LutronOperation.QUERY, LutronCommandType.TIMECLOCK, getIntegrationId(), parameters));
protected void greenMode(Integer action, @Nullable Integer parameter) {
sendCommand(new ModeCommand(LutronOperation.EXECUTE, getIntegrationId(), action, parameter));
}
protected void queryGreenMode(Object... parameters) {
sendCommand(new LutronCommand(LutronOperation.QUERY, LutronCommandType.MODE, getIntegrationId(), parameters));
protected void queryGreenMode(Integer action) {
sendCommand(new ModeCommand(LutronOperation.QUERY, getIntegrationId(), action, null));
}
protected void querySysvar(Object... parameters) {
sendCommand(new LutronCommand(LutronOperation.QUERY, LutronCommandType.SYSVAR, getIntegrationId(), parameters));
}
protected void queryShadegrp(Object... parameters) {
sendCommand(
new LutronCommand(LutronOperation.QUERY, LutronCommandType.SHADEGRP, getIntegrationId(), parameters));
protected void queryGroup(Integer action) {
sendCommand(new GroupCommand(LutronOperation.QUERY, getIntegrationId(), action, null));
}
}

View File

@@ -0,0 +1,130 @@
/**
* 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.lutron.internal.handler;
import static org.openhab.binding.lutron.internal.LutronBindingConstants.CHANNEL_GROUPSTATE;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.lutron.internal.config.OGroupConfig;
import org.openhab.binding.lutron.internal.protocol.GroupCommand;
import org.openhab.binding.lutron.internal.protocol.lip.LutronCommandType;
import org.openhab.core.library.types.StringType;
import org.openhab.core.thing.Bridge;
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.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Handler responsible for communicating occupancy group states.
*
* @author Bob Adair - Initial contribution
*/
@NonNullByDefault
public class OGroupHandler extends LutronHandler {
private static final String STATE_OCCUPIED = "OCCUPIED";
private static final String STATE_UNOCCUPIED = "UNOCCUPIED";
private static final String STATE_UNKNOWN = "UNKNOWN";
private final Logger logger = LoggerFactory.getLogger(OGroupHandler.class);
private @NonNullByDefault({}) OGroupConfig config;
public OGroupHandler(Thing thing) {
super(thing);
}
@Override
public int getIntegrationId() {
if (this.config == null) {
throw new IllegalStateException("handler not initialized");
}
return config.integrationId;
}
@Override
public void initialize() {
config = getConfigAs(OGroupConfig.class);
if (config.integrationId <= 0) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
"No valid integrationId configured");
return;
}
logger.debug("Initializing Occupancy Group handler for integration ID {}", getIntegrationId());
initDeviceState();
}
@Override
protected void initDeviceState() {
logger.debug("Initializing device state for Occupancy Group {}", getIntegrationId());
Bridge bridge = getBridge();
if (bridge == null) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "No bridge configured");
} else if (bridge.getStatus() == ThingStatus.ONLINE) {
updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.NONE, "Awaiting initial response");
queryGroup(GroupCommand.ACTION_GROUPSTATE);
// handleUpdate() will set thing status to online when response arrives
} else {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
}
}
@Override
public void channelLinked(ChannelUID channelUID) {
if (channelUID.getId().equals(CHANNEL_GROUPSTATE)) {
// Refresh state when new item is linked.
queryGroup(GroupCommand.ACTION_GROUPSTATE);
}
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
if (channelUID.getId().equals(CHANNEL_GROUPSTATE)) {
if (command instanceof RefreshType) {
queryGroup(GroupCommand.ACTION_GROUPSTATE);
}
}
}
@Override
public void handleUpdate(LutronCommandType type, String... parameters) {
int state;
if (type == LutronCommandType.GROUP && parameters.length > 1
&& GroupCommand.ACTION_GROUPSTATE.toString().equals(parameters[0])) {
try {
state = Integer.parseInt(parameters[1]);
} catch (NumberFormatException e) {
logger.debug("Error parsing response parameter: {}", e.getMessage());
return;
}
if (getThing().getStatus() == ThingStatus.UNKNOWN) {
updateStatus(ThingStatus.ONLINE);
}
if (state == GroupCommand.STATE_GRP_OCCUPIED) {
updateState(CHANNEL_GROUPSTATE, new StringType(STATE_OCCUPIED));
} else if (state == GroupCommand.STATE_GRP_UNOCCUPIED) {
updateState(CHANNEL_GROUPSTATE, new StringType(STATE_UNOCCUPIED));
} else if (state == GroupCommand.STATE_GRP_UNKNOWN) {
updateState(CHANNEL_GROUPSTATE, new StringType(STATE_UNKNOWN));
} else {
logger.debug("Invalid occupancy state received: {}", state);
updateState(CHANNEL_GROUPSTATE, new StringType(STATE_UNKNOWN));
}
}
}
}

View File

@@ -15,7 +15,8 @@ package org.openhab.binding.lutron.internal.handler;
import static org.openhab.binding.lutron.internal.LutronBindingConstants.CHANNEL_OCCUPANCYSTATUS;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.lutron.internal.protocol.LutronCommandType;
import org.openhab.binding.lutron.internal.protocol.DeviceCommand;
import org.openhab.binding.lutron.internal.protocol.lip.LutronCommandType;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.ChannelUID;
@@ -33,10 +34,6 @@ import org.slf4j.LoggerFactory;
*/
@NonNullByDefault
public class OccupancySensorHandler extends LutronHandler {
private static final String OCCUPIED_STATE_UPDATE = "2";
private static final String STATE_OCCUPIED = "3";
private static final String STATE_UNOCCUPIED = "4";
private final Logger logger = LoggerFactory.getLogger(OccupancySensorHandler.class);
private int integrationId;
@@ -82,10 +79,11 @@ public class OccupancySensorHandler extends LutronHandler {
@Override
public void handleUpdate(LutronCommandType type, String... parameters) {
if (type == LutronCommandType.DEVICE && parameters.length == 2 && OCCUPIED_STATE_UPDATE.equals(parameters[0])) {
if (STATE_OCCUPIED.equals(parameters[1])) {
if (type == LutronCommandType.DEVICE && parameters.length == 2
&& DeviceCommand.OCCUPIED_STATE_COMPONENT.equals(parameters[0])) {
if (DeviceCommand.STATE_OCCUPIED.equals(parameters[1])) {
updateState(CHANNEL_OCCUPANCYSTATUS, OnOffType.ON);
} else if (STATE_UNOCCUPIED.equals(parameters[1])) {
} else if (DeviceCommand.STATE_UNOCCUPIED.equals(parameters[1])) {
updateState(CHANNEL_OCCUPANCYSTATUS, OnOffType.OFF);
}
}

View File

@@ -42,10 +42,20 @@ public class PicoKeypadHandler extends BaseKeypadHandler {
switch (mod) {
case "2B":
buttonList = kp.getComponents(mod, ComponentType.BUTTON);
leapButtonMap = KeypadConfigPico.LEAPBUTTONS_2B;
break;
case "2BRL":
buttonList = kp.getComponents(mod, ComponentType.BUTTON);
leapButtonMap = KeypadConfigPico.LEAPBUTTONS_2BRL;
break;
case "3B":
buttonList = kp.getComponents(mod, ComponentType.BUTTON);
leapButtonMap = KeypadConfigPico.LEAPBUTTONS_3B;
break;
case "4B":
buttonList = kp.getComponents(mod, ComponentType.BUTTON);
leapButtonMap = KeypadConfigPico.LEAPBUTTONS_4B;
break;
default:
logger.warn("No valid keypad model defined ({}). Assuming model 3BRL.", mod);
@@ -53,6 +63,7 @@ public class PicoKeypadHandler extends BaseKeypadHandler {
case "Generic":
case "3BRL":
buttonList = kp.getComponents("3BRL", ComponentType.BUTTON);
leapButtonMap = KeypadConfigPico.LEAPBUTTONS_3BRL;
break;
}
}

View File

@@ -17,7 +17,9 @@ import static org.openhab.binding.lutron.internal.LutronBindingConstants.CHANNEL
import java.math.BigDecimal;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.lutron.internal.protocol.LutronCommandType;
import org.openhab.binding.lutron.internal.protocol.OutputCommand;
import org.openhab.binding.lutron.internal.protocol.lip.LutronCommandType;
import org.openhab.binding.lutron.internal.protocol.lip.TargetType;
import org.openhab.core.library.types.PercentType;
import org.openhab.core.library.types.StopMoveType;
import org.openhab.core.library.types.UpDownType;
@@ -38,16 +40,12 @@ import org.slf4j.LoggerFactory;
*/
@NonNullByDefault
public class ShadeHandler extends LutronHandler {
private static final Integer ACTION_ZONELEVEL = 1;
private static final Integer ACTION_STARTRAISING = 2;
private static final Integer ACTION_STARTLOWERING = 3;
private static final Integer ACTION_STOP = 4;
private static final Integer ACTION_POSITION_UPDATE = 32; // undocumented in integration protocol guide
private static final Integer PARAMETER_POSITION_UPDATE = 2; // undocumented in integration protocol guide
private final Logger logger = LoggerFactory.getLogger(ShadeHandler.class);
protected int integrationId;
private boolean leap = false;
public ShadeHandler(Thing thing) {
super(thing);
@@ -68,6 +66,11 @@ public class ShadeHandler extends LutronHandler {
integrationId = id.intValue();
logger.debug("Initializing Shade handler for integration ID {}", id);
LutronBridgeHandler bridgeHandler = getBridgeHandler();
if (bridgeHandler instanceof LeapBridgeHandler) {
leap = true;
}
initDeviceState();
}
@@ -79,7 +82,8 @@ public class ShadeHandler extends LutronHandler {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "No bridge configured");
} else if (bridge.getStatus() == ThingStatus.ONLINE) {
updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.NONE, "Awaiting initial response");
queryOutput(ACTION_ZONELEVEL); // handleUpdate() will set thing status to online when response arrives
queryOutput(TargetType.SHADE, OutputCommand.ACTION_ZONELEVEL);
// handleUpdate() will set thing status to online when response arrives
} else {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
}
@@ -89,7 +93,7 @@ public class ShadeHandler extends LutronHandler {
public void channelLinked(ChannelUID channelUID) {
// Refresh state when new item is linked.
if (channelUID.getId().equals(CHANNEL_SHADELEVEL)) {
queryOutput(ACTION_ZONELEVEL);
queryOutput(TargetType.SHADE, OutputCommand.ACTION_ZONELEVEL);
}
}
@@ -98,15 +102,27 @@ public class ShadeHandler extends LutronHandler {
if (channelUID.getId().equals(CHANNEL_SHADELEVEL)) {
if (command instanceof PercentType) {
int level = ((PercentType) command).intValue();
output(ACTION_ZONELEVEL, level, 0);
output(TargetType.SHADE, OutputCommand.ACTION_ZONELEVEL, level, null, null);
if (leap) {
// LEAP may not send back a position update
updateState(CHANNEL_SHADELEVEL, new PercentType(level));
}
} else if (command.equals(UpDownType.UP)) {
output(ACTION_STARTRAISING);
output(TargetType.SHADE, OutputCommand.ACTION_STARTRAISING, null, null, null);
if (leap) {
// LEAP won't send a position update when fully open
updateState(CHANNEL_SHADELEVEL, new PercentType(100));
}
} else if (command.equals(UpDownType.DOWN)) {
output(ACTION_STARTLOWERING);
output(TargetType.SHADE, OutputCommand.ACTION_STARTLOWERING, null, null, null);
if (leap) {
// LEAP won't send a position update when fully closed
updateState(CHANNEL_SHADELEVEL, new PercentType(0));
}
} else if (command.equals(StopMoveType.STOP)) {
output(ACTION_STOP);
output(TargetType.SHADE, OutputCommand.ACTION_STOP, null, null, null);
} else if (command instanceof RefreshType) {
queryOutput(ACTION_ZONELEVEL);
queryOutput(TargetType.SHADE, OutputCommand.ACTION_ZONELEVEL);
}
}
}
@@ -114,14 +130,14 @@ public class ShadeHandler extends LutronHandler {
@Override
public void handleUpdate(LutronCommandType type, String... parameters) {
if (type == LutronCommandType.OUTPUT && parameters.length >= 2) {
if (ACTION_ZONELEVEL.toString().equals(parameters[0])) {
if (OutputCommand.ACTION_ZONELEVEL.toString().equals(parameters[0])) {
BigDecimal level = new BigDecimal(parameters[1]);
if (getThing().getStatus() == ThingStatus.UNKNOWN) {
updateStatus(ThingStatus.ONLINE);
}
logger.trace("Shade {} received zone level: {}", getIntegrationId(), level);
updateState(CHANNEL_SHADELEVEL, new PercentType(level));
} else if (ACTION_POSITION_UPDATE.toString().equals(parameters[0])
} else if (OutputCommand.ACTION_POSITION_UPDATE.toString().equals(parameters[0])
&& PARAMETER_POSITION_UPDATE.toString().equals(parameters[1]) && parameters.length >= 3) {
BigDecimal level = new BigDecimal(parameters[2]);
logger.trace("Shade {} received position update: {}", getIntegrationId(), level);

View File

@@ -17,7 +17,9 @@ import static org.openhab.binding.lutron.internal.LutronBindingConstants.CHANNEL
import java.math.BigDecimal;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.lutron.internal.protocol.LutronCommandType;
import org.openhab.binding.lutron.internal.protocol.OutputCommand;
import org.openhab.binding.lutron.internal.protocol.lip.LutronCommandType;
import org.openhab.binding.lutron.internal.protocol.lip.TargetType;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.ChannelUID;
@@ -36,8 +38,6 @@ import org.slf4j.LoggerFactory;
*/
@NonNullByDefault
public class SwitchHandler extends LutronHandler {
private static final Integer ACTION_ZONELEVEL = 1;
private final Logger logger = LoggerFactory.getLogger(SwitchHandler.class);
private int integrationId;
@@ -67,7 +67,8 @@ public class SwitchHandler extends LutronHandler {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "No bridge configured");
} else if (bridge.getStatus() == ThingStatus.ONLINE) {
updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.NONE, "Awaiting initial response");
queryOutput(ACTION_ZONELEVEL); // handleUpdate() will set thing status to online when response arrives
queryOutput(TargetType.SWITCH, OutputCommand.ACTION_ZONELEVEL);
// handleUpdate() will set thing status to online when response arrives
} else {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
}
@@ -77,9 +78,9 @@ public class SwitchHandler extends LutronHandler {
public void handleCommand(ChannelUID channelUID, Command command) {
if (channelUID.getId().equals(CHANNEL_SWITCH)) {
if (command.equals(OnOffType.ON)) {
output(ACTION_ZONELEVEL, 100);
output(TargetType.SWITCH, OutputCommand.ACTION_ZONELEVEL, 100, null, null);
} else if (command.equals(OnOffType.OFF)) {
output(ACTION_ZONELEVEL, 0);
output(TargetType.SWITCH, OutputCommand.ACTION_ZONELEVEL, 0, null, null);
}
}
}
@@ -92,7 +93,7 @@ public class SwitchHandler extends LutronHandler {
@Override
public void handleUpdate(LutronCommandType type, String... parameters) {
if (type == LutronCommandType.OUTPUT && parameters.length > 1
&& ACTION_ZONELEVEL.toString().equals(parameters[0])) {
&& OutputCommand.ACTION_ZONELEVEL.toString().equals(parameters[0])) {
BigDecimal level = new BigDecimal(parameters[1]);
if (getThing().getStatus() == ThingStatus.UNKNOWN) {
updateStatus(ThingStatus.ONLINE);
@@ -105,7 +106,7 @@ public class SwitchHandler extends LutronHandler {
public void channelLinked(ChannelUID channelUID) {
if (channelUID.getId().equals(CHANNEL_SWITCH)) {
// Refresh state when new item is linked.
queryOutput(ACTION_ZONELEVEL);
queryOutput(TargetType.SWITCH, OutputCommand.ACTION_ZONELEVEL);
}
}
}

View File

@@ -19,7 +19,8 @@ import java.math.BigDecimal;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.lutron.internal.config.SysvarConfig;
import org.openhab.binding.lutron.internal.protocol.LutronCommandType;
import org.openhab.binding.lutron.internal.protocol.SysvarCommand;
import org.openhab.binding.lutron.internal.protocol.lip.LutronCommandType;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.ChannelUID;
@@ -38,8 +39,6 @@ import org.slf4j.LoggerFactory;
*/
@NonNullByDefault
public class SysvarHandler extends LutronHandler {
private static final Integer ACTION_GETSETSYSVAR = 1;
private final Logger logger = LoggerFactory.getLogger(SysvarHandler.class);
private @Nullable SysvarConfig config;
@@ -80,7 +79,8 @@ public class SysvarHandler extends LutronHandler {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "No bridge configured");
} else if (bridge.getStatus() == ThingStatus.ONLINE) {
updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.NONE, "Awaiting initial response");
querySysvar(ACTION_GETSETSYSVAR); // handleUpdate() will set thing status to online when response arrives
querySysvar(SysvarCommand.ACTION_GETSETSYSVAR);
// handleUpdate() will set thing status to online when response arrives
} else {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
}
@@ -90,7 +90,7 @@ public class SysvarHandler extends LutronHandler {
public void channelLinked(ChannelUID channelUID) {
if (channelUID.getId().equals(CHANNEL_VARSTATE)) {
// Refresh state when new item is linked.
querySysvar(ACTION_GETSETSYSVAR);
querySysvar(SysvarCommand.ACTION_GETSETSYSVAR);
}
}
@@ -99,7 +99,7 @@ public class SysvarHandler extends LutronHandler {
if (channelUID.getId().equals(CHANNEL_VARSTATE)) {
if (command instanceof Number) {
int state = ((Number) command).intValue();
sysvar(ACTION_GETSETSYSVAR, state);
sysvar(SysvarCommand.ACTION_GETSETSYSVAR, state);
}
}
}
@@ -107,7 +107,7 @@ public class SysvarHandler extends LutronHandler {
@Override
public void handleUpdate(LutronCommandType type, String... parameters) {
if (type == LutronCommandType.SYSVAR && parameters.length > 1
&& ACTION_GETSETSYSVAR.toString().equals(parameters[0])) {
&& SysvarCommand.ACTION_GETSETSYSVAR.toString().equals(parameters[0])) {
BigDecimal state = new BigDecimal(parameters[1]);
if (getThing().getStatus() == ThingStatus.UNKNOWN) {
updateStatus(ThingStatus.ONLINE);

View File

@@ -20,7 +20,8 @@ import java.util.Calendar;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.lutron.internal.protocol.LutronCommandType;
import org.openhab.binding.lutron.internal.protocol.TimeclockCommand;
import org.openhab.binding.lutron.internal.protocol.lip.LutronCommandType;
import org.openhab.core.library.types.DateTimeType;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.thing.Bridge;
@@ -40,14 +41,6 @@ import org.slf4j.LoggerFactory;
*/
@NonNullByDefault
public class TimeclockHandler extends LutronHandler {
private static final Integer ACTION_CLOCKMODE = 1;
private static final Integer ACTION_SUNRISE = 2;
private static final Integer ACTION_SUNSET = 3;
private static final Integer ACTION_EXECEVENT = 5;
private static final Integer ACTION_SETEVENT = 6;
private static final Integer EVENT_ENABLE = 1;
private static final Integer EVENT_DISABLE = 2;
private final Logger logger = LoggerFactory.getLogger(TimeclockHandler.class);
private int integrationId;
@@ -81,7 +74,8 @@ public class TimeclockHandler extends LutronHandler {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "No bridge configured");
} else if (bridge.getStatus() == ThingStatus.ONLINE) {
updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.NONE, "Awaiting initial response");
queryTimeclock(ACTION_CLOCKMODE); // handleUpdate() will set thing status to online when response arrives
queryTimeclock(TimeclockCommand.ACTION_CLOCKMODE);
// handleUpdate() will set thing status to online when response arrives
} else {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
}
@@ -91,7 +85,7 @@ public class TimeclockHandler extends LutronHandler {
public void channelLinked(ChannelUID channelUID) {
logger.debug("Handling channel link request for timeclock {}", integrationId);
if (channelUID.getId().equals(CHANNEL_CLOCKMODE)) {
queryTimeclock(ACTION_CLOCKMODE);
queryTimeclock(TimeclockCommand.ACTION_CLOCKMODE);
}
}
@@ -103,42 +97,42 @@ public class TimeclockHandler extends LutronHandler {
if (channelUID.getId().equals(CHANNEL_CLOCKMODE)) {
if (command instanceof DecimalType) {
Integer mode = ((DecimalType) command).intValue();
timeclock(ACTION_CLOCKMODE, mode);
timeclock(TimeclockCommand.ACTION_CLOCKMODE, mode, null);
} else if (command instanceof RefreshType) {
queryTimeclock(ACTION_CLOCKMODE);
queryTimeclock(TimeclockCommand.ACTION_CLOCKMODE);
} else {
logger.debug("Invalid command type for clockmode channnel");
}
} else if (channelUID.getId().equals(CHANNEL_EXECEVENT)) {
if (command instanceof DecimalType) {
Integer index = ((DecimalType) command).intValue();
timeclock(ACTION_EXECEVENT, index);
timeclock(TimeclockCommand.ACTION_EXECEVENT, index, null);
} else {
logger.debug("Invalid command type for execevent channnel");
}
} else if (channelUID.getId().equals(CHANNEL_SUNRISE)) {
if (command instanceof RefreshType) {
queryTimeclock(ACTION_SUNRISE);
queryTimeclock(TimeclockCommand.ACTION_SUNRISE);
} else {
logger.debug("Invalid command type for sunrise channnel");
}
} else if (channelUID.getId().equals(CHANNEL_SUNSET)) {
if (command instanceof RefreshType) {
queryTimeclock(ACTION_SUNSET);
queryTimeclock(TimeclockCommand.ACTION_SUNSET);
} else {
logger.debug("Invalid command type for sunset channnel");
}
} else if (channelUID.getId().equals(CHANNEL_ENABLEEVENT)) {
if (command instanceof DecimalType) {
Integer index = ((DecimalType) command).intValue();
timeclock(ACTION_SETEVENT, index, EVENT_ENABLE);
timeclock(TimeclockCommand.ACTION_SETEVENT, index, true);
} else {
logger.debug("Invalid command type for enableevent channnel");
}
} else if (channelUID.getId().equals(CHANNEL_DISABLEEVENT)) {
if (command instanceof DecimalType) {
Integer index = ((DecimalType) command).intValue();
timeclock(ACTION_SETEVENT, index, EVENT_DISABLE);
timeclock(TimeclockCommand.ACTION_SETEVENT, index, false);
} else {
logger.debug("Invalid command type for disableevent channnel");
}
@@ -172,37 +166,37 @@ public class TimeclockHandler extends LutronHandler {
logger.debug("Handling update received from timeclock {}", integrationId);
try {
if (parameters.length >= 2 && ACTION_CLOCKMODE.toString().equals(parameters[0])) {
if (parameters.length >= 2 && TimeclockCommand.ACTION_CLOCKMODE.toString().equals(parameters[0])) {
Integer mode = Integer.valueOf(parameters[1]);
if (getThing().getStatus() == ThingStatus.UNKNOWN) {
updateStatus(ThingStatus.ONLINE);
}
updateState(CHANNEL_CLOCKMODE, new DecimalType(mode));
} else if (parameters.length >= 2 && ACTION_SUNRISE.toString().equals(parameters[0])) {
} else if (parameters.length >= 2 && TimeclockCommand.ACTION_SUNRISE.toString().equals(parameters[0])) {
Calendar calendar = parseLutronTime(parameters[1]);
if (calendar != null) {
updateState(CHANNEL_SUNRISE,
new DateTimeType(ZonedDateTime.ofInstant(calendar.toInstant(), ZoneId.systemDefault())));
}
} else if (parameters.length >= 2 && ACTION_SUNSET.toString().equals(parameters[0])) {
} else if (parameters.length >= 2 && TimeclockCommand.ACTION_SUNSET.toString().equals(parameters[0])) {
Calendar calendar = parseLutronTime(parameters[1]);
if (calendar != null) {
updateState(CHANNEL_SUNSET,
new DateTimeType(ZonedDateTime.ofInstant(calendar.toInstant(), ZoneId.systemDefault())));
}
} else if (parameters.length >= 2 && ACTION_EXECEVENT.toString().equals(parameters[0])) {
} else if (parameters.length >= 2 && TimeclockCommand.ACTION_EXECEVENT.toString().equals(parameters[0])) {
Integer index = Integer.valueOf(parameters[1]);
updateState(CHANNEL_EXECEVENT, new DecimalType(index));
} else if (parameters.length >= 3 && ACTION_SETEVENT.toString().equals(parameters[0])) {
} else if (parameters.length >= 3 && TimeclockCommand.ACTION_SETEVENT.toString().equals(parameters[0])) {
Integer index = Integer.valueOf(parameters[1]);
Integer state = Integer.valueOf(parameters[2]);
if (state.equals(EVENT_ENABLE)) {
if (state.equals(TimeclockCommand.EVENT_ENABLE)) {
updateState(CHANNEL_ENABLEEVENT, new DecimalType(index));
} else if (state.equals(EVENT_DISABLE)) {
} else if (state.equals(TimeclockCommand.EVENT_DISABLE)) {
updateState(CHANNEL_DISABLEEVENT, new DecimalType(index));
}
}

View File

@@ -16,6 +16,7 @@ import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.lutron.internal.KeypadComponent;
import org.openhab.binding.lutron.internal.discovery.project.ComponentType;
import org.openhab.binding.lutron.internal.protocol.lip.TargetType;
import org.openhab.core.thing.Thing;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -100,5 +101,6 @@ public class VirtualKeypadHandler extends BaseKeypadHandler {
super(thing);
// Mark all channels "Advanced" since most are unlikely to be used in any particular config
advancedChannels = true;
commandTargetType = TargetType.VIRTUALKEYPAD; // For the LEAP bridge
}
}

View File

@@ -13,6 +13,7 @@
package org.openhab.binding.lutron.internal.keypadconfig;
import java.util.Arrays;
import java.util.Map;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.lutron.internal.KeypadComponent;
@@ -26,6 +27,13 @@ import org.openhab.binding.lutron.internal.discovery.project.ComponentType;
@NonNullByDefault
public final class KeypadConfigPico extends KeypadConfig {
// Button mappings for LEAP protocol
public static final Map<Integer, Integer> LEAPBUTTONS_2B = Map.of(2, 1, 4, 2);
public static final Map<Integer, Integer> LEAPBUTTONS_2BRL = Map.of(2, 1, 4, 2, 5, 3, 6, 4);
public static final Map<Integer, Integer> LEAPBUTTONS_3B = Map.of(2, 1, 3, 2, 4, 3);
public static final Map<Integer, Integer> LEAPBUTTONS_4B = Map.of(8, 1, 9, 2, 10, 3, 11, 4);
public static final Map<Integer, Integer> LEAPBUTTONS_3BRL = Map.of(2, 1, 3, 2, 4, 3, 5, 4, 6, 5);
private static enum Component implements KeypadComponent {
// Buttons for 2B, 2BRL, 3B, and 3BRL models
BUTTON1(2, "button1", "Button 1", ComponentType.BUTTON),

View File

@@ -0,0 +1,121 @@
/**
* 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.lutron.internal.protocol;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.lutron.internal.handler.LeapBridgeHandler;
import org.openhab.binding.lutron.internal.protocol.leap.CommandType;
import org.openhab.binding.lutron.internal.protocol.leap.LeapCommand;
import org.openhab.binding.lutron.internal.protocol.leap.Request;
import org.openhab.binding.lutron.internal.protocol.lip.LutronCommandType;
import org.openhab.binding.lutron.internal.protocol.lip.LutronOperation;
import org.openhab.binding.lutron.internal.protocol.lip.TargetType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Lutron DEVICE command object
*
* @author Bob Adair - Initial contribution
*/
@NonNullByDefault
public class DeviceCommand extends LutronCommandNew {
// keypad defs
public static final Integer ACTION_PRESS = 3;
public static final Integer ACTION_RELEASE = 4;
public static final Integer ACTION_HOLD = 5;
public static final Integer ACTION_LED_STATE = 9;
public static final Integer LED_OFF = 0;
public static final Integer LED_ON = 1;
public static final Integer LED_FLASH = 2; // Same as 1 on RA2 keypads
public static final Integer LED_RAPIDFLASH = 3; // Same as 1 on RA2 keypads
// occupancy sensor defs
public static final String OCCUPIED_STATE_COMPONENT = "2";
public static final String STATE_OCCUPIED = "3";
public static final String STATE_UNOCCUPIED = "4";
private final Logger logger = LoggerFactory.getLogger(DeviceCommand.class);
private final Integer component;
private final @Nullable Integer leapComponent;
private final Integer action;
private final @Nullable Object parameter;
public DeviceCommand(TargetType targetType, LutronOperation operation, Integer integrationId, Integer component,
@Nullable Integer leapComponent, Integer action, @Nullable Object parameter) {
super(targetType, operation, LutronCommandType.DEVICE, integrationId);
this.action = action;
this.component = component;
this.leapComponent = leapComponent;
this.parameter = parameter;
}
@Override
public String lipCommand() {
StringBuilder builder = new StringBuilder().append(operation).append(commandType);
builder.append(',').append(integrationId);
builder.append(',').append(component);
builder.append(',').append(action);
if (parameter != null) {
builder.append(',').append(parameter);
}
return builder.toString();
}
@Override
public @Nullable LeapCommand leapCommand(LeapBridgeHandler bridgeHandler, @Nullable Integer leapZone) {
if (targetType == TargetType.KEYPAD) {
if (leapComponent == null) {
logger.debug("Ignoring device command. No leap component in command.");
return null;
}
Integer integrationId = this.integrationId;
Integer leapComponent = this.leapComponent; // make the broken null checker happy
if (action.equals(DeviceCommand.ACTION_PRESS) && integrationId != null && leapComponent != null) {
int button = bridgeHandler.getButton(integrationId, leapComponent);
if (button > 0) {
return new LeapCommand(Request.buttonCommand(button, CommandType.PRESSANDHOLD));
}
} else if (action.equals(DeviceCommand.ACTION_RELEASE) && integrationId != null && leapComponent != null) {
int button = bridgeHandler.getButton(integrationId, leapComponent);
if (button > 0) {
return new LeapCommand(Request.buttonCommand(button, CommandType.RELEASE));
}
} else {
logger.debug("Ignoring device command with unsupported action.");
return null;
}
} else if (targetType == TargetType.VIRTUALKEYPAD) {
if (action.equals(DeviceCommand.ACTION_PRESS)) {
return new LeapCommand(Request.virtualButtonCommand(component, CommandType.PRESSANDRELEASE));
} else if (!action.equals(DeviceCommand.ACTION_RELEASE)) {
logger.debug("Ignoring device command with unsupported action.");
return null;
}
} else {
logger.debug("Ignoring device command with unsupported target type.");
return null;
}
logger.debug("Ignoring unsupported device command.");
return null;
}
@Override
public String toString() {
return lipCommand();
}
}

View File

@@ -0,0 +1,83 @@
/**
* 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.lutron.internal.protocol;
import org.eclipse.jdt.annotation.NonNullByDefault;
import com.google.gson.annotations.SerializedName;
/**
* Defines Lutron fan controller speed settings
*
* @author Bob Adair - Initial contribution
*/
@NonNullByDefault
public enum FanSpeedType {
@SerializedName("High")
HIGH(100, "High"),
@SerializedName("MediumHigh")
MEDIUMHIGH(75, "MediumHigh"),
@SerializedName("Medium")
MEDIUM(50, "Medium"),
@SerializedName("Low")
LOW(25, "Low"),
@SerializedName("Off")
OFF(0, "Off");
/** Fan speed expressed as a percentage **/
private final int speed;
/** Fan speed expressed as a String (used by LEAP) **/
private final String leapValue;
FanSpeedType(int speed, String leapValue) {
this.speed = speed;
this.leapValue = leapValue;
}
public int speed() {
return speed;
}
public String leapValue() {
return leapValue;
}
@Override
public String toString() {
return leapValue;
}
public static FanSpeedType toFanSpeedType(int percentage) {
if (percentage == OFF.speed) {
return FanSpeedType.OFF;
} else if (percentage > OFF.speed && percentage <= LOW.speed) {
return FanSpeedType.LOW;
} else if (percentage > LOW.speed && percentage <= MEDIUM.speed) {
return FanSpeedType.MEDIUM;
} else if (percentage > MEDIUM.speed && percentage <= MEDIUMHIGH.speed) {
return FanSpeedType.MEDIUMHIGH;
} else {
return FanSpeedType.HIGH;
}
}
public static FanSpeedType toFanSpeedType(String speedString) {
for (FanSpeedType enumValue : FanSpeedType.values()) {
if (enumValue.leapValue.equalsIgnoreCase(speedString)) {
return enumValue;
}
}
return OFF;
}
}

View File

@@ -0,0 +1,80 @@
/**
* 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.lutron.internal.protocol;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.lutron.internal.handler.LeapBridgeHandler;
import org.openhab.binding.lutron.internal.protocol.leap.LeapCommand;
import org.openhab.binding.lutron.internal.protocol.leap.Request;
import org.openhab.binding.lutron.internal.protocol.lip.LutronCommandType;
import org.openhab.binding.lutron.internal.protocol.lip.LutronOperation;
import org.openhab.binding.lutron.internal.protocol.lip.TargetType;
/**
* Lutron GROUP command object
*
* @author Bob Adair - Initial contribution
*/
@NonNullByDefault
public class GroupCommand extends LutronCommandNew {
public static final Integer ACTION_GROUPSTATE = 3;
public static final Integer STATE_GRP_OCCUPIED = 3;
public static final Integer STATE_GRP_UNOCCUPIED = 4;
public static final Integer STATE_GRP_UNKNOWN = 255;
private final Integer action;
private final @Nullable Integer state;
/**
* GroupCommand constructor
*
* @param targetType
* @param operation
* @param integrationId
* @param action
* @param state
*/
public GroupCommand(LutronOperation operation, Integer integrationId, Integer action, @Nullable Integer state) {
super(TargetType.GROUP, operation, LutronCommandType.GROUP, integrationId);
this.action = action;
this.state = state;
}
@Override
public String lipCommand() {
StringBuilder builder = new StringBuilder().append(operation).append(commandType);
builder.append(',').append(integrationId);
builder.append(',').append(action);
if (state != null) {
builder.append(',').append(state);
}
return builder.toString();
}
@Override
public @Nullable LeapCommand leapCommand(LeapBridgeHandler bridgeHandler, @Nullable Integer leapZone) {
if (action.equals(GroupCommand.ACTION_GROUPSTATE)) {
// Get status for all occupancy groups because you can't query just one
return new LeapCommand(Request.getOccupancyGroupStatus());
} else {
return null;
}
}
@Override
public String toString() {
return lipCommand();
}
}

View File

@@ -0,0 +1,71 @@
/**
* 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.lutron.internal.protocol;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.lutron.internal.handler.LeapBridgeHandler;
import org.openhab.binding.lutron.internal.protocol.leap.LeapCommand;
import org.openhab.binding.lutron.internal.protocol.lip.LutronCommandType;
import org.openhab.binding.lutron.internal.protocol.lip.LutronOperation;
import org.openhab.binding.lutron.internal.protocol.lip.TargetType;
/**
* Generic LIP command for use inside bridge handler
*
* @author Bob Adair - Initial contribution
*/
@NonNullByDefault
public class LIPCommand extends LutronCommandNew {
private final Object[] parameters;
public LIPCommand(TargetType targetType, LutronOperation operation, LutronCommandType CommandType,
@Nullable Integer integrationId, Object... parameters) {
super(targetType, operation, CommandType, integrationId);
this.parameters = parameters;
}
@Override
public String lipCommand() {
StringBuilder builder = new StringBuilder().append(operation).append(commandType);
if (integrationId != null) {
builder.append(',').append(integrationId);
}
if (parameters != null) { // This CAN be null
for (Object parameter : parameters) {
builder.append(',').append(parameter);
}
}
return builder.toString();
}
@Override
public @Nullable LeapCommand leapCommand(LeapBridgeHandler bridgeHandler, @Nullable Integer leapZone) {
return null;
}
@Override
public String toString() {
return lipCommand();
}
public int getNumberParameter(int position) {
if (parameters.length > position && parameters[position] instanceof Number) {
Number num = (Number) parameters[position];
return num.intValue();
} else {
throw new IllegalArgumentException("Invalid command parameter");
}
}
}

View File

@@ -1,62 +0,0 @@
/**
* 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.lutron.internal.protocol;
/**
* Command to a Lutron integration access point.
*
* @author Allan Tong - Initial contribution
*
*/
public class LutronCommand {
private final LutronOperation operation;
private final LutronCommandType type;
private final int integrationId;
private final Object[] parameters;
public LutronCommand(LutronOperation operation, LutronCommandType type, int integrationId, Object... parameters) {
this.operation = operation;
this.type = type;
this.integrationId = integrationId;
this.parameters = parameters;
}
public LutronCommandType getType() {
return this.type;
}
public int getIntegrationId() {
return this.integrationId;
}
public Object[] getParameters() {
return this.parameters;
}
@Override
public String toString() {
StringBuilder builder = new StringBuilder().append(this.operation).append(this.type);
if (integrationId >= 0) {
builder.append(',').append(this.integrationId);
}
if (parameters != null) {
for (Object parameter : parameters) {
builder.append(',').append(parameter);
}
}
return builder.toString();
}
}

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.lutron.internal.protocol;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.lutron.internal.handler.LeapBridgeHandler;
import org.openhab.binding.lutron.internal.protocol.leap.LeapCommand;
import org.openhab.binding.lutron.internal.protocol.lip.LutronCommandType;
import org.openhab.binding.lutron.internal.protocol.lip.LutronOperation;
import org.openhab.binding.lutron.internal.protocol.lip.TargetType;
/**
* Lutron command abstract base class
*
* @author Bob Adair - Initial contribution
*/
@NonNullByDefault
public abstract class LutronCommandNew {
public final TargetType targetType;
protected final LutronOperation operation;
protected final LutronCommandType commandType;
protected final @Nullable Integer integrationId;
public LutronCommandNew(TargetType targetType, LutronOperation operation, LutronCommandType type,
@Nullable Integer integrationId) {
this.targetType = targetType;
this.operation = operation;
this.commandType = type;
this.integrationId = integrationId;
}
public LutronCommandType getType() {
return commandType;
}
public LutronOperation getOperation() {
return operation;
}
public @Nullable Integer getIntegrationId() {
return integrationId;
}
public abstract String lipCommand();
public abstract @Nullable LeapCommand leapCommand(LeapBridgeHandler bridgeHandler, @Nullable Integer leapZone);
}

View File

@@ -131,7 +131,15 @@ public class LutronDuration {
}
public String asLeapString() {
return ""; // TBD
Integer seconds = this.seconds;
if (seconds.equals(0) && hundredths > 0) {
// use 1 second if interval is > 0 and < 1
seconds = 1;
} else if (hundredths >= 50) {
// else apply normal rounding of hundredths
seconds++;
}
return String.format("%02d:%02d:%02d", seconds / 3600, (seconds % 3600) / 60, (seconds % 60));
}
@Override

View File

@@ -0,0 +1,62 @@
/**
* 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.lutron.internal.protocol;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.lutron.internal.handler.LeapBridgeHandler;
import org.openhab.binding.lutron.internal.protocol.leap.LeapCommand;
import org.openhab.binding.lutron.internal.protocol.lip.LutronCommandType;
import org.openhab.binding.lutron.internal.protocol.lip.LutronOperation;
import org.openhab.binding.lutron.internal.protocol.lip.TargetType;
/**
* Lutron MODE command object
*
* @author Bob Adair - Initial contribution
*/
@NonNullByDefault
public class ModeCommand extends LutronCommandNew {
public static final Integer ACTION_STEP = 1;
private final Integer action;
private final @Nullable Integer parameter;
public ModeCommand(LutronOperation operation, Integer integrationId, Integer action, @Nullable Integer parameter) {
super(TargetType.GREENMODE, operation, LutronCommandType.MODE, integrationId);
this.action = action;
this.parameter = parameter;
}
@Override
public String lipCommand() {
StringBuilder builder = new StringBuilder().append(operation).append(commandType);
builder.append(',').append(integrationId);
builder.append(',').append(action);
if (parameter != null) {
builder.append(',').append(parameter);
}
return builder.toString();
}
@Override
public @Nullable LeapCommand leapCommand(LeapBridgeHandler bridgeHandler, @Nullable Integer leapZone) {
return null;
}
@Override
public String toString() {
return lipCommand();
}
}

View File

@@ -0,0 +1,211 @@
/**
* 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.lutron.internal.protocol;
import java.util.Locale;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.lutron.internal.handler.LeapBridgeHandler;
import org.openhab.binding.lutron.internal.protocol.leap.CommandType;
import org.openhab.binding.lutron.internal.protocol.leap.LeapCommand;
import org.openhab.binding.lutron.internal.protocol.leap.Request;
import org.openhab.binding.lutron.internal.protocol.lip.LutronCommandType;
import org.openhab.binding.lutron.internal.protocol.lip.LutronOperation;
import org.openhab.binding.lutron.internal.protocol.lip.TargetType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Lutron OUTPUT command object
*
* @author Bob Adair - Initial contribution
*/
@NonNullByDefault
public class OutputCommand extends LutronCommandNew {
// shade, blind, dimmer defs
public static final Integer ACTION_ZONELEVEL = 1;
public static final Integer ACTION_STARTRAISING = 2;
public static final Integer ACTION_STARTLOWERING = 3;
public static final Integer ACTION_STOP = 4;
public static final Integer ACTION_POSITION_UPDATE = 32; // For shades/blinds. Undocumented in protocol guide.
// blind defs
public static final Integer ACTION_LIFTLEVEL = 1;
public static final Integer ACTION_TILTLEVEL = 9;
public static final Integer ACTION_LIFTTILTLEVEL = 10;
public static final Integer ACTION_STARTRAISINGTILT = 11;
public static final Integer ACTION_STARTLOWERINGTILT = 12;
public static final Integer ACTION_STOPTILT = 13;
public static final Integer ACTION_STARTRAISINGLIFT = 14;
public static final Integer ACTION_STARTLOWERINGLIFT = 15;
public static final Integer ACTION_STOPLIFT = 16;
// cco defs
public static final Integer ACTION_STATE = 1;
public static final Integer ACTION_PULSE = 6;
private final Logger logger = LoggerFactory.getLogger(OutputCommand.class);
private final Integer action;
private final @Nullable Number parameter;
private final @Nullable LutronDuration fadeTime;
private final @Nullable LutronDuration delayTime;
private final FanSpeedType fanSpeed;
/**
* OutputCommand constructor
*
* @param targetType
* @param operation
* @param integrationId
* @param action
* @param parameter
* @param fadeTime
* @param delayTime
*/
public OutputCommand(TargetType targetType, LutronOperation operation, Integer integrationId, Integer action,
@Nullable Number parameter, @Nullable LutronDuration fadeTime, @Nullable LutronDuration delayTime) {
super(targetType, operation, LutronCommandType.OUTPUT, integrationId);
this.action = action;
this.parameter = parameter;
if (parameter != null) {
this.fanSpeed = FanSpeedType.toFanSpeedType(parameter.intValue());
} else {
this.fanSpeed = FanSpeedType.OFF;
}
this.fadeTime = fadeTime;
this.delayTime = delayTime;
}
/**
* OutputCommand constructor for fan commands
*
* @param targetType
* @param operation
* @param integrationId
* @param action
* @param fanSpeed
* @param fadeTime
* @param delayTime
*/
public OutputCommand(TargetType targetType, LutronOperation operation, Integer integrationId, Integer action,
FanSpeedType fanSpeed, @Nullable LutronDuration fadeTime, @Nullable LutronDuration delayTime) {
super(targetType, operation, LutronCommandType.OUTPUT, integrationId);
this.action = action;
this.fanSpeed = fanSpeed;
this.parameter = fanSpeed.speed();
this.fadeTime = fadeTime;
this.delayTime = delayTime;
}
@Override
public String lipCommand() {
StringBuilder builder = new StringBuilder().append(operation).append(commandType);
builder.append(',').append(integrationId);
builder.append(',').append(action);
if (parameter != null && targetType == TargetType.CCO && action.equals(OutputCommand.ACTION_PULSE)) {
builder.append(',').append(String.format(Locale.ROOT, "%.2f", parameter));
} else if (parameter != null) {
builder.append(',').append(parameter);
}
if (fadeTime != null) {
builder.append(',').append(fadeTime);
} else if (fadeTime == null && delayTime != null) {
// must add 0 placeholder here in order to set delay time
builder.append(',').append("0");
}
if (delayTime != null) {
builder.append(',').append(delayTime);
}
return builder.toString();
}
@Override
public @Nullable LeapCommand leapCommand(LeapBridgeHandler bridgeHandler, @Nullable Integer leapZone) {
int zone;
Number parameter = this.parameter;
if (leapZone == null) {
return null;
} else {
zone = leapZone;
}
if (operation == LutronOperation.QUERY) {
if (action.equals(OutputCommand.ACTION_ZONELEVEL)) {
return new LeapCommand(Request.getZoneStatus(zone));
} else {
logger.debug("Ignoring unsupported query action");
return null;
}
} else if (operation == LutronOperation.EXECUTE) {
if (targetType == TargetType.SWITCH) {
if (action.equals(OutputCommand.ACTION_ZONELEVEL) && parameter != null) {
return new LeapCommand(Request.goToLevel(zone, parameter.intValue()));
} else {
logger.debug("Ignoring unsupported switch action");
return null;
}
} else if (targetType == TargetType.DIMMER) {
if (action.equals(OutputCommand.ACTION_ZONELEVEL) && parameter != null) {
if (fadeTime == null && delayTime == null) {
return new LeapCommand(Request.goToLevel(zone, parameter.intValue()));
} else {
LutronDuration fade = (fadeTime == null) ? new LutronDuration(0) : fadeTime;
LutronDuration delay = (delayTime == null) ? new LutronDuration(0) : delayTime;
return new LeapCommand(Request.goToDimmedLevel(zone, parameter.intValue(), fade.asLeapString(),
delay.asLeapString()));
}
} else {
logger.debug("Ignoring unsupported dimmer action");
return null;
}
} else if (targetType == TargetType.FAN) {
if (action.equals(OutputCommand.ACTION_ZONELEVEL)) {
return new LeapCommand(Request.goToFanSpeed(zone, fanSpeed));
} else {
logger.debug("Ignoring unsupported fan action");
return null;
}
} else if (targetType == TargetType.SHADE) {
if (action.equals(OutputCommand.ACTION_ZONELEVEL) && parameter != null) {
return new LeapCommand(Request.goToLevel(zone, parameter.intValue()));
} else if (action.equals(OutputCommand.ACTION_STARTRAISING)) {
return new LeapCommand(Request.zoneCommand(zone, CommandType.RAISE));
} else if (action.equals(OutputCommand.ACTION_STARTLOWERING)) {
return new LeapCommand(Request.zoneCommand(zone, CommandType.LOWER));
} else if (action.equals(OutputCommand.ACTION_STOP)) {
return new LeapCommand(Request.zoneCommand(zone, CommandType.STOP));
} else {
logger.debug("Ignoring unsupported shade action");
return null;
}
} else {
logger.debug("Ignoring unsupported target type: {}", targetType);
return null;
}
} else {
logger.debug("Ignoring unsupported operation: {}", operation);
return null;
}
}
@Override
public String toString() {
return lipCommand();
}
}

View File

@@ -0,0 +1,71 @@
/**
* 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.lutron.internal.protocol;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.lutron.internal.handler.LeapBridgeHandler;
import org.openhab.binding.lutron.internal.protocol.leap.LeapCommand;
import org.openhab.binding.lutron.internal.protocol.lip.LutronCommandType;
import org.openhab.binding.lutron.internal.protocol.lip.LutronOperation;
import org.openhab.binding.lutron.internal.protocol.lip.TargetType;
/**
* Lutron SYSVAR command object
*
* @author Bob Adair - Initial contribution
*/
@NonNullByDefault
public class SysvarCommand extends LutronCommandNew {
public static final Integer ACTION_GETSETSYSVAR = 1;
private final Integer action;
private final @Nullable Object parameter;
/**
* SysvarCommand constructor
*
* @param targetType
* @param operation
* @param integrationId
* @param action
* @param parameter
*/
public SysvarCommand(LutronOperation operation, Integer integrationId, Integer action, @Nullable Object parameter) {
super(TargetType.SYSVAR, operation, LutronCommandType.SYSVAR, integrationId);
this.action = action;
this.parameter = parameter;
}
@Override
public String lipCommand() {
StringBuilder builder = new StringBuilder().append(operation).append(commandType);
builder.append(',').append(integrationId);
builder.append(',').append(action);
if (parameter != null) {
builder.append(',').append(parameter);
}
return builder.toString();
}
@Override
public @Nullable LeapCommand leapCommand(LeapBridgeHandler bridgeHandler, @Nullable Integer leapZone) {
return null; // No equivalent LEAP command
}
@Override
public String toString() {
return lipCommand();
}
}

View File

@@ -0,0 +1,85 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.lutron.internal.protocol;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.lutron.internal.handler.LeapBridgeHandler;
import org.openhab.binding.lutron.internal.protocol.leap.LeapCommand;
import org.openhab.binding.lutron.internal.protocol.lip.LutronCommandType;
import org.openhab.binding.lutron.internal.protocol.lip.LutronOperation;
import org.openhab.binding.lutron.internal.protocol.lip.TargetType;
/**
* Lutron TIMECLOCK command object
*
* @author Bob Adair - Initial contribution
*/
@NonNullByDefault
public class TimeclockCommand extends LutronCommandNew {
public static final Integer ACTION_CLOCKMODE = 1;
public static final Integer ACTION_SUNRISE = 2;
public static final Integer ACTION_SUNSET = 3;
public static final Integer ACTION_EXECEVENT = 5;
public static final Integer ACTION_SETEVENT = 6;
public static final Integer EVENT_ENABLE = 1;
public static final Integer EVENT_DISABLE = 2;
private final Integer action;
private final @Nullable Object parameter;
private final @Nullable Boolean enable;
/**
* TimeclockCommand constructor
*
* @param targetType
* @param operation
* @param integrationId
* @param action
* @param parameter
* @param enable true = enable, false = disable
*/
public TimeclockCommand(LutronOperation operation, Integer integrationId, Integer action,
@Nullable Object parameter, @Nullable Boolean enable) {
super(TargetType.TIMECLOCK, operation, LutronCommandType.TIMECLOCK, integrationId);
this.action = action;
this.parameter = parameter;
this.enable = enable;
}
@Override
public String lipCommand() {
StringBuilder builder = new StringBuilder().append(operation).append(commandType);
builder.append(',').append(integrationId);
builder.append(',').append(action);
if (parameter != null) {
builder.append(',').append(parameter);
}
if (enable != null) {
builder.append(',');
builder.append((enable) ? '1' : '2');
}
return builder.toString();
}
@Override
public @Nullable LeapCommand leapCommand(LeapBridgeHandler bridgeHandler, @Nullable Integer leapZone) {
return null;
}
@Override
public String toString() {
return lipCommand();
}
}

View File

@@ -0,0 +1,50 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.lutron.internal.protocol.leap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
*
* Abstract base class for LEAP message body objects
*
* @author Bob Adair - Initial contribution
*/
@NonNullByDefault
public abstract class AbstractMessageBody {
/**
* Utility method to extract the int from a href String using the supplied Pattern.
*
* @return the int or 0 if unable to extract
*/
protected static int hrefNumber(Pattern pattern, @Nullable String href) {
if (href == null) {
return 0;
}
Matcher matcher = pattern.matcher(href);
if (matcher.find()) {
try {
return Integer.parseInt(matcher.group(1));
} catch (NumberFormatException e) {
return 0;
}
} else {
return 0;
}
}
}

View File

@@ -0,0 +1,59 @@
/**
* 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.lutron.internal.protocol.leap;
import org.eclipse.jdt.annotation.NonNullByDefault;
import com.google.gson.annotations.SerializedName;
/**
* LEAP CommandType enum
*
* @author Bob Adair - Initial contribution
*/
@NonNullByDefault
public enum CommandType {
@SerializedName("GoToDimmedLevel")
GOTODIMMEDLEVEL("GoToDimmedLevel"),
@SerializedName("GoToFanSpeed")
GOTOFANSPEED("GoToFanSpeed"),
@SerializedName("GoToLevel")
GOTOLEVEL("GoToLevel"),
@SerializedName("PressAndHold")
PRESSANDHOLD("PressAndHold"),
@SerializedName("PressAndRelease")
PRESSANDRELEASE("PressAndRelease"),
@SerializedName("Release")
RELEASE("Release"),
@SerializedName("ShadeLimitLower")
SHADELIMITLOWER("ShadeLimitLower"),
@SerializedName("ShadeLimitRaise")
SHADELIMITRAISE("ShadeLimitRaise"),
@SerializedName("Raise")
RAISE("Raise"),
@SerializedName("Lower")
LOWER("Lower"),
@SerializedName("Stop")
STOP("Stop");
private final transient String string;
CommandType(String string) {
this.string = string;
}
@Override
public String toString() {
return string;
}
}

View File

@@ -0,0 +1,70 @@
/**
* 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.lutron.internal.protocol.leap;
import org.eclipse.jdt.annotation.NonNullByDefault;
import com.google.gson.annotations.SerializedName;
/**
* LEAP CommuniqueType enum
*
* @author Bob Adair - Initial contribution
*/
@NonNullByDefault
public enum CommuniqueType {
// Requests
@SerializedName("CreateRequest")
CREATEREQUEST("CreateRequest"),
@SerializedName("ReadRequest")
READREQUEST("ReadRequest"),
@SerializedName("UpdateRequest")
UPDATEREQUEST("UpdateRequest"),
@SerializedName("DeleteRequest")
DELETEREQUEST("DeleteRequest"), // ?
@SerializedName("SubscribeRequest")
SUBSCRIBEREQUEST("SubscribeRequest"),
@SerializedName("UnubscribeRequest")
UNSUBSCRIBEREQUEST("UnubscribeRequest"),
@SerializedName("Execute")
EXECUTEREQUEST("Execute"),
// Responses
@SerializedName("CreateResponse")
CREATERESPONSE("CreateResponse"),
@SerializedName("ReadResponse")
READRESPONSE("ReadResponse"),
@SerializedName("UpdateResponse")
UPDATERESPONSE("UpdateResponse"),
@SerializedName("DeleteResponse")
DELETERESPONSE("DeleteResponse"), // ?
@SerializedName("SubscribeResponse")
SUBSCRIBERESPONSE("SubscribeResponse"),
@SerializedName("UnsubscribeResponse")
UNSUBSCRIBERESPONSE("UnsubscribeResponse"),
@SerializedName("ExecuteResponse")
EXECUTERESPONSE("ExecuteResponse"), // ?
@SerializedName("ExceptionResponse")
EXCEPTIONRESPONSE("ExceptionResponse");
private final transient String string;
CommuniqueType(String string) {
this.string = string;
}
@Override
public String toString() {
return string;
}
}

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.lutron.internal.protocol.leap;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* LeapCommand represents a LEAP protocol command
*
* @author Bob Adair - Initial contribution
*/
@NonNullByDefault
public class LeapCommand {
private String command;
public LeapCommand(String command) {
this.command = command;
}
@Override
public String toString() {
return command;
}
}

View File

@@ -0,0 +1,310 @@
/**
* 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.lutron.internal.protocol.leap;
import java.util.LinkedList;
import java.util.List;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.lutron.internal.protocol.leap.dto.Area;
import org.openhab.binding.lutron.internal.protocol.leap.dto.ButtonGroup;
import org.openhab.binding.lutron.internal.protocol.leap.dto.Device;
import org.openhab.binding.lutron.internal.protocol.leap.dto.ExceptionDetail;
import org.openhab.binding.lutron.internal.protocol.leap.dto.Header;
import org.openhab.binding.lutron.internal.protocol.leap.dto.OccupancyGroup;
import org.openhab.binding.lutron.internal.protocol.leap.dto.OccupancyGroupStatus;
import org.openhab.binding.lutron.internal.protocol.leap.dto.ZoneStatus;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParseException;
import com.google.gson.JsonParser;
import com.google.gson.JsonSyntaxException;
/**
* Class responsible for parsing incoming LEAP messages
*
* @author Bob Adair - Initial contribution
*/
@NonNullByDefault
public class LeapMessageParser {
private final Logger logger = LoggerFactory.getLogger(LeapMessageParser.class);
private final Gson gson;
private final LeapMessageParserCallbacks callback;
/**
* LeapMessageParser Constructor
*
* @param callback Object implementing the LeapMessageParserCallbacks interface
*/
public LeapMessageParser(LeapMessageParserCallbacks callback) {
gson = new GsonBuilder().create();
this.callback = callback;
}
/**
* Parse and process a LEAP protocol message
*
* @param msg String containing the LEAP message
*/
public void handleMessage(String msg) {
if (msg.trim().equals("")) {
return; // Ignore empty lines
}
logger.trace("Received message: {}", msg);
try {
JsonObject message = (JsonObject) new JsonParser().parse(msg);
if (!message.has("CommuniqueType")) {
logger.debug("No CommuniqueType found in message: {}", msg);
return;
}
String communiqueType = message.get("CommuniqueType").getAsString();
// CommuniqueType type = CommuniqueType.valueOf(communiqueType);
logger.debug("Received CommuniqueType: {}", communiqueType);
callback.validMessageReceived(communiqueType);
switch (communiqueType) {
case "CreateResponse":
return;
case "ReadResponse":
handleReadResponseMessage(message);
break;
case "UpdateResponse":
break;
case "SubscribeResponse":
// Subscribe responses can contain bodies with data
handleReadResponseMessage(message);
return;
case "UnsubscribeResponse":
return;
case "ExceptionResponse":
handleExceptionResponse(message);
return;
default:
logger.debug("Unknown CommuniqueType received: {}", communiqueType);
break;
}
} catch (JsonParseException e) {
logger.debug("Error parsing message: {}", e.getMessage());
return;
}
}
/**
* Method called by handleMessage() to handle all LEAP ExceptionResponse messages.
*
* @param message LEAP message
*/
private void handleExceptionResponse(JsonObject message) {
String detailMessage = "";
try {
JsonObject header = message.get("Header").getAsJsonObject();
Header headerObj = gson.fromJson(header, Header.class);
if (MessageBodyType.ExceptionDetail.toString().equalsIgnoreCase(headerObj.messageBodyType)
&& message.has("Body")) {
JsonObject body = message.get("Body").getAsJsonObject();
ExceptionDetail exceptionDetail = gson.fromJson(body, ExceptionDetail.class);
if (exceptionDetail != null) {
detailMessage = exceptionDetail.message;
}
}
logger.debug("Exception response received. Status: {} URL: {} Message: {}", headerObj.statusCode,
headerObj.url, detailMessage);
} catch (JsonParseException | IllegalStateException e) {
logger.debug("Exception response received. Error parsing exception message: {}", e.getMessage());
return;
}
}
/**
* Method called by handleMessage() to handle all LEAP ReadResponse and SubscribeResponse messages.
*
* @param message LEAP message
*/
private void handleReadResponseMessage(JsonObject message) {
try {
JsonObject header = message.get("Header").getAsJsonObject();
Header headerObj = gson.fromJson(header, Header.class);
// if 204/NoContent response received for buttongroup request, create empty button map
if (Request.BUTTON_GROUP_URL.equals(headerObj.url)
&& Header.STATUS_NO_CONTENT.equalsIgnoreCase(headerObj.statusCode)) {
callback.handleEmptyButtonGroupDefinition();
return;
}
if (!header.has("MessageBodyType")) {
logger.trace("No MessageBodyType in header");
return;
}
String messageBodyType = header.get("MessageBodyType").getAsString();
logger.trace("MessageBodyType: {}", messageBodyType);
if (!message.has("Body")) {
logger.debug("No Body found in message");
return;
}
JsonObject body = message.get("Body").getAsJsonObject();
switch (messageBodyType) {
case "OnePingResponse":
parseOnePingResponse(body);
break;
case "OneZoneStatus":
parseOneZoneStatus(body);
break;
case "MultipleAreaDefinition":
parseMultipleAreaDefinition(body);
break;
case "MultipleButtonGroupDefinition":
parseMultipleButtonGroupDefinition(body);
break;
case "MultipleDeviceDefinition":
parseMultipleDeviceDefinition(body);
break;
case "MultipleOccupancyGroupDefinition":
parseMultipleOccupancyGroupDefinition(body);
break;
case "MultipleOccupancyGroupStatus":
parseMultipleOccupancyGroupStatus(body);
break;
case "MultipleVirtualButtonDefinition":
break;
default:
logger.debug("Unknown MessageBodyType received: {}", messageBodyType);
break;
}
} catch (JsonParseException | IllegalStateException e) {
logger.debug("Error parsing message: {}", e.getMessage());
return;
}
}
private @Nullable <T extends AbstractMessageBody> T parseBodySingle(JsonObject messageBody, String memberName,
Class<T> type) {
try {
if (messageBody.has(memberName)) {
JsonObject jsonObject = messageBody.get(memberName).getAsJsonObject();
T obj = gson.fromJson(jsonObject, type);
return obj;
} else {
logger.debug("Member name {} not found in JSON message", memberName);
return null;
}
} catch (IllegalStateException | JsonSyntaxException e) {
logger.debug("Error parsing JSON message: {}", e.getMessage());
return null;
}
}
private <T extends AbstractMessageBody> List<T> parseBodyMultiple(JsonObject messageBody, String memberName,
Class<T> type) {
List<T> objList = new LinkedList<T>();
try {
if (messageBody.has(memberName)) {
JsonArray jsonArray = messageBody.get(memberName).getAsJsonArray();
for (JsonElement element : jsonArray) {
JsonObject jsonObject = element.getAsJsonObject();
T obj = gson.fromJson(jsonObject, type);
objList.add(obj);
}
return objList;
} else {
logger.debug("Member name {} not found in JSON message", memberName);
return objList;
}
} catch (IllegalStateException | JsonSyntaxException e) {
logger.debug("Error parsing JSON message: {}", e.getMessage());
return objList;
}
}
private void parseOnePingResponse(JsonObject messageBody) {
logger.debug("Ping response received");
}
/**
* Parses a OneZoneStatus message body. Calls handleZoneUpdate() to dispatch zone updates.
*/
private void parseOneZoneStatus(JsonObject messageBody) {
ZoneStatus zoneStatus = parseBodySingle(messageBody, "ZoneStatus", ZoneStatus.class);
if (zoneStatus != null) {
callback.handleZoneUpdate(zoneStatus);
}
}
/**
* Parses a MultipleAreaDefinition message body.
*/
private void parseMultipleAreaDefinition(JsonObject messageBody) {
logger.trace("Parsing area list");
List<Area> areaList = parseBodyMultiple(messageBody, "Areas", Area.class);
callback.handleMultipleAreaDefinition(areaList);
}
/**
* Parses a MultipleOccupancyGroupDefinition message body.
*/
private void parseMultipleOccupancyGroupDefinition(JsonObject messageBody) {
logger.trace("Parsing occupancy group list");
List<OccupancyGroup> oGroupList = parseBodyMultiple(messageBody, "OccupancyGroups", OccupancyGroup.class);
callback.handleMultipleOccupancyGroupDefinition(oGroupList);
}
/**
* Parses a MultipleOccupancyGroupStatus message body and updates occupancy status.
*/
private void parseMultipleOccupancyGroupStatus(JsonObject messageBody) {
logger.trace("Parsing occupancy group status list");
List<OccupancyGroupStatus> statusList = parseBodyMultiple(messageBody, "OccupancyGroupStatuses",
OccupancyGroupStatus.class);
for (OccupancyGroupStatus status : statusList) {
int groupNumber = status.getOccupancyGroup();
if (groupNumber > 0) {
logger.debug("OccupancyGroup: {} Status: {}", groupNumber, status.occupancyStatus);
callback.handleGroupUpdate(groupNumber, status.occupancyStatus);
}
}
}
/**
* Parses a MultipleDeviceDefinition message body and loads the zoneToDevice and deviceToZone maps. Also passes the
* device data on to the discovery service and calls setBridgeProperties() with the hub's device entry.
*/
private void parseMultipleDeviceDefinition(JsonObject messageBody) {
List<Device> deviceList = parseBodyMultiple(messageBody, "Devices", Device.class);
callback.handleMultipleDeviceDefintion(deviceList);
}
/**
* Parse a MultipleButtonGroupDefinition message body and load the results into deviceButtonMap.
*/
private void parseMultipleButtonGroupDefinition(JsonObject messageBody) {
List<ButtonGroup> buttonGroupList = parseBodyMultiple(messageBody, "ButtonGroups", ButtonGroup.class);
callback.handleMultipleButtonGroupDefinition(buttonGroupList);
}
}

View File

@@ -0,0 +1,47 @@
/**
* 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.lutron.internal.protocol.leap;
import java.util.List;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.lutron.internal.protocol.leap.dto.Area;
import org.openhab.binding.lutron.internal.protocol.leap.dto.ButtonGroup;
import org.openhab.binding.lutron.internal.protocol.leap.dto.Device;
import org.openhab.binding.lutron.internal.protocol.leap.dto.OccupancyGroup;
import org.openhab.binding.lutron.internal.protocol.leap.dto.ZoneStatus;
/**
* Interface defining callback routines used by LeapMessageParser
*
* @author Bob Adair - Initial contribution
*/
@NonNullByDefault
public interface LeapMessageParserCallbacks {
public void validMessageReceived(String communiqueType);
public void handleEmptyButtonGroupDefinition();
public void handleZoneUpdate(ZoneStatus zoneStatus);
public void handleGroupUpdate(int groupNumber, String occupancyStatus);
public void handleMultipleButtonGroupDefinition(List<ButtonGroup> buttonGroupList);
public void handleMultipleDeviceDefintion(List<Device> deviceList);
public void handleMultipleAreaDefinition(List<Area> areaList);
public void handleMultipleOccupancyGroupDefinition(List<OccupancyGroup> oGroupList);
}

View File

@@ -0,0 +1,79 @@
/**
* 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.lutron.internal.protocol.leap;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* LEAP MessageBodyType enum
*
* @author Bob Adair - Initial contribution
*/
@NonNullByDefault
public enum MessageBodyType {
ExceptionDetail("ExceptionDetail"),
MultipleAffectedZoneDefinition("MultipleAffectedZoneDefinition"),
MultipleAreaDefinition("MultipleAreaDefinition"),
MultipleButtonDefinition("MultipleButtonDefinition"),
MultipleButtonGroupDefinition("MultipleButtonGroupDefinition"),
MultipleDeviceDefinition("MultipleDeviceDefinition"),
MultipleDeviceStatus("MultipleDeviceStatus"),
MultipleOccupancyGroupDefinition("MultipleOccupancyGroupDefinition"),
MultipleOccupancyGroupStatus("MultipleOccupancyGroupStatus"),
MultiplePresetAssignmentDefinition("MultiplePresetAssignmentDefinition"),
MultipleProgrammingModelDefinition("MultipleProgrammingModelDefinition"),
MultipleServerDefinition("MultipleServerDefinition"),
MultipleServiceDefinition("MultipleServiceDefinition"),
MultipleTimeclockDefinition("MultipleTimeclockDefinition"),
MultipleVirtualButtonDefinition("MultipleVirtualButtonDefinition"),
MultipleZoneDefinition("MultipleZoneDefinition"),
MultipleZoneStatus("MultipleZoneStatus"),
OneAffectedZoneDefinition("OneAffectedZoneDefinition"),
OneAlexaDataSummaryDefinition("OneAlexaDataSummaryDefinition"),
OneAreaDefinition("OneAreaDefinition"),
OneAreaLoadSheddingDefinition("OneAreaLoadSheddingDefinition"),
OneButtonDefinition("OneButtonDefinition"),
OneButtonGroupDefinition("OneButtonGroupDefinition"),
OneDeviceDefinition("OneDeviceDefinition"),
OneDeviceRulesDefinition("OneDeviceRulesDefinition"),
OneDeviceStatus("OneDeviceStatus"),
OneGoogleHomeDataSummaryDefinition("OneGoogleHomeDataSummaryDefinition"),
OneLinkNodeDefinition("OneLinkNodeDefinition"),
OneLIPIdListDefinition("OneLIPIdListDefinition"),
OneNetworkInterfaceDefinition("OneNetworkInterfaceDefinition"),
OneOccupancyGroupDefinition("OneOccupancyGroupDefinition"),
OnePairingListDefinition("OnePairingListDefinition"),
OnePingResponse("OnePingResponse"),
OnePresetAssignmentDefinition("OnePresetAssignmentDefinition"),
OnePresetDefinition("OnePresetDefinition"),
OneProgrammingModelDefinition("OneProgrammingModelDefinition"),
OneProjectDefinition("OneProjectDefinition"),
OneServerDefinition("OneServerDefinition"),
OneServiceDefinition("OneServiceDefinition"),
OneSystemDefinition("OneSystemDefinition"),
OneTimeclockEventRulesDefinition("OneTimeclockEventRulesDefinition"),
OneVirtualButtonDefinition("OneVirtualButtonDefinition"),
OneZoneDefinition("OneZoneDefinition"),
OneZoneStatus("OneZoneStatus");
private final transient String string;
MessageBodyType(String string) {
this.string = string;
}
@Override
public String toString() {
return string;
}
}

View File

@@ -0,0 +1,121 @@
/**
* 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.lutron.internal.protocol.leap;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.lutron.internal.protocol.FanSpeedType;
/**
* Contains static methods for constructing LEAP messages
*
* @author Bob Adair - Initial contribution
*/
@NonNullByDefault
public class Request {
public static final String BUTTON_GROUP_URL = "/buttongroup";
public static String goToLevel(int zone, int value) {
String request = "{\"CommuniqueType\": \"CreateRequest\","
+ "\"Header\": {\"Url\": \"/zone/%d/commandprocessor\"}," + "\"Body\": {" + "\"Command\": {"
+ "\"CommandType\": \"GoToLevel\"," + "\"Parameter\": [{\"Type\": \"Level\", \"Value\": %d}]}}}";
return String.format(request, zone, value);
}
/** fadeTime must be in the format hh:mm:ss **/
public static String goToDimmedLevel(int zone, int value, String fadeTime) {
String request = "{\"CommuniqueType\": \"CreateRequest\","
+ "\"Header\": {\"Url\": \"/zone/%d/commandprocessor\"},\"Body\": {\"Command\": {"
+ "\"CommandType\": \"GoToDimmedLevel\","
+ "\"DimmedLevelParameters\": {\"Level\": %d, \"FadeTime\": \"%s\"}}}}";
return String.format(request, zone, value, fadeTime);
}
/** fadeTime and delayTime must be in the format hh:mm:ss **/
public static String goToDimmedLevel(int zone, int value, String fadeTime, String delayTime) {
String request = "{\"CommuniqueType\": \"CreateRequest\","
+ "\"Header\": {\"Url\": \"/zone/%d/commandprocessor\"},\"Body\": {\"Command\": {"
+ "\"CommandType\": \"GoToDimmedLevel\","
+ "\"DimmedLevelParameters\": {\"Level\": %d, \"FadeTime\": \"%s\", \"DelayTime\": \"%s\"}}}}";
return String.format(request, zone, value, fadeTime, delayTime);
}
public static String goToFanSpeed(int zone, FanSpeedType fanSpeed) {
String request = "{\"CommuniqueType\": \"CreateRequest\","
+ "\"Header\": {\"Url\": \"/zone/%d/commandprocessor\"}," + "\"Body\": {"
+ "\"Command\": {\"CommandType\": \"GoToFanSpeed\","
+ "\"FanSpeedParameters\": {\"FanSpeed\": \"%s\"}}}}";
return String.format(request, zone, fanSpeed.leapValue());
}
public static String buttonCommand(int button, CommandType command) {
String request = "{\"CommuniqueType\": \"CreateRequest\","
+ "\"Header\": {\"Url\": \"/button/%d/commandprocessor\"},"
+ "\"Body\": {\"Command\": {\"CommandType\": \"%s\"}}}";
return String.format(request, button, command.toString());
}
public static String virtualButtonCommand(int virtualbutton, CommandType command) {
String request = "{\"CommuniqueType\": \"CreateRequest\","
+ "\"Header\": {\"Url\": \"/virtualbutton/%d/commandprocessor\"},"
+ "\"Body\": {\"Command\": {\"CommandType\": \"%s\"}}}";
return String.format(request, virtualbutton, command.toString());
}
public static String zoneCommand(int zone, CommandType commandType) {
String request = "{\"CommuniqueType\": \"CreateRequest\","
+ "\"Header\": {\"Url\": \"/zone/%d/commandprocessor\"}," + "\"Body\": {" + "\"Command\": {"
+ "\"CommandType\": \"%s\"}}}";
return String.format(request, zone, commandType.toString());
}
public static String request(CommuniqueType cType, String url) {
String request = "{\"CommuniqueType\": \"%s\",\"Header\": {\"Url\": \"%s\"}}";
return String.format(request, cType.toString(), url);
}
public static String ping() {
return request(CommuniqueType.READREQUEST, "/server/1/status/ping");
}
public static String getDevices() {
return request(CommuniqueType.READREQUEST, "/device");
}
public static String getVirtualButtons() {
return request(CommuniqueType.READREQUEST, "/virtualbutton");
}
public static String getButtonGroups() {
return request(CommuniqueType.READREQUEST, BUTTON_GROUP_URL);
}
public static String getAreas() {
return request(CommuniqueType.READREQUEST, "/area");
}
public static String getOccupancyGroups() {
return request(CommuniqueType.READREQUEST, "/occupancygroup");
}
public static String getZoneStatus(int zone) {
return request(CommuniqueType.READREQUEST, String.format("/zone/%d/status", zone));
}
public static String getOccupancyGroupStatus() {
return request(CommuniqueType.READREQUEST, "/occupancygroup/status");
}
public static String subscribeOccupancyGroupStatus() {
return request(CommuniqueType.SUBSCRIBEREQUEST, "/occupancygroup/status");
}
}

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.lutron.internal.protocol.leap.dto;
import com.google.gson.annotations.SerializedName;
/**
* LEAP AffectedZone Object
*
* @author Bob Adair - Initial contribution
*/
public class AffectedZone {
@SerializedName("href")
public String href;
@SerializedName("Zone")
public Href zone = new Href();
public AffectedZone() {
}
}

View File

@@ -0,0 +1,65 @@
/**
* 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.lutron.internal.protocol.leap.dto;
import java.util.regex.Pattern;
import org.openhab.binding.lutron.internal.protocol.leap.AbstractMessageBody;
import com.google.gson.annotations.SerializedName;
/**
* LEAP Area object
*
* @author Bob Adair - Initial contribution
*/
public class Area extends AbstractMessageBody {
public static final Pattern AREA_HREF_PATTERN = Pattern.compile("/area/([0-9]+)");
@SerializedName("href")
public String href;
@SerializedName("Name")
public String name;
@SerializedName("Parent")
public Href parent;
// @SerializedName("Category")
// public Category category;
@SerializedName("AssociatedDevices")
public Href[] associatedDevices;
@SerializedName("AssociatedOccupancyGroups")
public Href[] associatedOccupancyGroups;
@SerializedName("LoadShedding")
public Href loadShedding;
@SerializedName("OccupancySettings")
public Href occupancySettings;
@SerializedName("OccupancySensorSettings")
public Href occupancySensorSettings;
@SerializedName("DaylightingGainSettings")
public Href daylightingGainSettings;
public Area() {
}
public int getArea() {
return hrefNumber(AREA_HREF_PATTERN, href);
}
}

View File

@@ -0,0 +1,70 @@
/**
* 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.lutron.internal.protocol.leap.dto;
import java.util.LinkedList;
import java.util.List;
import java.util.regex.Pattern;
import org.openhab.binding.lutron.internal.protocol.leap.AbstractMessageBody;
import com.google.gson.annotations.SerializedName;
/**
* LEAP ButtonGroup Object
*
* @author Bob Adair - Initial contribution
*/
public class ButtonGroup extends AbstractMessageBody {
public static final Pattern BUTTONGROUP_HREF_PATTERN = Pattern.compile("/buttongroup/([0-9]+)");
public static final Pattern BUTTON_HREF_PATTERN = Pattern.compile("/button/([0-9]+)");
@SerializedName("href")
public String href;
@SerializedName("Parent") // device href
public Href parent = new Href();
@SerializedName("Buttons")
public Href[] buttons;
@SerializedName("AffectedZones")
public AffectedZone[] affectedZones;
@SerializedName("SortOrder")
public Integer sortOrder;
@SerializedName("StopIfMoving")
public String stopIfMoving; // Enabled or Disabled
@SerializedName("ProgrammingType")
public String programmingType; // Column
public ButtonGroup() {
}
public int getButtonGroup() {
return hrefNumber(BUTTONGROUP_HREF_PATTERN, href);
}
public int getParentDevice() {
if (parent != null && parent.href != null) {
return hrefNumber(Device.DEVICE_HREF_PATTERN, parent.href);
} else {
return 0;
}
}
public List<Integer> getButtonList() {
LinkedList<Integer> buttonNumList = new LinkedList<>();
for (Href button : buttons) {
int buttonNum = hrefNumber(BUTTON_HREF_PATTERN, button.href);
buttonNumList.add(buttonNum);
}
return buttonNumList;
}
}

View File

@@ -0,0 +1,132 @@
/**
* 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.lutron.internal.protocol.leap.dto;
import java.util.regex.Pattern;
import org.openhab.binding.lutron.internal.protocol.leap.AbstractMessageBody;
import com.google.gson.annotations.SerializedName;
/**
* LEAP Device Object
*
* @author Bob Adair - Initial contribution
*/
public class Device extends AbstractMessageBody {
public static final Pattern DEVICE_HREF_PATTERN = Pattern.compile("/device/([0-9]+)");
private static final Pattern ZONE_HREF_PATTERN = Pattern.compile("/zone/([0-9]+)");
@SerializedName("href")
public String href;
@SerializedName("Name")
public String name;
@SerializedName("FullyQualifiedName")
public String[] fullyQualifiedName;
@SerializedName("Parent")
public Href parent = new Href();
@SerializedName("SerialNumber")
public String serialNumber;
@SerializedName("ModelNumber")
public String modelNumber;
@SerializedName("DeviceType")
public String deviceType;
@SerializedName("LocalZones")
public Href[] localZones;
@SerializedName("AssociatedArea")
public Href associatedArea = new Href();
@SerializedName("OccupancySensors")
public Href[] occupancySensors;
@SerializedName("LinkNodes")
public Href[] linkNodes;
@SerializedName("DeviceRules")
public Href[] deviceRules;
@SerializedName("RepeaterProperties")
public RepeaterProperties repeaterProperties;
@SerializedName("FirmwareImage")
public FirmwareImage firmwareImage;
public class FirmwareImage {
@SerializedName("Firmware")
public Firmware firmware;
@SerializedName("Installed")
public Installed installed;
}
public class Firmware {
@SerializedName("DisplayName")
public String displayName;
}
public class Installed {
@SerializedName("Year")
public int year;
@SerializedName("Month")
public int month;
@SerializedName("Day")
public int day;
@SerializedName("Hour")
public int hour;
@SerializedName("Minute")
public int minute;
@SerializedName("Second")
public int second;
@SerializedName("Utc")
public String utc;
}
public class RepeaterProperties {
@SerializedName("IsRepeater")
public boolean isRepeater;
}
public Device() {
}
public int getDevice() {
return hrefNumber(DEVICE_HREF_PATTERN, href);
}
/**
* Returns the zone number of the first zone listed in LocalZones.
* Currently devices should only have one zone listed.
*/
public int getZone() {
if (localZones != null && localZones.length > 0) {
return hrefNumber(ZONE_HREF_PATTERN, localZones[0].href);
} else {
return 0;
}
}
public String getFullyQualifiedName() {
if (fullyQualifiedName != null && fullyQualifiedName.length > 0) {
return String.join(" ", fullyQualifiedName);
} else {
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.lutron.internal.protocol.leap.dto;
import org.openhab.binding.lutron.internal.protocol.leap.AbstractMessageBody;
import com.google.gson.annotations.SerializedName;
/**
* LEAP ExceptionDetail object
*
* @author Bob Adair - Initial contribution
*/
public class ExceptionDetail extends AbstractMessageBody {
@SerializedName("Message")
public String message = "";
public ExceptionDetail() {
}
}

View File

@@ -0,0 +1,35 @@
/**
* 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.lutron.internal.protocol.leap.dto;
import com.google.gson.annotations.SerializedName;
/**
* LEAP message header
*
* @author Bob Adair - Initial contribution
*/
public class Header {
public static final String STATUS_NO_CONTENT = "204 NoContent";
public static final String STATUS_OK = "200 OK";
@SerializedName("MessageBodyType")
public String messageBodyType;
@SerializedName("StatusCode")
public String statusCode;
@SerializedName("Url")
public String url;
public Header() {
}
}

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.lutron.internal.protocol.leap.dto;
import com.google.gson.annotations.SerializedName;
/**
* @author Bob Adair - Initial contribution
*/
public class Href {
@SerializedName("href")
public String href = "";
public Href() {
}
public Href(String href) {
this.href = href;
}
}

View File

@@ -0,0 +1,59 @@
/**
* 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.lutron.internal.protocol.leap.dto;
import java.util.regex.Pattern;
import org.openhab.binding.lutron.internal.protocol.leap.AbstractMessageBody;
import com.google.gson.annotations.SerializedName;
/**
* LEAP OccupancyGroup object
*
* @author Bob Adair - Initial contribution
*/
public class OccupancyGroup extends AbstractMessageBody {
public static final Pattern OGROUP_HREF_PATTERN = Pattern.compile("/occupancygroup/([0-9]+)");
@SerializedName("href")
public String href;
@SerializedName("AssociatedSensors")
public OccupancySensor[] associatedSensors;
@SerializedName("AssociatedAreas")
public AreaHref[] associatedAreas;
@SerializedName("ProgrammingType")
public String programmingType;
@SerializedName("ProgrammingModel")
public Href programmingModel;
public class AreaHref {
@SerializedName("Area")
public Href area;
public int getAreaNumber() {
return hrefNumber(Area.AREA_HREF_PATTERN, area.href);
}
}
public OccupancyGroup() {
}
public int getOccupancyGroup() {
return hrefNumber(OGROUP_HREF_PATTERN, href);
}
}

View File

@@ -0,0 +1,48 @@
/**
* 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.lutron.internal.protocol.leap.dto;
import java.util.regex.Pattern;
import org.openhab.binding.lutron.internal.protocol.leap.AbstractMessageBody;
import com.google.gson.annotations.SerializedName;
/**
* LEAP OccupancyGroupStatus object
*
* @author Bob Adair - Initial contribution
*/
public class OccupancyGroupStatus extends AbstractMessageBody {
public static final Pattern OGROUP_HREF_PATTERN = Pattern.compile("/occupancygroup/([0-9]+)");
@SerializedName("href")
public String href;
@SerializedName("OccupancyGroup")
public Href occupancyGroup;
@SerializedName("OccupancyStatus")
public String occupancyStatus; // Occupied, Unoccupied, or Unknown
public OccupancyGroupStatus() {
}
public int getOccupancyGroup() {
if (occupancyGroup != null && occupancyGroup.href != null) {
return hrefNumber(OGROUP_HREF_PATTERN, occupancyGroup.href);
} else {
return 0;
}
}
}

View File

@@ -0,0 +1,23 @@
/**
* 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.lutron.internal.protocol.leap.dto;
import com.google.gson.annotations.SerializedName;
/**
* @author Bob Adair - Initial contribution
*/
public class OccupancySensor {
@SerializedName("OccupancySensor")
public Href occupancySensor;
}

View File

@@ -0,0 +1,61 @@
/**
* 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.lutron.internal.protocol.leap.dto;
import java.util.regex.Pattern;
import org.openhab.binding.lutron.internal.protocol.FanSpeedType;
import org.openhab.binding.lutron.internal.protocol.leap.AbstractMessageBody;
import com.google.gson.annotations.SerializedName;
/**
* LEAP ZoneStatus Object
*
* @author Bob Adair - Initial contribution
*/
public class ZoneStatus extends AbstractMessageBody {
public static final Pattern ZONE_HREF_PATTERN = Pattern.compile("/zone/([0-9]+)");
@SerializedName("href")
public String href = "";
@SerializedName("Level")
public int level; // 0-100
@SerializedName("SwitchedLevel")
public String switchedLevel = ""; // "On" or "Off"
@SerializedName("FanSpeed")
public FanSpeedType fanSpeed;
@SerializedName("Zone")
public Href zone = new Href();;
@SerializedName("StatusAccuracy")
public String statusAccuracy = ""; // "Good" or ??
public ZoneStatus() {
}
public int getZone() {
if (zone != null) {
return hrefNumber(ZONE_HREF_PATTERN, zone.href);
} else {
return 0;
}
}
public boolean statusAccuracyGood() {
return "Good".equals(statusAccuracy);
}
public boolean switchedLevelOn() {
return "On".equals(switchedLevel);
}
}

View File

@@ -10,7 +10,7 @@
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.lutron.internal.protocol;
package org.openhab.binding.lutron.internal.protocol.lip;
/**
* Type of command in the Lutron integration protocol.

View File

@@ -10,17 +10,18 @@
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.lutron.internal.protocol;
package org.openhab.binding.lutron.internal.protocol.lip;
/**
* Requested operation of a command to the Lutron integration protocol.
* Requested operation of a command in the Lutron integration protocol.
*
* @author Allan Tong - Initial contribution
*
*/
public enum LutronOperation {
EXECUTE("#"),
QUERY("?");
QUERY("?"),
RESPONSE("~");
private final String operationChar;

View File

@@ -0,0 +1,37 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.lutron.internal.protocol.lip;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Target device type enum. Used to annotate LutronCommand objects so the LEAP bridge can translate them.
*
* @author Bob Adair - Initial contribution
*/
@NonNullByDefault
public enum TargetType {
BLIND,
BRIDGE,
CCO,
DIMMER,
FAN,
GREENMODE,
GROUP,
KEYPAD,
SHADE,
SWITCH,
SYSVAR,
TIMECLOCK,
VIRTUALKEYPAD;
}

View File

@@ -74,6 +74,8 @@ public class DbXmlInfoReader {
xstream.alias("Area", Area.class);
xstream.aliasField("Name", Area.class, "name");
xstream.useAttributeFor(Area.class, "name");
xstream.aliasField("IntegrationID", Area.class, "integrationId");
xstream.useAttributeFor(Area.class, "integrationId");
xstream.aliasField("DeviceGroups", Area.class, "deviceNodes");
xstream.aliasField("Outputs", Area.class, "outputs");
xstream.aliasField("Areas", Area.class, "areas");

View File

@@ -6,7 +6,7 @@
<bridge-type id="ipbridge">
<label>Lutron IP Access Point</label>
<description>Ethernet access point to Lutron lighting control system</description>
<description>A Lutron controller using Lutron Integration Protocol (LIP) over TCP/IP</description>
<properties>
<property name="vendor">Lutron</property>
</properties>
@@ -57,9 +57,79 @@
</config-description>
</bridge-type>
<bridge-type id="leapbridge">
<label>Lutron LEAP Access Point</label>
<description>A Lutron controller using LEAP protocol over TCP/IP</description>
<channels>
<channel id="command" typeId="command-type"/>
</channels>
<properties>
<property name="vendor">Lutron</property>
</properties>
<representation-property>serialNumber</representation-property>
<config-description>
<parameter name="ipAddress" type="text" required="true">
<context>network-address</context>
<label>IP or Host Name</label>
<description>The IP or host name of the Lutron integration access point</description>
</parameter>
<parameter name="port" type="integer">
<label>TCP Port</label>
<description>The bridge TCP port</description>
<default>8081</default>
</parameter>
<parameter name="keystore" type="text" required="true">
<label>Keystore File</label>
<description>Java keystore containing caseta key and certs</description>
</parameter>
<parameter name="keystorePassword" type="text">
<context>password</context>
<label>Keystore Password</label>
<description>Password for the keystore file</description>
</parameter>
<parameter name="certValidate" type="boolean" required="false">
<label>Validate SSL Certificates</label>
<description>Validate server SSL certificate</description>
<default>true</default>
</parameter>
<parameter name="reconnect" type="integer" min="1" max="60" unit="min">
<label>Reconnect Interval</label>
<description>The period in minutes that the handler will wait between connection attempts</description>
<unitLabel>minutes</unitLabel>
<default>5</default>
<advanced>true</advanced>
</parameter>
<parameter name="heartbeat" type="integer" min="1" max="60" unit="min">
<label>Keepalive Heartbeat Interval</label>
<description>The period in minutes between connection heartbeat checks</description>
<unitLabel>minutes</unitLabel>
<default>5</default>
<advanced>true</advanced>
</parameter>
<parameter name="delay" type="integer" min="0" max="250" unit="ms">
<label>Send Delay</label>
<description>The delay in milliseconds between sending integration commands (for throttling)</description>
<unitLabel>ms</unitLabel>
<default>0</default>
<advanced>true</advanced>
</parameter>
</config-description>
</bridge-type>
<channel-type id="command-type" advanced="true">
<item-type>String</item-type>
<label>LEAP Command</label>
<description>LEAP command to send (for debugging)</description>
</channel-type>
<thing-type id="dimmer">
<supported-bridge-type-refs>
<bridge-type-ref id="ipbridge"/>
<bridge-type-ref id="leapbridge"/>
</supported-bridge-type-refs>
<label>Maestro Dimmer</label>
@@ -102,9 +172,34 @@
</config-description>
</thing-type>
<thing-type id="fan">
<supported-bridge-type-refs>
<bridge-type-ref id="ipbridge"/>
<bridge-type-ref id="leapbridge"/>
</supported-bridge-type-refs>
<label>Fan Speed Controller</label>
<description>Controls ceiling fans</description>
<channels>
<channel id="fanspeed" typeId="fanControl"/>
<channel id="fanlevel" typeId="lightDimmer"/>
</channels>
<representation-property>integrationId</representation-property>
<config-description>
<parameter name="integrationId" type="integer" required="true">
<label>Integration ID</label>
<description>Address of fan controller in the Lutron system</description>
</parameter>
</config-description>
</thing-type>
<thing-type id="shade">
<supported-bridge-type-refs>
<bridge-type-ref id="ipbridge"/>
<bridge-type-ref id="leapbridge"/>
</supported-bridge-type-refs>
<label>Sivoia QS Shade</label>
@@ -160,6 +255,7 @@
<thing-type id="switch">
<supported-bridge-type-refs>
<bridge-type-ref id="ipbridge"/>
<bridge-type-ref id="leapbridge"/>
</supported-bridge-type-refs>
<label>Maestro Switch</label>
@@ -307,6 +403,29 @@
</config-description>
</thing-type>
<thing-type id="ogroup">
<supported-bridge-type-refs>
<bridge-type-ref id="ipbridge"/>
<bridge-type-ref id="leapbridge"/>
</supported-bridge-type-refs>
<label>Occupancy Group</label>
<description>Shows state of occupancy sensor group</description>
<channels>
<channel id="groupstate" typeId="groupState"/>
</channels>
<representation-property>integrationId</representation-property>
<config-description>
<parameter name="integrationId" type="integer" required="true">
<label>Integration ID</label>
<description>Occupancy group number</description>
</parameter>
</config-description>
</thing-type>
<thing-type id="keypad">
<supported-bridge-type-refs>
<bridge-type-ref id="ipbridge"/>
@@ -484,6 +603,7 @@
<thing-type id="pico">
<supported-bridge-type-refs>
<bridge-type-ref id="ipbridge"/>
<bridge-type-ref id="leapbridge"/>
</supported-bridge-type-refs>
<label>Pico Keypad</label>
@@ -601,6 +721,7 @@
<thing-type id="virtualkeypad">
<supported-bridge-type-refs>
<bridge-type-ref id="ipbridge"/>
<bridge-type-ref id="leapbridge"/>
</supported-bridge-type-refs>
<label>Virtual Keypad</label>
@@ -617,8 +738,8 @@
<label>Virtual Keypad Type</label>
<description>System type for virtual keypad</description>
<options>
<option value="Caseta">Caseta Scene Buttons</option>
<option value="Other">HomeWorks/RA 2/Other</option>
<option value="Caseta">Caseta/RA2 Select Scene Buttons</option>
<option value="Other">HomeWorks/RadioRA 2/Other</option>
</options>
<default>Other</default>
</parameter>
@@ -1172,6 +1293,33 @@
<state readOnly="true"/>
</channel-type>
<channel-type id="fanControl">
<item-type>String</item-type>
<label>Fan Speed</label>
<state readOnly="false">
<options>
<option value="OFF">Off</option>
<option value="LOW">Low</option>
<option value="MEDIUM">Medium</option>
<option value="MEDIUMHIGH">MediumHigh</option>
<option value="HIGH">High</option>
</options>
</state>
</channel-type>
<channel-type id="groupState">
<item-type>String</item-type>
<label>Occupancy State</label>
<category>Motion</category>
<state readOnly="true">
<options>
<option value="OCCUPIED">Occupied</option>
<option value="UNOCCUPIED">Unoccupied</option>
<option value="UNKNOWN">Unknown</option>
</options>
</state>
</channel-type>
<channel-type id="button">
<item-type>Switch</item-type>
<label>Keypad Button</label>