added migrated 2.x add-ons

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

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<features name="org.openhab.binding.lutron-${project.version}" xmlns="http://karaf.apache.org/xmlns/features/v1.4.0">
<repository>mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features</repository>
<feature name="openhab-binding-lutron" description="Lutron Binding" version="${project.version}">
<feature>openhab-runtime-base</feature>
<feature>openhab-transport-serial</feature>
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.lutron/${project.version}</bundle>
</feature>
</features>

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.action;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.math.BigDecimal;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.lutron.internal.handler.DimmerHandler;
import org.openhab.binding.lutron.internal.protocol.LutronDuration;
import org.openhab.core.automation.annotation.ActionInput;
import org.openhab.core.automation.annotation.RuleAction;
import org.openhab.core.thing.binding.ThingActions;
import org.openhab.core.thing.binding.ThingActionsScope;
import org.openhab.core.thing.binding.ThingHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link DimmerActions} defines thing actions for DimmerHandler.
*
* @author Bob Adair - Initial contribution
*/
@ThingActionsScope(name = "lutron")
@NonNullByDefault
public class DimmerActions implements ThingActions, IDimmerActions {
private final Logger logger = LoggerFactory.getLogger(DimmerActions.class);
private @Nullable DimmerHandler handler;
public DimmerActions() {
logger.trace("Lutron Dimmer actions service created");
}
@Override
public void setThingHandler(@Nullable ThingHandler handler) {
if (handler instanceof DimmerHandler) {
this.handler = (DimmerHandler) handler;
}
}
@Override
public @Nullable ThingHandler getThingHandler() {
return handler;
}
/**
* The setLevel dimmer thing action
*/
@Override
@RuleAction(label = "setLevel", description = "Send set level command with fade and delay times")
public void setLevel(
@ActionInput(name = "level", label = "Dimmer Level", description = "New dimmer level (0-100)") @Nullable Double level,
@ActionInput(name = "fadeTime", label = "Fade Time", description = "Time to fade to new level (seconds)") @Nullable Double fadeTime,
@ActionInput(name = "delayTime", label = "Delay Time", description = "Delay before starting fade (seconds)") @Nullable Double delayTime) {
DimmerHandler dimmerHandler = handler;
if (dimmerHandler == null) {
logger.debug("Handler not set for Dimmer thing actions.");
return;
}
if (level == null) {
logger.debug("Ignoring setLevel command due to null level value.");
return;
}
if (fadeTime == null) {
logger.debug("Ignoring setLevel command due to null value for fadeTime.");
return;
}
if (delayTime == null) {
logger.debug("Ignoring setLevel command due to null value for delayTime.");
return;
}
Double lightLevel = level;
if (lightLevel > 100.0) {
lightLevel = 100.0;
} else if (lightLevel < 0.0) {
lightLevel = 0.0;
}
try {
dimmerHandler.setLightLevel(new BigDecimal(lightLevel).setScale(2, BigDecimal.ROUND_HALF_UP),
new LutronDuration(fadeTime), new LutronDuration(delayTime));
} catch (IllegalArgumentException e) {
logger.debug("Ignoring setLevel command due to illegal argument exception: {}", e.getMessage());
}
}
/**
* Static setLevel method for Rules DSL backward compatibility
*/
public static void setLevel(@Nullable ThingActions actions, @Nullable Double level, @Nullable Double fadeTime,
@Nullable Double delayTime) {
invokeMethodOf(actions).setLevel(level, fadeTime, delayTime); // Replace when core issue #1536 is fixed
}
/**
* This is only necessary to work around a bug in openhab-core (issue #1536). It should be removed once that is
* resolved.
*/
private static IDimmerActions invokeMethodOf(@Nullable ThingActions actions) {
if (actions == null) {
throw new IllegalArgumentException("actions cannot be null");
}
if (actions.getClass().getName().equals(DimmerActions.class.getName())) {
if (actions instanceof IDimmerActions) {
return (IDimmerActions) actions;
} else {
return (IDimmerActions) Proxy.newProxyInstance(IDimmerActions.class.getClassLoader(),
new Class[] { IDimmerActions.class }, (Object proxy, Method method, Object[] args) -> {
Method m = actions.getClass().getDeclaredMethod(method.getName(),
method.getParameterTypes());
return m.invoke(actions, args);
});
}
}
throw new IllegalArgumentException("Actions is not an instance of DimmerActions");
}
}

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.action;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* The {@link IDimmerActions} interface defines the interface for all thing actions supported by the dimmer thing.
* This is only necessary to work around a bug in openhab-core (issue #1536). It should be removed once that is
* resolved.
*
* @author Bob Adair - Initial contribution
*
*/
@NonNullByDefault
public interface IDimmerActions {
public void setLevel(@Nullable Double level, @Nullable Double fadeTime, @Nullable Double delayTime);
}

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;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.lutron.internal.discovery.project.ComponentType;
/**
* The {@link KeypadComponent} interface is used to access enums describing the possible components
* in a given keypad model.
*
* @author Bob Adair - Initial contribution
*/
@NonNullByDefault
public interface KeypadComponent {
public int id();
public String channel();
public String description();
public ComponentType type();
}

View File

@@ -0,0 +1,97 @@
/**
* 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;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.thing.ThingTypeUID;
/**
* The {@link LutronBinding} class defines common constants, which are
* used across the whole binding.
*
* @author Allan Tong - Initial contribution
*/
@NonNullByDefault
public class LutronBindingConstants {
public static final String BINDING_ID = "lutron";
// Bridge Type UIDs
public static final ThingTypeUID THING_TYPE_IPBRIDGE = new ThingTypeUID(BINDING_ID, "ipbridge");
// List of all Thing Type UIDs
public static final ThingTypeUID THING_TYPE_DIMMER = new ThingTypeUID(BINDING_ID, "dimmer");
public static final ThingTypeUID THING_TYPE_SHADE = new ThingTypeUID(BINDING_ID, "shade");
public static final ThingTypeUID THING_TYPE_SWITCH = new ThingTypeUID(BINDING_ID, "switch");
public static final ThingTypeUID THING_TYPE_OCCUPANCYSENSOR = new ThingTypeUID(BINDING_ID, "occupancysensor");
public static final ThingTypeUID THING_TYPE_KEYPAD = new ThingTypeUID(BINDING_ID, "keypad");
public static final ThingTypeUID THING_TYPE_TTKEYPAD = new ThingTypeUID(BINDING_ID, "ttkeypad");
public static final ThingTypeUID THING_TYPE_INTLKEYPAD = new ThingTypeUID(BINDING_ID, "intlkeypad");
public static final ThingTypeUID THING_TYPE_PICO = new ThingTypeUID(BINDING_ID, "pico");
public static final ThingTypeUID THING_TYPE_VIRTUALKEYPAD = new ThingTypeUID(BINDING_ID, "virtualkeypad");
public static final ThingTypeUID THING_TYPE_VCRX = new ThingTypeUID(BINDING_ID, "vcrx");
public static final ThingTypeUID THING_TYPE_CCO = new ThingTypeUID(BINDING_ID, "cco");
public static final ThingTypeUID THING_TYPE_CCO_PULSED = new ThingTypeUID(BINDING_ID, "ccopulsed");
public static final ThingTypeUID THING_TYPE_CCO_MAINTAINED = new ThingTypeUID(BINDING_ID, "ccomaintained");
public static final ThingTypeUID THING_TYPE_TIMECLOCK = new ThingTypeUID(BINDING_ID, "timeclock");
public static final ThingTypeUID THING_TYPE_GREENMODE = new ThingTypeUID(BINDING_ID, "greenmode");
public static final ThingTypeUID THING_TYPE_QSIO = new ThingTypeUID(BINDING_ID, "qsio");
public static final ThingTypeUID THING_TYPE_GRAFIKEYEKEYPAD = new ThingTypeUID(BINDING_ID, "grafikeyekeypad");
public static final ThingTypeUID THING_TYPE_BLIND = new ThingTypeUID(BINDING_ID, "blind");
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");
// 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_CLOCKMODE = "clockmode";
public static final String CHANNEL_SUNRISE = "sunrise";
public static final String CHANNEL_SUNSET = "sunset";
public static final String CHANNEL_EXECEVENT = "execevent";
public static final String CHANNEL_ENABLEEVENT = "enableevent";
public static final String CHANNEL_DISABLEEVENT = "disableevent";
public static final String CHANNEL_STEP = "step";
public static final String CHANNEL_BLINDLIFTLEVEL = "blindliftlevel";
public static final String CHANNEL_BLINDTILTLEVEL = "blindtiltlevel";
public static final String CHANNEL_VARSTATE = "varstate";
// Bridge config properties (used by discovery service)
public static final String HOST = "ipAddress";
public static final String USER = "user";
public static final String PASSWORD = "password";
public static final String SERIAL_NUMBER = "serialNumber";
public static final String DISCOVERY_FILE = "discoveryFile";
public static final String PROPERTY_PRODFAM = "productFamily";
public static final String PROPERTY_PRODTYP = "productType";
// Thing config properties
public static final String INTEGRATION_ID = "integrationId";
// CCO config properties
public static final String CCO_TYPE = "outputType";
public static final String CCO_TYPE_PULSED = "Pulsed";
public static final String CCO_TYPE_MAINTAINED = "Maintained";
public static final String DEFAULT_PULSE = "pulseLength";
// GreenMode config properties
public static final String POLL_INTERVAL = "pollInterval";
// Blind types
public static final String BLIND_TYPE_PARAMETER = "type";
public static final String BLIND_TYPE_SHEER = "Sheer";
public static final String BLIND_TYPE_VENETIAN = "Venetian";
}

View File

@@ -0,0 +1,224 @@
/**
* 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;
import static org.openhab.binding.lutron.internal.LutronBindingConstants.*;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jetty.client.HttpClient;
import org.openhab.binding.lutron.internal.discovery.LutronDeviceDiscoveryService;
import org.openhab.binding.lutron.internal.grxprg.GrafikEyeHandler;
import org.openhab.binding.lutron.internal.grxprg.PrgBridgeHandler;
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.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.MaintainedCcoHandler;
import org.openhab.binding.lutron.internal.handler.OccupancySensorHandler;
import org.openhab.binding.lutron.internal.handler.PalladiomKeypadHandler;
import org.openhab.binding.lutron.internal.handler.PicoKeypadHandler;
import org.openhab.binding.lutron.internal.handler.PulsedCcoHandler;
import org.openhab.binding.lutron.internal.handler.QSIOHandler;
import org.openhab.binding.lutron.internal.handler.ShadeHandler;
import org.openhab.binding.lutron.internal.handler.SwitchHandler;
import org.openhab.binding.lutron.internal.handler.SysvarHandler;
import org.openhab.binding.lutron.internal.handler.TabletopKeypadHandler;
import org.openhab.binding.lutron.internal.handler.TimeclockHandler;
import org.openhab.binding.lutron.internal.handler.VcrxHandler;
import org.openhab.binding.lutron.internal.handler.VirtualKeypadHandler;
import org.openhab.binding.lutron.internal.handler.WciHandler;
import org.openhab.binding.lutron.internal.hw.HwConstants;
import org.openhab.binding.lutron.internal.hw.HwDimmerHandler;
import org.openhab.binding.lutron.internal.hw.HwSerialBridgeHandler;
import org.openhab.binding.lutron.internal.radiora.RadioRAConstants;
import org.openhab.binding.lutron.internal.radiora.handler.PhantomButtonHandler;
import org.openhab.binding.lutron.internal.radiora.handler.RS232Handler;
import org.openhab.core.config.discovery.DiscoveryService;
import org.openhab.core.io.net.http.HttpClientFactory;
import org.openhab.core.io.transport.serial.SerialPortManager;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.ThingUID;
import org.openhab.core.thing.binding.BaseThingHandlerFactory;
import org.openhab.core.thing.binding.ThingHandler;
import org.openhab.core.thing.binding.ThingHandlerFactory;
import org.osgi.framework.ServiceRegistration;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link LutronHandlerFactory} is responsible for creating things and thing
* handlers.
*
* @author Allan Tong - Initial contribution
* @author Bob Adair - Added bridge discovery service registration/removal
*/
@NonNullByDefault
@Component(service = ThingHandlerFactory.class, configurationPid = "binding.lutron")
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()));
// Used by the HwDiscoveryService
public static final Set<ThingTypeUID> HW_DISCOVERABLE_DEVICE_TYPES_UIDS = Collections
.unmodifiableSet(Collections.singleton(HwConstants.THING_TYPE_HWDIMMER));
// 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()));
private final Logger logger = LoggerFactory.getLogger(LutronHandlerFactory.class);
private final SerialPortManager serialPortManager;
private final HttpClient httpClient;
@Activate
public LutronHandlerFactory(final @Reference SerialPortManager serialPortManager,
@Reference HttpClientFactory httpClientFactory) {
this.serialPortManager = serialPortManager;
this.httpClient = httpClientFactory.getCommonHttpClient();
}
@Override
public boolean supportsThingType(ThingTypeUID thingTypeUID) {
return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID)
|| DISCOVERABLE_DEVICE_TYPES_UIDS.contains(thingTypeUID)
|| HW_DISCOVERABLE_DEVICE_TYPES_UIDS.contains(thingTypeUID);
}
private final Map<ThingUID, @Nullable ServiceRegistration<?>> discoveryServiceRegMap = new HashMap<>();
// Marked as Nullable only to fix incorrect redundant null check complaints after adding null annotations
@Override
protected @Nullable ThingHandler createHandler(Thing thing) {
ThingTypeUID thingTypeUID = thing.getThingTypeUID();
if (thingTypeUID.equals(THING_TYPE_IPBRIDGE)) {
IPBridgeHandler bridgeHandler = new IPBridgeHandler((Bridge) thing);
registerDiscoveryService(bridgeHandler);
return bridgeHandler;
} else if (thingTypeUID.equals(THING_TYPE_DIMMER)) {
return new DimmerHandler(thing);
} else if (thingTypeUID.equals(THING_TYPE_SHADE)) {
return new ShadeHandler(thing);
} else if (thingTypeUID.equals(THING_TYPE_SWITCH)) {
return new SwitchHandler(thing);
} else if (thingTypeUID.equals(THING_TYPE_CCO)) {
return new CcoHandler(thing);
} else if (thingTypeUID.equals(THING_TYPE_CCO_PULSED)) {
return new PulsedCcoHandler(thing);
} else if (thingTypeUID.equals(THING_TYPE_CCO_MAINTAINED)) {
return new MaintainedCcoHandler(thing);
} else if (thingTypeUID.equals(THING_TYPE_OCCUPANCYSENSOR)) {
return new OccupancySensorHandler(thing);
} else if (thingTypeUID.equals(THING_TYPE_KEYPAD)) {
return new KeypadHandler(thing);
} else if (thingTypeUID.equals(THING_TYPE_TTKEYPAD)) {
return new TabletopKeypadHandler(thing);
} else if (thingTypeUID.equals(THING_TYPE_INTLKEYPAD)) {
return new IntlKeypadHandler(thing);
} else if (thingTypeUID.equals(THING_TYPE_PICO)) {
return new PicoKeypadHandler(thing);
} else if (thingTypeUID.equals(THING_TYPE_GRAFIKEYEKEYPAD)) {
return new GrafikEyeKeypadHandler(thing);
} else if (thingTypeUID.equals(THING_TYPE_PALLADIOMKEYPAD)) {
return new PalladiomKeypadHandler(thing);
} else if (thingTypeUID.equals(THING_TYPE_VIRTUALKEYPAD)) {
return new VirtualKeypadHandler(thing);
} else if (thingTypeUID.equals(THING_TYPE_VCRX)) {
return new VcrxHandler(thing);
} else if (thingTypeUID.equals(THING_TYPE_WCI)) {
return new WciHandler(thing);
} else if (thingTypeUID.equals(THING_TYPE_TIMECLOCK)) {
return new TimeclockHandler(thing);
} else if (thingTypeUID.equals(THING_TYPE_GREENMODE)) {
return new GreenModeHandler(thing);
} else if (thingTypeUID.equals(THING_TYPE_QSIO)) {
return new QSIOHandler(thing);
} else if (thingTypeUID.equals(THING_TYPE_BLIND)) {
return new BlindHandler(thing);
} else if (thingTypeUID.equals(THING_TYPE_SYSVAR)) {
return new SysvarHandler(thing);
} else if (thingTypeUID.equals(PrgConstants.THING_TYPE_PRGBRIDGE)) {
return new PrgBridgeHandler((Bridge) thing);
} else if (thingTypeUID.equals(PrgConstants.THING_TYPE_GRAFIKEYE)) {
return new GrafikEyeHandler(thing);
} else if (thingTypeUID.equals(RadioRAConstants.THING_TYPE_RS232)) {
return new RS232Handler((Bridge) thing, serialPortManager);
} else if (thingTypeUID.equals(RadioRAConstants.THING_TYPE_DIMMER)) {
return new org.openhab.binding.lutron.internal.radiora.handler.DimmerHandler(thing);
} else if (thingTypeUID.equals(RadioRAConstants.THING_TYPE_SWITCH)) {
return new org.openhab.binding.lutron.internal.radiora.handler.SwitchHandler(thing);
} else if (thingTypeUID.equals(RadioRAConstants.THING_TYPE_PHANTOM)) {
return new PhantomButtonHandler(thing);
} else if (thingTypeUID.equals(HwConstants.THING_TYPE_HWSERIALBRIDGE)) {
return new HwSerialBridgeHandler((Bridge) thing, serialPortManager);
} else if (thingTypeUID.equals(HwConstants.THING_TYPE_HWDIMMER)) {
return new HwDimmerHandler(thing);
}
return null;
}
@Override
protected synchronized void removeHandler(ThingHandler thingHandler) {
if (thingHandler instanceof IPBridgeHandler) {
ServiceRegistration<?> serviceReg = discoveryServiceRegMap.remove(thingHandler.getThing().getUID());
if (serviceReg != null) {
logger.debug("Unregistering discovery service.");
serviceReg.unregister();
}
}
}
/**
* Register a discovery service for an IP bridge handler.
*
* @param bridgeHandler bridge handler for which to register the discovery service
*/
private synchronized void registerDiscoveryService(IPBridgeHandler bridgeHandler) {
logger.debug("Registering discovery service.");
LutronDeviceDiscoveryService discoveryService = new LutronDeviceDiscoveryService(bridgeHandler, httpClient);
bridgeHandler.setDiscoveryService(discoveryService);
discoveryServiceRegMap.put(bridgeHandler.getThing().getUID(),
bundleContext.registerService(DiscoveryService.class.getName(), discoveryService, null));
}
}

View File

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

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.config;
import java.math.BigDecimal;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Configuration settings for a {@link org.openhab.binding.lutron.internal.handler.DimmerHandler}.
*
* @author Allan Tong - Initial contribution
*/
@NonNullByDefault
public class DimmerConfig {
private static final int DEFAULT_FADE = 1;
private static final int DEFAULT_ONLEVEL = 100;
private static final boolean DEFAULT_ONTOLAST = false;
public int integrationId;
public BigDecimal fadeInTime = new BigDecimal(DEFAULT_FADE);
public BigDecimal fadeOutTime = new BigDecimal(DEFAULT_FADE);
public BigDecimal onLevel = new BigDecimal(DEFAULT_ONLEVEL);
public Boolean onToLast = new Boolean(DEFAULT_ONTOLAST);
}

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.config;
import org.apache.commons.lang.StringUtils;
/**
* Configuration settings for an {@link org.openhab.binding.lutron.internal.handler.IPBridgeHandler}.
*
* @author Allan Tong - Initial contribution
* @author Bob Adair - Added reconnect, heartbeat and discoveryFile parameters
*/
public class IPBridgeConfig {
public String ipAddress;
public String user;
public String password;
public String discoveryFile;
public int reconnect;
public int heartbeat;
public int delay = 0;
public boolean sameConnectionParameters(IPBridgeConfig config) {
return StringUtils.equals(ipAddress, config.ipAddress) && StringUtils.equals(user, config.user)
&& StringUtils.equals(password, config.password) && (reconnect == config.reconnect)
&& (heartbeat == config.heartbeat) && (delay == config.delay);
}
}

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.SysvarHandler}.
*
* @author Bob Adair - Initial contribution
*/
@NonNullByDefault
public class SysvarConfig {
public int integrationId;
}

View File

@@ -0,0 +1,475 @@
/**
* 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.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Stack;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.client.api.Response;
import org.eclipse.jetty.client.util.InputStreamResponseListener;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.HttpMethod;
import org.eclipse.jetty.http.HttpStatus;
import org.openhab.binding.lutron.internal.LutronHandlerFactory;
import org.openhab.binding.lutron.internal.discovery.project.Area;
import org.openhab.binding.lutron.internal.discovery.project.Component;
import org.openhab.binding.lutron.internal.discovery.project.ComponentType;
import org.openhab.binding.lutron.internal.discovery.project.Device;
import org.openhab.binding.lutron.internal.discovery.project.DeviceGroup;
import org.openhab.binding.lutron.internal.discovery.project.DeviceNode;
import org.openhab.binding.lutron.internal.discovery.project.DeviceType;
import org.openhab.binding.lutron.internal.discovery.project.GreenMode;
import org.openhab.binding.lutron.internal.discovery.project.Output;
import org.openhab.binding.lutron.internal.discovery.project.OutputType;
import org.openhab.binding.lutron.internal.discovery.project.Project;
import org.openhab.binding.lutron.internal.discovery.project.Timeclock;
import org.openhab.binding.lutron.internal.handler.IPBridgeHandler;
import org.openhab.binding.lutron.internal.keypadconfig.KeypadConfig;
import org.openhab.binding.lutron.internal.keypadconfig.KeypadConfigGrafikEye;
import org.openhab.binding.lutron.internal.keypadconfig.KeypadConfigIntlSeetouch;
import org.openhab.binding.lutron.internal.keypadconfig.KeypadConfigPalladiom;
import org.openhab.binding.lutron.internal.keypadconfig.KeypadConfigPico;
import org.openhab.binding.lutron.internal.keypadconfig.KeypadConfigSeetouch;
import org.openhab.binding.lutron.internal.keypadconfig.KeypadConfigTabletopSeetouch;
import org.openhab.binding.lutron.internal.xml.DbXmlInfoReader;
import org.openhab.core.config.discovery.AbstractDiscoveryService;
import org.openhab.core.config.discovery.DiscoveryResult;
import org.openhab.core.config.discovery.DiscoveryResultBuilder;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.ThingUID;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link LutronDeviceDiscoveryService} finds all devices paired with Lutron bridges by retrieving the
* configuration XML from them via HTTP.
*
* @author Allan Tong - Initial contribution
* @author Bob Adair - Added support for more output devices and keypads, VCRX, repeater virtual buttons,
* Timeclock, and Green Mode. Added option to read XML from file. Switched to jetty HTTP client for better
* exception handling. Added keypad model discovery.
*/
@NonNullByDefault
public class LutronDeviceDiscoveryService extends AbstractDiscoveryService {
private static final int DECLARATION_MAX_LEN = 80;
private static final long HTTP_REQUEST_TIMEOUT = 60; // seconds
private static final int DISCOVERY_SERVICE_TIMEOUT = 90; // seconds
private static final String XML_DECLARATION_START = "<?xml";
private static final Pattern XML_DECLARATION_PATTERN = Pattern.compile(XML_DECLARATION_START,
Pattern.LITERAL | Pattern.CASE_INSENSITIVE);
private final Logger logger = LoggerFactory.getLogger(LutronDeviceDiscoveryService.class);
private IPBridgeHandler bridgeHandler;
private DbXmlInfoReader dbXmlInfoReader = new DbXmlInfoReader();
private final HttpClient httpClient;
private @Nullable Future<?> scanTask;
public LutronDeviceDiscoveryService(IPBridgeHandler bridgeHandler, HttpClient httpClient)
throws IllegalArgumentException {
super(LutronHandlerFactory.DISCOVERABLE_DEVICE_TYPES_UIDS, DISCOVERY_SERVICE_TIMEOUT);
this.bridgeHandler = bridgeHandler;
this.httpClient = httpClient;
}
@Override
protected synchronized void startScan() {
if (scanTask == null || scanTask.isDone()) {
scanTask = scheduler.submit(this::asyncDiscoveryTask);
}
}
private synchronized void asyncDiscoveryTask() {
try {
readDeviceDatabase();
} catch (RuntimeException e) {
logger.warn("Runtime exception scanning for devices: {}", e.getMessage(), e);
if (scanListener != null) {
scanListener.onErrorOccurred(null); // null so it won't log a stack trace
}
}
}
private void readDeviceDatabase() {
Project project = null;
if (bridgeHandler == null || bridgeHandler.getIPBridgeConfig() == null) {
logger.debug("Unable to get bridge config. Exiting.");
return;
}
String discFileName = bridgeHandler.getIPBridgeConfig().discoveryFile;
String address = "http://" + bridgeHandler.getIPBridgeConfig().ipAddress + "/DbXmlInfo.xml";
if (discFileName == null || discFileName.isEmpty()) {
// Read XML from bridge via HTTP
logger.trace("Sending http request for {}", address);
InputStreamResponseListener listener = new InputStreamResponseListener();
Response response = null;
// Use response stream instead of doing it the simple synchronous way because the response can be very large
httpClient.newRequest(address).method(HttpMethod.GET).timeout(HTTP_REQUEST_TIMEOUT, TimeUnit.SECONDS)
.header(HttpHeader.ACCEPT, "text/html").header(HttpHeader.ACCEPT_CHARSET, "utf-8").send(listener);
try {
response = listener.get(HTTP_REQUEST_TIMEOUT, TimeUnit.SECONDS);
} catch (InterruptedException | TimeoutException | ExecutionException e) {
logger.info("Exception getting HTTP response: {}", e.getMessage());
}
if (response != null && response.getStatus() == HttpStatus.OK_200) {
logger.trace("Received good http response.");
try (InputStream responseStream = listener.getInputStream();
InputStreamReader xmlStreamReader = new InputStreamReader(responseStream,
StandardCharsets.UTF_8);
BufferedReader xmlBufReader = new BufferedReader(xmlStreamReader)) {
flushPrePrologLines(xmlBufReader);
project = dbXmlInfoReader.readFromXML(xmlBufReader);
if (project == null) {
logger.info("Failed to parse XML project file from {}", address);
}
} catch (IOException e) {
logger.info("IOException while processing XML project file: {}", e.getMessage());
}
} else {
if (response != null) {
logger.info("Received HTTP error response: {} {}", response.getStatus(), response.getReason());
} else {
logger.info("No response for HTTP request.");
}
}
} else {
// Read XML from file
File xmlFile = new File(discFileName);
try (BufferedReader xmlReader = Files.newBufferedReader(xmlFile.toPath(), StandardCharsets.UTF_8)) {
flushPrePrologLines(xmlReader);
project = dbXmlInfoReader.readFromXML(xmlReader);
if (project == null) {
logger.info("Could not process XML project file {}", discFileName);
}
} catch (IOException | SecurityException e) {
logger.info("Exception reading XML project file {} : {}", discFileName, e.getMessage());
}
}
if (project != null) {
Stack<String> locationContext = new Stack<>();
for (Area area : project.getAreas()) {
processArea(area, locationContext);
}
for (Timeclock timeclock : project.getTimeclocks()) {
processTimeclocks(timeclock, locationContext);
}
for (GreenMode greenMode : project.getGreenModes()) {
processGreenModes(greenMode, locationContext);
}
}
}
/**
* Flushes any lines or characters before the start of the XML declaration in the supplied BufferedReader.
*
* @param xmlReader BufferedReader source of the XML document
* @throws IOException
*/
private void flushPrePrologLines(BufferedReader xmlReader) throws IOException {
String inLine = null;
xmlReader.mark(DECLARATION_MAX_LEN);
boolean foundXmlDec = false;
while (!foundXmlDec && (inLine = xmlReader.readLine()) != null) {
Matcher matcher = XML_DECLARATION_PATTERN.matcher(inLine);
if (matcher.find()) {
foundXmlDec = true;
xmlReader.reset();
if (matcher.start() > 0) {
logger.trace("Discarding {} characters.", matcher.start());
xmlReader.skip(matcher.start());
}
} else {
logger.trace("Discarding line: {}", inLine);
xmlReader.mark(DECLARATION_MAX_LEN);
}
}
}
private void processArea(Area area, Stack<String> context) {
context.push(area.getName());
for (DeviceNode deviceNode : area.getDeviceNodes()) {
if (deviceNode instanceof DeviceGroup) {
processDeviceGroup((DeviceGroup) deviceNode, context);
} else if (deviceNode instanceof Device) {
processDevice((Device) deviceNode, context);
}
}
for (Output output : area.getOutputs()) {
processOutput(output, context);
}
for (Area subarea : area.getAreas()) {
processArea(subarea, context);
}
context.pop();
}
private void processDeviceGroup(DeviceGroup deviceGroup, Stack<String> context) {
context.push(deviceGroup.getName());
for (Device device : deviceGroup.getDevices()) {
processDevice(device, context);
}
context.pop();
}
private void processDevice(Device device, Stack<String> context) {
List<Integer> buttons;
KeypadConfig kpConfig;
String kpModel;
DeviceType type = device.getDeviceType();
if (type != null) {
String label = generateLabel(context, device.getName());
switch (type) {
case MOTION_SENSOR:
notifyDiscovery(THING_TYPE_OCCUPANCYSENSOR, device.getIntegrationId(), label);
break;
case SEETOUCH_KEYPAD:
case HYBRID_SEETOUCH_KEYPAD:
kpConfig = new KeypadConfigSeetouch();
discoverKeypad(device, label, THING_TYPE_KEYPAD, "seeTouch Keypad", kpConfig);
break;
case INTERNATIONAL_SEETOUCH_KEYPAD:
kpConfig = new KeypadConfigIntlSeetouch();
discoverKeypad(device, label, THING_TYPE_INTLKEYPAD, "International seeTouch Keypad", kpConfig);
break;
case SEETOUCH_TABLETOP_KEYPAD:
kpConfig = new KeypadConfigTabletopSeetouch();
discoverKeypad(device, label, THING_TYPE_TTKEYPAD, "Tabletop seeTouch Keypad", kpConfig);
break;
case PALLADIOM_KEYPAD:
kpConfig = new KeypadConfigPalladiom();
discoverKeypad(device, label, THING_TYPE_PALLADIOMKEYPAD, "Palladiom Keypad", kpConfig);
break;
case PICO_KEYPAD:
kpConfig = new KeypadConfigPico();
discoverKeypad(device, label, THING_TYPE_PICO, "Pico Keypad", kpConfig);
break;
case VISOR_CONTROL_RECEIVER:
notifyDiscovery(THING_TYPE_VCRX, device.getIntegrationId(), label);
break;
case WCI:
notifyDiscovery(THING_TYPE_WCI, device.getIntegrationId(), label);
break;
case MAIN_REPEATER:
notifyDiscovery(THING_TYPE_VIRTUALKEYPAD, device.getIntegrationId(), label);
break;
case QS_IO_INTERFACE:
notifyDiscovery(THING_TYPE_QSIO, device.getIntegrationId(), label);
break;
case GRAFIK_EYE_QS:
buttons = getComponentIdList(device.getComponents(), ComponentType.BUTTON);
// remove button IDs >= 300 which the handler does not recognize
List<Integer> buttonsCopy = new ArrayList<>(buttons);
for (Integer c : buttonsCopy) {
if (c >= 300) {
buttons.remove(Integer.valueOf(c));
}
}
kpConfig = new KeypadConfigGrafikEye();
kpModel = kpConfig.determineModelFromComponentIds(buttons);
if (kpModel == null) {
logger.info("Unable to determine model of GrafikEye Keypad {} with button IDs: {}",
device.getIntegrationId(), buttons);
notifyDiscovery(THING_TYPE_GRAFIKEYEKEYPAD, device.getIntegrationId(), label);
} else {
logger.debug("Found GrafikEye keypad {} model: {}", device.getIntegrationId(), kpModel);
notifyDiscovery(THING_TYPE_GRAFIKEYEKEYPAD, device.getIntegrationId(), label, "model", kpModel);
}
break;
}
} else {
logger.warn("Unrecognized device type {}", device.getType());
}
}
private void discoverKeypad(Device device, String label, ThingTypeUID ttUid, String description,
KeypadConfig kpConfig) {
List<Integer> buttons = getComponentIdList(device.getComponents(), ComponentType.BUTTON);
String kpModel = kpConfig.determineModelFromComponentIds(buttons);
if (kpModel == null) {
logger.info("Unable to determine model of {} {} with button IDs: {}", description,
device.getIntegrationId(), buttons);
notifyDiscovery(ttUid, device.getIntegrationId(), label);
} else {
logger.debug("Found {} {} model: {}", description, device.getIntegrationId(), kpModel);
notifyDiscovery(ttUid, device.getIntegrationId(), label, "model", kpModel);
}
}
private List<Integer> getComponentIdList(List<Component> clist, ComponentType ctype) {
List<Integer> returnList = new LinkedList<>();
for (Component c : clist) {
if (c.getComponentType() == ctype) {
returnList.add(c.getComponentNumber());
}
}
return returnList;
}
private void processOutput(Output output, Stack<String> context) {
OutputType type = output.getOutputType();
if (type != null) {
String label = generateLabel(context, output.getName());
switch (type) {
case INC:
case MLV:
case ELV:
case DALI:
case ECO_SYSTEM_FLUORESCENT:
case FLUORESCENT_DB:
case ZERO_TO_TEN:
case AUTO_DETECT:
case CEILING_FAN_TYPE:
notifyDiscovery(THING_TYPE_DIMMER, output.getIntegrationId(), label);
break;
case NON_DIM:
case NON_DIM_INC:
case NON_DIM_ELV:
case RELAY_LIGHTING:
notifyDiscovery(THING_TYPE_SWITCH, output.getIntegrationId(), label);
break;
case CCO_PULSED:
notifyDiscovery(THING_TYPE_CCO, output.getIntegrationId(), label, CCO_TYPE, CCO_TYPE_PULSED);
break;
case CCO_MAINTAINED:
notifyDiscovery(THING_TYPE_CCO, output.getIntegrationId(), label, CCO_TYPE, CCO_TYPE_MAINTAINED);
break;
case SYSTEM_SHADE:
case MOTOR:
notifyDiscovery(THING_TYPE_SHADE, output.getIntegrationId(), label);
break;
case SHEER_BLIND:
notifyDiscovery(THING_TYPE_BLIND, output.getIntegrationId(), label, BLIND_TYPE_PARAMETER,
BLIND_TYPE_SHEER);
break;
case VENETIAN_BLIND:
notifyDiscovery(THING_TYPE_BLIND, output.getIntegrationId(), label, BLIND_TYPE_PARAMETER,
BLIND_TYPE_VENETIAN);
break;
}
} else {
logger.warn("Unrecognized output type {}", output.getType());
}
}
private void processTimeclocks(Timeclock timeclock, Stack<String> context) {
String label = generateLabel(context, timeclock.getName());
notifyDiscovery(THING_TYPE_TIMECLOCK, timeclock.getIntegrationId(), label);
}
private void processGreenModes(GreenMode greenmode, Stack<String> context) {
String label = generateLabel(context, greenmode.getName());
notifyDiscovery(THING_TYPE_GREENMODE, greenmode.getIntegrationId(), label);
}
private void notifyDiscovery(ThingTypeUID thingTypeUID, @Nullable Integer integrationId, String label,
@Nullable String propName, @Nullable Object propValue) {
if (integrationId == null) {
logger.info("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.debug("Discovered {}", uid);
}
private void notifyDiscovery(ThingTypeUID thingTypeUID, Integer integrationId, String label) {
notifyDiscovery(thingTypeUID, integrationId, label, null, null);
}
private String generateLabel(Stack<String> context, String deviceName) {
return String.join(" ", context) + " " + deviceName;
}
}

View File

@@ -0,0 +1,235 @@
/**
* 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.io.IOException;
import java.net.DatagramPacket;
import java.net.InetAddress;
import java.net.MulticastSocket;
import java.net.SocketTimeoutException;
import java.nio.charset.StandardCharsets;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
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.Thing;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.ThingUID;
import org.osgi.service.component.annotations.Component;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link LutronMcastBridgeDiscoveryService} finds RadioRA 2 Main Repeaters and HomeWorks QS
* Processors on the network using multicast.
*
* @author Allan Tong - Initial contribution
* @author Bob Adair - Renamed and added bridge properties
*/
@NonNullByDefault
@Component(service = DiscoveryService.class, immediate = true, configurationPid = "discovery.lutron")
public class LutronMcastBridgeDiscoveryService extends AbstractDiscoveryService {
private static final int SCAN_INTERVAL_MINUTES = 30;
private static final int SCAN_TIMEOUT_MS = 2000;
private static final Set<ThingTypeUID> BRIDGE_TYPE_UID = Collections.singleton(THING_TYPE_IPBRIDGE);
private static final String GROUP_ADDRESS = "224.0.37.42";
private static final byte[] QUERY_DATA = "<LUTRON=1>".getBytes(StandardCharsets.US_ASCII);
private static final int QUERY_DEST_PORT = 2647;
private static final Pattern BRIDGE_PROP_PATTERN = Pattern.compile("<([^=>]+)=([^>]*)>");
private static final String PRODFAM_RA2 = "RadioRA2";
private static final String PRODFAM_HWQS = "Gulliver";
private static final String DEFAULT_LABEL = "RadioRA2 MainRepeater";
private final Logger logger = LoggerFactory.getLogger(LutronMcastBridgeDiscoveryService.class);
private @Nullable ScheduledFuture<?> scanTask;
private @Nullable ScheduledFuture<?> backgroundScan;
public LutronMcastBridgeDiscoveryService() {
super(BRIDGE_TYPE_UID, 5);
}
@Override
protected void startScan() {
this.scanTask = scheduler.schedule(new RepeaterScanner(), 0, TimeUnit.SECONDS);
}
@Override
protected void stopScan() {
super.stopScan();
if (this.scanTask != null) {
this.scanTask.cancel(true);
}
}
@Override
public void abortScan() {
super.abortScan();
if (this.scanTask != null) {
this.scanTask.cancel(true);
}
}
@Override
protected void startBackgroundDiscovery() {
if (this.backgroundScan == null) {
this.backgroundScan = scheduler.scheduleWithFixedDelay(new RepeaterScanner(), 1, SCAN_INTERVAL_MINUTES,
TimeUnit.MINUTES);
}
}
@Override
protected void stopBackgroundDiscovery() {
if (this.backgroundScan != null) {
this.backgroundScan.cancel(true);
this.backgroundScan = null;
}
}
private class RepeaterScanner implements Runnable {
@Override
public void run() {
try {
queryForRepeaters();
} catch (InterruptedException e) {
logger.info("Bridge device scan interrupted");
} catch (IOException e) {
logger.warn("Communication error during bridge scan: {}", e.getMessage());
}
}
private void queryForRepeaters() throws IOException, InterruptedException {
logger.debug("Scanning for Lutron bridge devices using multicast");
InetAddress group = InetAddress.getByName(GROUP_ADDRESS);
try (MulticastSocket socket = new MulticastSocket()) {
socket.setSoTimeout(SCAN_TIMEOUT_MS);
socket.joinGroup(group);
try {
// Try to ensure that joinGroup has taken effect. Without this delay, the query
// packet ends up going out before the group join.
Thread.sleep(1000);
socket.send(new DatagramPacket(QUERY_DATA, QUERY_DATA.length, group, QUERY_DEST_PORT));
byte[] buf = new byte[4096];
DatagramPacket packet = new DatagramPacket(buf, buf.length);
try {
while (!Thread.interrupted()) {
socket.receive(packet);
createBridge(packet);
}
logger.info("Bridge device scan interrupted");
} catch (SocketTimeoutException e) {
logger.trace(
"Timed out waiting for multicast response. Presumably all bridge devices have already responded.");
}
} finally {
socket.leaveGroup(group);
}
}
}
private void createBridge(DatagramPacket packet) {
// Check response for the list of properties reported by the device. At a
// minimum the IP address and serial number are needed in order to create
// the bridge.
String data = new String(packet.getData(), packet.getOffset(), packet.getLength(),
StandardCharsets.US_ASCII);
Matcher matcher = BRIDGE_PROP_PATTERN.matcher(data);
Map<String, @Nullable String> bridgeProperties = new HashMap<>();
while (matcher.find()) {
bridgeProperties.put(matcher.group(1), matcher.group(2));
logger.trace("Bridge property: {} : {}", matcher.group(1), matcher.group(2));
}
String ipAddress = bridgeProperties.get("IPADDR");
String serialNumber = bridgeProperties.get("SERNUM");
String productFamily = bridgeProperties.get("PRODFAM");
String productType = bridgeProperties.get("PRODTYPE");
String codeVersion = bridgeProperties.get("CODEVER");
String macAddress = bridgeProperties.get("MACADDR");
if (ipAddress != null && !ipAddress.trim().isEmpty() && serialNumber != null
&& !serialNumber.trim().isEmpty()) {
Map<String, Object> properties = new HashMap<>();
properties.put(HOST, ipAddress);
properties.put(SERIAL_NUMBER, serialNumber);
if (PRODFAM_RA2.equals(productFamily)) {
properties.put(PROPERTY_PRODFAM, "RadioRA 2");
} else if (PRODFAM_HWQS.equals(productFamily)) {
properties.put(PROPERTY_PRODFAM, "HomeWorks QS");
} else {
if (productFamily != null) {
properties.put(PROPERTY_PRODFAM, productFamily);
}
}
if (productType != null && !productType.trim().isEmpty()) {
properties.put(PROPERTY_PRODTYP, productType);
}
if (codeVersion != null && !codeVersion.trim().isEmpty()) {
properties.put(Thing.PROPERTY_FIRMWARE_VERSION, codeVersion);
}
if (macAddress != null && !macAddress.trim().isEmpty()) {
properties.put(Thing.PROPERTY_MAC_ADDRESS, macAddress);
}
ThingUID uid = new ThingUID(THING_TYPE_IPBRIDGE, serialNumber);
String label = generateLabel(productFamily, productType);
DiscoveryResult result = DiscoveryResultBuilder.create(uid).withLabel(label).withProperties(properties)
.withRepresentationProperty(SERIAL_NUMBER).build();
thingDiscovered(result);
logger.debug("Discovered Lutron bridge device {}", uid);
}
}
private String generateLabel(@Nullable String productFamily, @Nullable String productType) {
if (productFamily != null && !productFamily.trim().isEmpty() && productType != null
&& !productType.trim().isEmpty()) {
return productFamily + " " + productType;
}
return DEFAULT_LABEL;
}
}
}

View File

@@ -0,0 +1,196 @@
/**
* 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.net.InetAddress;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.jmdns.ServiceInfo;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.config.discovery.DiscoveryResult;
import org.openhab.core.config.discovery.DiscoveryResultBuilder;
import org.openhab.core.config.discovery.mdns.MDNSDiscoveryParticipant;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.ThingUID;
import org.osgi.service.component.annotations.Component;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link LutronMdnsBridgeDiscoveryService} discovers Lutron Caseta Smart Bridge Pro and eventually RA2 Select Main
* Repeater and other Lutron devices on the network using mDNS.
*
* @author Bob Adair - Initial contribution
*/
@Component(immediate = true)
@NonNullByDefault
public class LutronMdnsBridgeDiscoveryService implements MDNSDiscoveryParticipant {
// Lutron mDNS service <app>.<protocol>.<servicedomain>
private static final String LUTRON_MDNS_SERVICE_TYPE = "_lutron._tcp.local.";
private static final String PRODFAM_CASETA = "Caseta";
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";
private static final Pattern HOSTNAME_REGEX = Pattern.compile("lutron-([0-9a-f]+)\\."); // ex: lutron-01f1529a.local
private final Logger logger = LoggerFactory.getLogger(LutronMdnsBridgeDiscoveryService.class);
@Override
public Set<ThingTypeUID> getSupportedThingTypeUIDs() {
return Collections.singleton(THING_TYPE_IPBRIDGE);
}
@Override
public String getServiceType() {
return LUTRON_MDNS_SERVICE_TYPE;
}
@Override
public @Nullable DiscoveryResult createResult(ServiceInfo service) {
if (!service.hasData()) {
return null;
}
String nice = service.getNiceTextString();
String qualifiedName = service.getQualifiedName();
InetAddress[] ipAddresses = service.getInetAddresses();
String devclass = service.getPropertyString("DEVCLASS");
String codever = service.getPropertyString("CODEVER");
String macaddr = service.getPropertyString("MACADDR");
logger.debug("Lutron mDNS bridge discovery notified of Lutron mDNS service: {}", nice);
logger.trace("Lutron mDNS service qualifiedName: {}", qualifiedName);
logger.trace("Lutron mDNS service ipAddresses: {} ({})", ipAddresses, ipAddresses.length);
logger.trace("Lutron mDNS service property DEVCLASS: {}", devclass);
logger.trace("Lutron mDNS service property CODEVER: {}", codever);
logger.trace("Lutron mDNS service property MACADDR: {}", macaddr);
Map<String, Object> properties = new HashMap<>();
String label = DEFAULT_LABEL;
if (ipAddresses.length < 1) {
return null;
}
if (ipAddresses.length > 1) {
logger.debug("Multiple addresses found for discovered Lutron device. Using only the first.");
}
properties.put(HOST, ipAddresses[0].getHostAddress());
String bridgeHostName = ipAddresses[0].getHostName();
logger.debug("Lutron mDNS bridge hostname: {}", bridgeHostName);
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;
} else if (DEVCLASS_RA2_SELECT.equals(devclass)) {
properties.put(PROPERTY_PRODFAM, PRODFAM_RA2_SELECT);
properties.put(PROPERTY_PRODTYP, PRODTYP_RA2_SELECT);
label = PRODFAM_RA2_SELECT + " " + PRODTYP_RA2_SELECT;
} else if (DEVCLASS_CONNECT_BRIDGE.equals(devclass)) {
logger.debug("Lutron Connect Bridge discovered. Ignoring.");
return null;
} 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
}
if (!bridgeHostName.equals(ipAddresses[0].getHostAddress())) {
label = label + " " + bridgeHostName;
}
if (codever != null) {
properties.put(Thing.PROPERTY_FIRMWARE_VERSION, codever);
}
if (macaddr != null) {
properties.put(Thing.PROPERTY_MAC_ADDRESS, macaddr);
}
String sn = getSerial(service);
if (sn != null) {
logger.trace("Lutron mDNS bridge serial number: {}", sn);
properties.put(SERIAL_NUMBER, sn);
} else {
logger.debug("Unable to determine serial number of discovered Lutron bridge device.");
return null;
}
ThingUID uid = getThingUID(service);
if (uid != null) {
DiscoveryResult result = DiscoveryResultBuilder.create(uid).withLabel(label).withProperties(properties)
.withRepresentationProperty(SERIAL_NUMBER).build();
logger.debug("Discovered Lutron bridge device via mDNS {}", uid);
return result;
} else {
logger.trace("Failed to create uid for discovered Lutron bridge device");
return null;
}
}
@Override
public @Nullable ThingUID getThingUID(ServiceInfo service) {
String serial = getSerial(service);
if (serial == null) {
return null;
} else {
return new ThingUID(THING_TYPE_IPBRIDGE, serial);
}
}
/**
* Returns the device serial number for the mDNS service by extracting it from the hostname.
* Used as unique thing representation property.
*
* @param service Lutron mDNS service
* @return String containing serial number, or null if it cannot be determined
*/
private @Nullable String getSerial(ServiceInfo service) {
InetAddress[] ipAddresses = service.getInetAddresses();
if (ipAddresses.length < 1) {
return null;
}
Matcher matcher = HOSTNAME_REGEX.matcher(ipAddresses[0].getHostName());
boolean matched = matcher.find();
String serialnum = null;
if (matched) {
serialnum = matcher.group(1);
}
if (matched && serialnum != null && !serialnum.isEmpty()) {
return serialnum;
} else {
return null;
}
}
}

View File

@@ -0,0 +1,46 @@
/**
* 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.project;
import java.util.Collections;
import java.util.List;
/**
* This class represents a location defined in the Lutron system. Areas are organized
* hierarchically and can represent an entire house, a room in the house, or a specific
* location within a room.
*
* @author Allan Tong - Initial contribution
*/
public class Area {
private String name;
private List<DeviceNode> deviceNodes;
private List<Output> outputs;
private List<Area> areas;
public String getName() {
return name;
}
public List<DeviceNode> getDeviceNodes() {
return deviceNodes != null ? deviceNodes : Collections.<DeviceNode> emptyList();
}
public List<Output> getOutputs() {
return outputs != null ? outputs : Collections.<Output> emptyList();
}
public List<Area> getAreas() {
return areas != null ? areas : Collections.<Area> emptyList();
}
}

View File

@@ -0,0 +1,36 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.lutron.internal.discovery.project;
/**
* A component of an input device in a Lutron system. Generally each component of
* the device maps to a channel of the device thing.
*
* @author Allan Tong - Initial contribution
*/
public class Component {
private Integer componentNumber;
private String type;
public Integer getComponentNumber() {
return componentNumber;
}
public ComponentType getComponentType() {
try {
return ComponentType.valueOf(this.type);
} catch (Exception e) {
return null;
}
}
}

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.discovery.project;
/**
* Component type in a Lutron device.
*
* @author Allan Tong - Initial contribution
*/
public enum ComponentType {
BUTTON,
CCI,
LED,
SCENE_CONTROLLER
}

View File

@@ -0,0 +1,52 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.lutron.internal.discovery.project;
import java.util.Collections;
import java.util.List;
/**
* An input device in a Lutron system such as a keypad or occupancy sensor.
*
* @author Allan Tong - Initial contribution
*/
public class Device implements DeviceNode {
private String name;
private Integer integrationId;
private String type;
private List<Component> components;
public String getName() {
return name;
}
public Integer getIntegrationId() {
return integrationId;
}
public String getType() {
return type;
}
public DeviceType getDeviceType() {
try {
return DeviceType.valueOf(this.type);
} catch (Exception e) {
return null;
}
}
public List<Component> getComponents() {
return components != null ? components : Collections.<Component> emptyList();
}
}

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.discovery.project;
import java.util.Collections;
import java.util.List;
/**
* A group of input devices in the Lutron system.
*
* @author Allan Tong - Initial contribution
*/
public class DeviceGroup implements DeviceNode {
private String name;
private List<Device> devices;
public String getName() {
return name;
}
public List<Device> getDevices() {
return devices != null ? devices : Collections.<Device> emptyList();
}
}

View File

@@ -0,0 +1,22 @@
/**
* 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.project;
/**
* Marker interface representing either an input device or a group of input
* devices.
*
* @author Allan Tong - Initial contribution
*/
public interface DeviceNode {
}

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.discovery.project;
/**
* Type of input device in a Lutron system.
*
* @author Allan Tong - Initial contribution
*/
public enum DeviceType {
GRAFIK_EYE_QS,
HYBRID_SEETOUCH_KEYPAD,
INTERNATIONAL_SEETOUCH_KEYPAD,
MAIN_REPEATER,
MOTION_SENSOR,
PALLADIOM_KEYPAD,
PICO_KEYPAD,
QS_IO_INTERFACE,
SEETOUCH_KEYPAD,
SEETOUCH_TABLETOP_KEYPAD,
VISOR_CONTROL_RECEIVER,
WCI
}

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.discovery.project;
/**
* A Green Mode subsystem in a Lutron RadioRA2 controller
*
* @author Bob Adair - Initial contribution
*/
public class GreenMode {
private String name;
private Integer integrationId;
public String getName() {
// There may be no name in the XML document
return name != null ? name : "Green Mode Subsystem";
}
public Integer getIntegrationId() {
return integrationId;
}
}

View File

@@ -0,0 +1,44 @@
/**
* 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.project;
/**
* An output device in a Lutron system such as a switch or dimmer.
*
* @author Allan Tong - Initial contribution
*/
public class Output {
private String name;
private Integer integrationId;
private String type;
public String getName() {
return name;
}
public Integer getIntegrationId() {
return integrationId;
}
public String getType() {
return type;
}
public OutputType getOutputType() {
try {
return OutputType.valueOf(this.type);
} catch (Exception e) {
return null;
}
}
}

View File

@@ -0,0 +1,41 @@
/**
* 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.project;
/**
* Type of output device in a Lutron system.
*
* @author Allan Tong - Initial contribution
* @author Bob Adair - Added additional output types
*/
public enum OutputType {
AUTO_DETECT,
CCO_MAINTAINED,
CCO_PULSED,
CEILING_FAN_TYPE,
DALI,
ECO_SYSTEM_FLUORESCENT,
ELV,
FLUORESCENT_DB,
INC,
MLV,
MOTOR,
NON_DIM,
NON_DIM_ELV,
NON_DIM_INC,
RELAY_LIGHTING,
SHEER_BLIND,
SYSTEM_SHADE,
VENETIAN_BLIND,
ZERO_TO_TEN,
}

View File

@@ -0,0 +1,51 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.lutron.internal.discovery.project;
import java.util.Collections;
import java.util.List;
/**
* This class represents a Lutron system and the topology of device things within
* that system.
*
* @author Allan Tong - Initial contribution
* @author Bob Adair - Added Timeclock and Green Mode support
*/
public class Project {
private String appVersion;
private String xmlVersion;
private List<Area> areas;
private List<Timeclock> timeclocks;
private List<GreenMode> greenmodes;
public String getAppVersion() {
return appVersion;
}
public String getXmlVersion() {
return xmlVersion;
}
public List<Area> getAreas() {
return areas != null ? areas : Collections.<Area> emptyList();
}
public List<Timeclock> getTimeclocks() {
return timeclocks != null ? timeclocks : Collections.<Timeclock> emptyList();
}
public List<GreenMode> getGreenModes() {
return greenmodes != null ? greenmodes : Collections.<GreenMode> emptyList();
}
}

View File

@@ -0,0 +1,32 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.lutron.internal.discovery.project;
/**
* A Timeclock subsystem in a Lutron RadioRA2 or HWQS controller
*
* @author Bob Adair - Initial contribution
*/
public class Timeclock {
private String name;
private Integer integrationId;
public String getName() {
return name;
}
public Integer getIntegrationId() {
return integrationId;
}
}

View File

@@ -0,0 +1,178 @@
/**
* 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.grxprg;
/**
* Configuration class for the Grafik Eye controlled by the PRG interface
*
* @author Tim Roberts - Initial contribution
*/
public class GrafikEyeConfig {
/**
* The control unit identifier
*/
private int controlUnit;
/**
* The default fade for the unit
*/
private int fade;
/**
* The zones designated as shades as parsed
*/
private boolean[] shades = new boolean[8];
/**
* A string representing if the shade configuration was invalid. Will be null if valid, not-null if invalid
*/
private String shadeError;
/**
* Polling time (in seconds) to refresh state for the unit.
*/
private int polling;
/**
* Validates the configuration. Ensures the control unit. fade and shadeError are valid.
*
* @return a non-null text if invalid (explaining why), a null if valid
*/
String validate() {
if (controlUnit < 1 || controlUnit > 8) {
return "controlUnit must be between 1-8";
}
if (fade < 0 || fade > 3600) {
return "fade must be between 0-3600";
}
if (shadeError != null) {
return shadeError;
}
return null;
}
/**
* Returns the Control Unit identifier
*
* @return the control unit identifier
*/
public int getControlUnit() {
return controlUnit;
}
/**
* Sets the control unit identifier
*
* @param controlUnit the control unit identifier
*/
public void setControlUnit(int controlUnit) {
this.controlUnit = controlUnit;
}
/**
* Returns the default fade
*
* @return the default fade
*/
public int getFade() {
return fade;
}
/**
* Sets the default fade
*
* @param fade the default fade
*/
public void setFade(int fade) {
this.fade = fade;
}
/**
* Helper method to determine if the zone is a shade zone or not. If zone number is invalid, false will be returned.
*
* @param zone the zone number
* @return true if designated as a shade, false otherwise
*/
boolean isShadeZone(int zone) {
if (zone >= 1 && zone <= shades.length) {
return shades[zone - 1];
}
return false;
}
/**
* Returns a comma formatted list of shade zones
*
* @returna non-null, non-empty comma delimited list of shade zones
*/
public String getShadeZones() {
final StringBuilder sb = new StringBuilder();
for (int z = 0; z < shades.length; z++) {
if (shades[z]) {
if (sb.length() > 0) {
sb.append(',');
}
sb.append((z + 1));
}
}
return sb.toString();
}
/**
* Sets the shade zones from a comma delimited list (ex: "2,3,4")
*
* @param shadeZones a, possibly null, list of zones
*/
public void setShadeZones(String shadeZones) {
shadeError = null;
for (int zone = 0; zone < 8; zone++) {
shades[zone] = false;
}
if (shadeZones != null) {
for (String shadeZone : shadeZones.split(",")) {
try {
final int zone = Integer.parseInt(shadeZone);
if (zone >= 1 && zone <= 8) {
shades[zone - 1] = true;
} else {
shadeError = "Shade zone must be between 1-8: " + zone + " - ignoring";
}
} catch (NumberFormatException e) {
shadeError = "Unknown shade zone (can't parse to numeric): " + shadeZone + " - ignoring";
}
}
}
}
/**
* Gets the polling (in seconds) to refresh state
*
* @return the polling (in seconds) to refresh state
*/
public int getPolling() {
return polling;
}
/**
* Sets the polling (in seconds) to refresh state
*
* @param polling the polling (in seconds) to refresh state
*/
public void setPolling(int polling) {
this.polling = polling;
}
}

View File

@@ -0,0 +1,388 @@
/**
* 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.grxprg;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import org.apache.commons.lang.NullArgumentException;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.IncreaseDecreaseType;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.PercentType;
import org.openhab.core.library.types.StopMoveType;
import org.openhab.core.library.types.UpDownType;
import org.openhab.core.thing.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.BaseThingHandler;
import org.openhab.core.thing.binding.ThingHandler;
import org.openhab.core.types.Command;
import org.openhab.core.types.RefreshType;
import org.openhab.core.types.State;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link BaseThingHandler} is responsible for handling a specific grafik eye unit (identified by it's control
* number). This handler is responsible for handling the commands and management for a single grafik eye unit.
*
* @author Tim Roberts - Initial contribution
*/
public class GrafikEyeHandler extends BaseThingHandler {
/**
* Logger used by this class
*/
private Logger logger = LoggerFactory.getLogger(GrafikEyeHandler.class);
/**
* Cached instance of the {@link GrafikEyeConfig}. Will be null if disconnected.
*/
private GrafikEyeConfig config = null;
/**
* The current fade for the grafik eye (only used when setting zone intensity). Will initially be set from
* configuration.
*/
private int fade = 0;
/**
* The polling job to poll the actual state of the grafik eye
*/
private ScheduledFuture<?> pollingJob;
/**
* Constructs the handler from the {@link org.openhab.core.thing.Thing}
*
* @param thing a non-null {@link org.openhab.core.thing.Thing} the handler is for
*/
public GrafikEyeHandler(Thing thing) {
super(thing);
if (thing == null) {
throw new IllegalArgumentException("thing cannot be null");
}
}
/**
* {@inheritDoc}
*
* Handles commands to specific channels. This implementation will offload much of its work to the
* {@link PrgProtocolHandler}. Basically we validate the type of command for the channel then call the
* {@link PrgProtocolHandler} to handle the actual protocol. Special use case is the {@link RefreshType}
* where we call {{@link #handleRefresh(String)} to handle a refresh of the specific channel (which in turn calls
* {@link PrgProtocolHandler} to handle the actual refresh
*/
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
if (command instanceof RefreshType) {
handleRefresh(channelUID.getId());
return;
}
if (getThing().getStatus() != ThingStatus.ONLINE) {
// Ignore any command if not online
return;
}
String id = channelUID.getId();
if (id == null) {
logger.warn("Called with a null channel id - ignoring");
return;
}
if (id.equals(PrgConstants.CHANNEL_SCENE)) {
if (command instanceof DecimalType) {
final int scene = ((DecimalType) command).intValue();
getProtocolHandler().selectScene(config.getControlUnit(), scene);
} else {
logger.error("Received a SCENE command with a non DecimalType: {}", command);
}
} else if (id.equals(PrgConstants.CHANNEL_SCENELOCK)) {
if (command instanceof OnOffType) {
getProtocolHandler().setSceneLock(config.getControlUnit(), command == OnOffType.ON);
} else {
logger.error("Received a SCENELOCK command with a non OnOffType: {}", command);
}
} else if (id.equals(PrgConstants.CHANNEL_SCENESEQ)) {
if (command instanceof OnOffType) {
getProtocolHandler().setSceneSequence(config.getControlUnit(), command == OnOffType.ON);
} else {
logger.error("Received a SCENESEQ command with a non OnOffType: {}", command);
}
} else if (id.equals(PrgConstants.CHANNEL_ZONELOCK)) {
if (command instanceof OnOffType) {
getProtocolHandler().setZoneLock(config.getControlUnit(), command == OnOffType.ON);
} else {
logger.error("Received a ZONELOCK command with a non OnOffType: {}", command);
}
} else if (id.startsWith(PrgConstants.CHANNEL_ZONELOWER)) {
final Integer zone = getTrailingNbr(id, PrgConstants.CHANNEL_ZONELOWER);
if (zone != null) {
getProtocolHandler().setZoneLower(config.getControlUnit(), zone);
}
} else if (id.startsWith(PrgConstants.CHANNEL_ZONERAISE)) {
final Integer zone = getTrailingNbr(id, PrgConstants.CHANNEL_ZONERAISE);
if (zone != null) {
getProtocolHandler().setZoneRaise(config.getControlUnit(), zone);
}
} else if (id.equals(PrgConstants.CHANNEL_ZONEFADE)) {
if (command instanceof DecimalType) {
setFade(((DecimalType) command).intValue());
} else {
logger.error("Received a ZONEFADE command with a non DecimalType: {}", command);
}
} else if (id.startsWith(PrgConstants.CHANNEL_ZONEINTENSITY)) {
final Integer zone = getTrailingNbr(id, PrgConstants.CHANNEL_ZONEINTENSITY);
if (zone != null) {
if (command instanceof PercentType) {
final int intensity = ((PercentType) command).intValue();
getProtocolHandler().setZoneIntensity(config.getControlUnit(), zone, fade, intensity);
} else if (command instanceof OnOffType) {
getProtocolHandler().setZoneIntensity(config.getControlUnit(), zone, fade,
command == OnOffType.ON ? 100 : 0);
} else if (command instanceof IncreaseDecreaseType) {
getProtocolHandler().setZoneIntensity(config.getControlUnit(), zone, fade,
command == IncreaseDecreaseType.INCREASE);
} else {
logger.error("Received a ZONEINTENSITY command with a non DecimalType: {}", command);
}
}
} else if (id.startsWith(PrgConstants.CHANNEL_ZONESHADE)) {
final Integer zone = getTrailingNbr(id, PrgConstants.CHANNEL_ZONESHADE);
if (zone != null) {
if (command instanceof PercentType) {
logger.info("PercentType is not suppored by QED shades");
} else if (command == StopMoveType.MOVE) {
logger.info("StopMoveType.Move is not suppored by QED shades");
} else if (command == StopMoveType.STOP) {
getProtocolHandler().setZoneIntensity(config.getControlUnit(), zone, fade, 0);
} else if (command instanceof UpDownType) {
getProtocolHandler().setZoneIntensity(config.getControlUnit(), zone, fade,
command == UpDownType.UP ? 1 : 2);
} else {
logger.error("Received a ZONEINTENSITY command with a non DecimalType: {}", command);
}
}
} else {
logger.error("Unknown/Unsupported Channel id: {}", id);
}
}
/**
* Method that handles the {@link RefreshType} command specifically. Calls the {@link PrgProtocolHandler} to
* handle the actual refresh based on the channel id.
*
* @param id a non-null, possibly empty channel id to refresh
*/
private void handleRefresh(String id) {
if (getThing().getStatus() != ThingStatus.ONLINE) {
return;
}
if (id.equals(PrgConstants.CHANNEL_SCENE)) {
getProtocolHandler().refreshScene();
} else if (id.equals(PrgConstants.CHANNEL_ZONEINTENSITY)) {
getProtocolHandler().refreshZoneIntensity(config.getControlUnit());
} else if (id.equals(PrgConstants.CHANNEL_ZONEFADE)) {
updateState(PrgConstants.CHANNEL_ZONEFADE, new DecimalType(fade));
}
}
/**
* Gets the trailing number from the channel id (which usually represents the zone number).
*
* @param id a non-null, possibly empty channel id
* @param channelConstant a non-null, non-empty channel id constant to use in the parse.
* @return the trailing number or null if a parse exception occurs
*/
private Integer getTrailingNbr(String id, String channelConstant) {
try {
return Integer.parseInt(id.substring(channelConstant.length()));
} catch (NumberFormatException e) {
logger.warn("Unknown channel port #: {}", id);
return null;
}
}
/**
* Initializes the thing - basically calls {@link #internalInitialize()} to do the work
*/
@Override
public void initialize() {
cancelPolling();
internalInitialize();
}
/**
* Set's the unit to offline and attempts to reinitialize via {@link #internalInitialize()}
*/
@Override
public void thingUpdated(Thing thing) {
cancelPolling();
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.HANDLER_CONFIGURATION_PENDING);
this.thing = thing;
internalInitialize();
}
/**
* If the bridge goes offline, cancels the polling and goes offline. If the bridge goes online, will attempt to
* re-initialize via {@link #internalInitialize()}
*/
@Override
public void bridgeStatusChanged(ThingStatusInfo bridgeStatusInfo) {
cancelPolling();
if (bridgeStatusInfo.getStatus() == ThingStatus.ONLINE) {
internalInitialize();
} else {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
}
}
/**
* Initializes the grafik eye. Essentially validates the {@link GrafikEyeConfig}, updates the status to online and
* starts a status refresh job
*/
private void internalInitialize() {
config = getThing().getConfiguration().as(GrafikEyeConfig.class);
if (config == null) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Configuration file missing");
return;
}
final String configErr = config.validate();
if (configErr != null) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, configErr);
return;
}
final Bridge bridge = getBridge();
if (bridge == null || !(bridge.getHandler() instanceof PrgBridgeHandler)) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
"GrafikEye must have a parent PRG Bridge");
return;
}
final ThingHandler handler = bridge.getHandler();
if (handler.getThing().getStatus() != ThingStatus.ONLINE) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
return;
}
updateStatus(ThingStatus.ONLINE);
setFade(config.getFade());
cancelPolling();
pollingJob = this.scheduler.scheduleWithFixedDelay(new Runnable() {
@Override
public void run() {
final ThingStatus status = getThing().getStatus();
if (status == ThingStatus.ONLINE && config != null) {
getProtocolHandler().refreshState(config.getControlUnit());
}
}
}, 1, config.getPolling(), TimeUnit.SECONDS);
}
/**
* Helper method to cancel our polling if we are currently polling
*/
private void cancelPolling() {
if (pollingJob != null) {
pollingJob.cancel(true);
pollingJob = null;
}
}
/**
* Returns the {@link PrgProtocolHandler} to use
*
* @return a non-null {@link PrgProtocolHandler} to use
*/
private PrgProtocolHandler getProtocolHandler() {
final Bridge bridge = getBridge();
if (bridge == null || !(bridge.getHandler() instanceof PrgBridgeHandler)) {
throw new NullArgumentException("Cannot have a Grafix Eye thing outside of the PRG bridge");
}
final PrgProtocolHandler handler = ((PrgBridgeHandler) bridge.getHandler()).getProtocolHandler();
if (handler == null) {
throw new NullArgumentException("No protocol handler set in the PrgBridgeHandler!");
}
return handler;
}
/**
* Returns the control unit for this handler
*
* @return the control unit
*/
int getControlUnit() {
return config.getControlUnit();
}
/**
* Helper method to determine if the given zone is a shade. Off loads it's work to
* {@link GrafikEyeConfig#isShadeZone(int)}
*
* @param zone a zone to check
* @return true if a shade zone, false otherwise
*/
boolean isShade(int zone) {
return config == null ? false : config.isShadeZone(zone);
}
/**
* Helper method to expose the ability to change state outside of the class
*
* @param channelId the channel id
* @param state the new state
*/
void stateChanged(String channelId, State state) {
updateState(channelId, state);
}
/**
* Helper method to set the fade level. Will store the fade and update its state.
*
* @param fade the new fade
*/
private void setFade(int fade) {
if (fade < 0 || fade > 3600) {
throw new IllegalArgumentException("fade must be between 1-3600");
}
this.fade = fade;
updateState(PrgConstants.CHANNEL_ZONEFADE, new DecimalType(this.fade));
}
}

View File

@@ -0,0 +1,90 @@
/**
* 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.grxprg;
/**
* Configuration class for the GRX-PRG/GRX-CI-PRG bridge
*
* @author Tim Roberts - Initial contribution
*/
public class PrgBridgeConfig {
/**
* IP Address (or host name) of switch
*/
private String ipAddress;
/**
* The username to log in with
*/
private String userName;
/**
* Polling time (in seconds) to attempt a reconnect if the socket session has failed
*/
private int retryPolling;
/**
* Returns the IP address or host name of the switch
*
* @return the IP address or host name of the swtich
*/
public String getIpAddress() {
return ipAddress;
}
/**
* Sets the IP address or host name of the switch
*
* @param ipAddress the IP Address or host name of the switch
*/
public void setIpAddress(String ipAddress) {
this.ipAddress = ipAddress;
}
/**
* Gets the username used to login with
*
* @return the username used to login with
*/
public String getUserName() {
return userName;
}
/**
* Sets the username used to login with
*
* @param userName the username used to login with
*/
public void setUserName(String userName) {
this.userName = userName;
}
/**
* Gets the polling (in seconds) to reconnect
*
* @return the polling (in seconds) to reconnect
*/
public int getRetryPolling() {
return retryPolling;
}
/**
* Sets the polling (in seconds) to reconnect
*
* @param retryPolling the polling (in seconds to reconnect)
*/
public void setRetryPolling(int retryPolling) {
this.retryPolling = retryPolling;
}
}

View File

@@ -0,0 +1,354 @@
/**
* 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.grxprg;
import java.io.IOException;
import java.time.ZonedDateTime;
import java.util.GregorianCalendar;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import org.openhab.core.library.types.DateTimeType;
import org.openhab.core.library.types.DecimalType;
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.types.Command;
import org.openhab.core.types.RefreshType;
import org.openhab.core.types.State;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link PrgBridgeHandler} is responsible for handling all bridge interactions. This includes management of the
* connection and processing of any commands (thru the {@link PrgProtocolHandler}).
*
* @author Tim Roberts - Initial contribution
*/
public class PrgBridgeHandler extends BaseBridgeHandler {
private Logger logger = LoggerFactory.getLogger(PrgBridgeHandler.class);
/**
* The {@link PrgProtocolHandler} that handles the actual protocol. Will never be null
*/
private PrgProtocolHandler protocolHandler;
/**
* The {@link SocketSession} to the physical devices. Will never be null
*/
private SocketSession session;
/**
* The retry connection event. Null if not retrying.
*/
private ScheduledFuture<?> retryConnectionJob;
/**
* Constructs the handler from the {@link Bridge}. Simply calls the super constructor with the {@link Bridge},
* creates the session (unconnected) and the protocol handler.
*
* @param bridge a non-null {@link Bridge} the handler is for
*/
public PrgBridgeHandler(Bridge bridge) {
super(bridge);
if (bridge == null) {
throw new IllegalArgumentException("thing cannot be null");
}
final PrgBridgeConfig config = getPrgBridgeConfig();
session = new SocketSession(config.getIpAddress(), 23);
protocolHandler = new PrgProtocolHandler(session, new PrgHandlerCallback() {
@Override
public void stateChanged(String channelId, State state) {
updateState(channelId, state);
}
@Override
public void statusChanged(ThingStatus status, ThingStatusDetail detail, String msg) {
updateStatus(status, detail, msg);
if (status != ThingStatus.ONLINE) {
disconnect(true);
}
}
@Override
public void stateChanged(int controlUnit, String channelId, State state) {
getGrafikEyeHandler(controlUnit).stateChanged(channelId, state);
}
@Override
public boolean isShade(int controlUnit, int zone) {
return getGrafikEyeHandler(controlUnit).isShade(zone);
}
});
}
/**
* Internal method to retrieve the {@link PrgProtocolHandler} used by the bridge
*
* @return a non-null protocol handler to use
*/
PrgProtocolHandler getProtocolHandler() {
return protocolHandler;
}
/**
* Helper method used to retrieve the {@link GrafikEyeHandler} for a given control unit number. If not found, an
* IllegalArgumentException will be thrown.
*
* @param controlUnit a control number to retrieve
* @return a non-null {@link GrafikEyeHandler}
* @throws IllegalArgumentException if the {@link GrafikEyeHandler} for the given controlUnit was not found.
*/
private GrafikEyeHandler getGrafikEyeHandler(int controlUnit) {
for (Thing thing : getThing().getThings()) {
if (thing.getHandler() instanceof GrafikEyeHandler) {
final GrafikEyeHandler handler = (GrafikEyeHandler) thing.getHandler();
if (handler.getControlUnit() == controlUnit) {
return handler;
}
} else {
logger.warn("Should not be a non-GrafikEyeHandler as a thing to this bridge - ignoring: {}", thing);
}
}
throw new IllegalArgumentException("Could not find a GrafikEyeHandler for control unit : " + controlUnit);
}
/**
* {@inheritDoc}
*
* Handles commands to specific channels. This implementation will offload much of its work to the
* {@link PrgProtocolHandler}. Basically we validate the type of command for the channel then call the
* {@link PrgProtocolHandler} to handle the actual protocol. Special use case is the {@link RefreshType}
* where we call {{@link #handleRefresh(String)} to handle a refresh of the specific channel (which in turn calls
* {@link PrgProtocolHandler} to handle the actual refresh
*/
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
if (command instanceof RefreshType) {
handleRefresh(channelUID.getId());
return;
}
// if (getThing().getStatus() != ThingStatus.ONLINE) {
// // Ignore any command if not online
// return;
// }
String id = channelUID.getId();
if (id == null) {
logger.warn("Called with a null channel id - ignoring");
return;
}
if (id.equals(PrgConstants.CHANNEL_ZONELOWERSTOP)) {
protocolHandler.setZoneLowerStop();
} else if (id.equals(PrgConstants.CHANNEL_ZONERAISESTOP)) {
protocolHandler.setZoneRaiseStop();
} else if (id.equals(PrgConstants.CHANNEL_TIMECLOCK)) {
if (command instanceof DateTimeType) {
final ZonedDateTime zdt = ((DateTimeType) command).getZonedDateTime();
protocolHandler.setTime(GregorianCalendar.from(zdt));
} else {
logger.error("Received a TIMECLOCK channel command with a non DateTimeType: {}", command);
}
} else if (id.startsWith(PrgConstants.CHANNEL_SCHEDULE)) {
if (command instanceof DecimalType) {
final int schedule = ((DecimalType) command).intValue();
protocolHandler.selectSchedule(schedule);
} else {
logger.error("Received a SCHEDULE channel command with a non DecimalType: {}", command);
}
} else if (id.startsWith(PrgConstants.CHANNEL_SUPERSEQUENCESTART)) {
protocolHandler.startSuperSequence();
} else if (id.startsWith(PrgConstants.CHANNEL_SUPERSEQUENCEPAUSE)) {
protocolHandler.pauseSuperSequence();
} else if (id.startsWith(PrgConstants.CHANNEL_SUPERSEQUENCERESUME)) {
protocolHandler.resumeSuperSequence();
} else {
logger.error("Unknown/Unsupported Channel id: {}", id);
}
}
/**
* Method that handles the {@link RefreshType} command specifically. Calls the {@link PrgProtocolHandler} to
* handle the actual refresh based on the channel id.
*
* @param id a non-null, possibly empty channel id to refresh
*/
private void handleRefresh(String id) {
if (getThing().getStatus() != ThingStatus.ONLINE) {
return;
}
if (id.equals(PrgConstants.CHANNEL_TIMECLOCK)) {
protocolHandler.refreshTime();
} else if (id.equals(PrgConstants.CHANNEL_SCHEDULE)) {
protocolHandler.refreshSchedule();
} else if (id.equals(PrgConstants.CHANNEL_SUNRISE)) {
protocolHandler.refreshSunriseSunset();
} else if (id.equals(PrgConstants.CHANNEL_SUNSET)) {
protocolHandler.refreshSunriseSunset();
} else if (id.equals(PrgConstants.CHANNEL_SUPERSEQUENCESTATUS)) {
protocolHandler.reportSuperSequenceStatus();
} else if (id.equals(PrgConstants.CHANNEL_SUPERSEQUENCENEXTSTEP)) {
protocolHandler.reportSuperSequenceStatus();
} else if (id.equals(PrgConstants.CHANNEL_SUPERSEQUENCENEXTMIN)) {
protocolHandler.reportSuperSequenceStatus();
} else if (id.equals(PrgConstants.CHANNEL_SUPERSEQUENCENEXTSEC)) {
protocolHandler.reportSuperSequenceStatus();
}
}
/**
* {@inheritDoc}
*
* Initializes the handler. This initialization will read/validate the configuration and will attempt to connect to
* the switch (via {{@link #retryConnect()}.
*/
@Override
public void initialize() {
final PrgBridgeConfig config = getPrgBridgeConfig();
if (config == null) {
return;
}
if (config.getIpAddress() == null || config.getIpAddress().trim().length() == 0) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
"IP Address/Host Name of GRX-PRG/GRX-CI-PRG is missing from configuration");
return;
}
// Try initial connection in a scheduled task
this.scheduler.schedule(new Runnable() {
@Override
public void run() {
connect();
}
}, 1, TimeUnit.SECONDS);
}
/**
* Attempts to connect to the PRG unit. If successfully connect, the {@link PrgProtocolHandler#login()} will be
* called to log into the unit. If a connection cannot be established (or login failed), the connection attempt will
* be retried later (via {@link #retryConnect()})
*/
private void connect() {
final PrgBridgeConfig config = getPrgBridgeConfig();
String response = "Server is offline - will try to reconnect later";
try {
logger.info("Attempting connection ...");
session.connect();
response = protocolHandler.login(config.getUserName());
if (response == null) {
if (config != null) {
updateStatus(ThingStatus.ONLINE);
return;
}
}
} catch (Exception e) {
logger.error("Exception during connection attempt", e);
}
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, response);
retryConnect();
}
/**
* Attempts to disconnect from the session and will optionally retry the connection attempt.
*
* @param retryConnection true to retry connection attempts after the disconnect
*/
private void disconnect(boolean retryConnection) {
try {
session.disconnect();
} catch (IOException e) {
// ignore - we don't care
}
if (retryConnection) {
retryConnect();
}
}
/**
* Retries the connection attempt - schedules a job in {@link PrgBridgeConfig#getRetryPolling()} seconds to
* call the {@link #connect()} method. If a retry attempt is pending, the request is ignored.
*/
private void retryConnect() {
if (retryConnectionJob == null) {
final PrgBridgeConfig config = getPrgBridgeConfig();
if (config != null) {
logger.info("Will try to reconnect in {} seconds", config.getRetryPolling());
retryConnectionJob = this.scheduler.schedule(new Runnable() {
@Override
public void run() {
retryConnectionJob = null;
connect();
}
}, config.getRetryPolling(), TimeUnit.SECONDS);
}
} else {
logger.debug("RetryConnection called when a retry connection is pending - ignoring request");
}
}
/**
* Simplu gets the {@link PrgBridgeConfig} from the {@link Thing} and will set the status to offline if not
* found.
*
* @return a possible null {@link PrgBridgeConfig}
*/
private PrgBridgeConfig getPrgBridgeConfig() {
final PrgBridgeConfig config = getThing().getConfiguration().as(PrgBridgeConfig.class);
if (config == null) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Configuration file missing");
}
return config;
}
/**
* {@inheritDoc}
*
* Disposes of the handler. Will simply call {@link #disconnect(boolean)} to disconnect and NOT retry the
* connection
*/
@Override
public void dispose() {
disconnect(false);
}
}

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.grxprg;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.lutron.internal.LutronBindingConstants;
import org.openhab.core.thing.ThingTypeUID;
/**
* Defines common constants, which are used across the whole binding.
*
* @author Tim Roberts - Initial contribution
*/
@NonNullByDefault
public class PrgConstants {
public static final ThingTypeUID THING_TYPE_PRGBRIDGE = new ThingTypeUID(LutronBindingConstants.BINDING_ID,
"prgbridge");
public static final ThingTypeUID THING_TYPE_GRAFIKEYE = new ThingTypeUID(LutronBindingConstants.BINDING_ID,
"grafikeye");
// Channels for the PRG Interface
static final String CHANNEL_BUTTONPRESS = "buttonpress";
static final String CHANNEL_ZONELOWERSTOP = "zonelowerstop";
static final String CHANNEL_ZONERAISESTOP = "zoneraisestop";
static final String CHANNEL_TIMECLOCK = "timeclock";
static final String CHANNEL_SCHEDULE = "schedule";
static final String CHANNEL_SUNRISE = "sunrise";
static final String CHANNEL_SUNSET = "sunset";
static final String CHANNEL_SUPERSEQUENCESTART = "ssstart";
static final String CHANNEL_SUPERSEQUENCEPAUSE = "sspause";
static final String CHANNEL_SUPERSEQUENCERESUME = "ssresume";
static final String CHANNEL_SUPERSEQUENCESTATUS = "ssstatus";
static final String CHANNEL_SUPERSEQUENCENEXTSTEP = "ssnextstep";
static final String CHANNEL_SUPERSEQUENCENEXTMIN = "ssnextminute";
static final String CHANNEL_SUPERSEQUENCENEXTSEC = "ssnextsecond";
// Channels for the Grafik Eye
static final String CHANNEL_SCENE = "scene";
static final String CHANNEL_SCENELOCK = "scenelock";
static final String CHANNEL_SCENESEQ = "sceneseq";
static final String CHANNEL_ZONELOCK = "zonelock";
static final String CHANNEL_ZONELOWER = "zonelower";
static final String CHANNEL_ZONERAISE = "zoneraise";
static final String CHANNEL_ZONEFADE = "zonefade";
static final String CHANNEL_ZONEINTENSITY = "zoneintensity";
static final String CHANNEL_ZONESHADE = "zoneshade";
}

View File

@@ -0,0 +1,64 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.lutron.internal.grxprg;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingStatusDetail;
import org.openhab.core.types.State;
/**
*
* A callback to {@link PrgBridgeHandler} that will be used by the {@link PrgProtocolHandler} to update the status and
* state of the {@link PrgBridgeHandler} or a specific {@link GrafikEyeHandler}
*
* @author Tim Roberts - Initial contribution
*
*/
interface PrgHandlerCallback {
/**
* Callback to the {@link PrgBridgeHandler} to update the status of the {@link Bridge}
*
* @param status a non-null {@link org.openhab.core.thing.ThingStatus}
* @param detail a non-null {@link org.openhab.core.thing.ThingStatusDetail}
* @param msg a possibly null, possibly empty message
*/
void statusChanged(ThingStatus status, ThingStatusDetail detail, String msg);
/**
* Callback to the {@link PrgBridgeHandler} to update the state of an item
*
* @param channelId the non-null, non-empty channel id
* @param state the new non-null {@State}
*/
void stateChanged(String channelId, State state);
/**
* Callback to the {@link PrgBridgeHandler} to update the state of an item in a specific {@link GrafikEyeHandler}.
*
* @param controlUnit the control unit identifier to update
* @param channelId the non-null, non-empty channel id
* @param state the new non-null {@State}
*/
void stateChanged(int controlUnit, String channelId, State state);
/**
* Callback to the {@link PrgBridgeHandler} to determine if the specific zone on a specific control unit is a shade
* or not
*
* @param controlUnit the control unit identifier
* @param zone the zone identify
* @return true if a shade zone, false otherwise
*/
boolean isShade(int controlUnit, int zone);
}

View File

@@ -0,0 +1,386 @@
/**
* 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.grxprg;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintStream;
import java.net.Socket;
import java.net.SocketTimeoutException;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Represents a restartable socket connection to the underlying telnet session with an GRX-PRG/GRX-CI-PRG. Commands can
* be sent via {@link #sendCommand(String)} and responses will be received on the {@link SocketSessionCallback}
*
* @author Tim Roberts - Initial contribution
*/
public class SocketSession {
private final Logger logger = LoggerFactory.getLogger(SocketSession.class);
/**
* The host/ip address to connect to
*/
private final String host;
/**
* The port to connect to
*/
private final int port;
/**
* The actual socket being used. Will be null if not connected
*/
private Socket client;
/**
* The writer to the {@link #client}. Will be null if not connected
*/
private PrintStream writer;
/**
* The reader from the {@link #client}. Will be null if not connected
*/
private BufferedReader reader;
/**
* The {@link ResponseReader} that will be used to read from {@link #reader}
*/
private final ResponseReader responseReader = new ResponseReader();
/**
* The responses read from the {@link #responseReader}
*/
private final BlockingQueue<Object> responsesQueue = new ArrayBlockingQueue<>(50);
/**
* The dispatcher of responses from {@link #responsesQueue}
*/
private final Dispatcher dispatcher = new Dispatcher();
/**
* The {@link SocketSessionCallback} that the {@link #dispatcher} will call
*/
private AtomicReference<SocketSessionCallback> callback = new AtomicReference<>(null);
/**
* Creates the socket session from the given host and port
*
* @param host a non-null, non-empty host/ip address
* @param port the port number between 1 and 65535
*/
public SocketSession(String host, int port) {
if (host == null || host.trim().length() == 0) {
throw new IllegalArgumentException("Host cannot be null or empty");
}
if (port < 1 || port > 65535) {
throw new IllegalArgumentException("Port must be between 1 and 65535");
}
this.host = host;
this.port = port;
}
/**
* Sets the {@link SocketSessionCallback} to use when calling back the
* responses that have been received.
*
* @param callback a non-null {@link SocketSessionCallback} to use
*/
public void setCallback(SocketSessionCallback callback) {
if (callback == null) {
throw new IllegalArgumentException("callback cannot be null");
}
this.callback.set(callback);
}
/**
* Will attempt to connect to the {@link #host} on port {@link #port}. If we are current connected, will
* {@link #disconnect()} first. Once connected, the {@link #writer} and {@link #reader} will be created, the
* {@link #dispatcher} and {@link #responseReader} will be started.
*
* @throws java.io.IOException if an exception occurs during the connection attempt
*/
public void connect() throws IOException {
disconnect();
client = new Socket(host, port);
client.setKeepAlive(true);
client.setSoTimeout(1000); // allow reader to check to see if it should stop every 1 second
logger.debug("Connecting to {}:{}", host, port);
writer = new PrintStream(client.getOutputStream());
reader = new BufferedReader(new InputStreamReader(client.getInputStream()));
new Thread(responseReader).start();
new Thread(dispatcher).start();
}
/**
* Disconnects from the {@link #host} if we are {@link #isConnected()}. The {@link #writer}, {@link #reader} and
* {@link #client}
* will be closed and set to null. The {@link #dispatcher} and {@link #responseReader} will be stopped, the
* {@link #callback} will be nulled and the {@link #responsesQueue} will be cleared.
*
* @throws java.io.IOException if an exception occurs during the disconnect attempt
*/
public void disconnect() throws IOException {
if (isConnected()) {
logger.debug("Disconnecting from {}:{}", host, port);
dispatcher.stopRunning();
responseReader.stopRunning();
writer.close();
writer = null;
reader.close();
reader = null;
client.close();
client = null;
callback.set(null);
responsesQueue.clear();
}
}
/**
* Returns true if we are connected ({@link #client} is not null and is connected)
*
* @return true if connected, false otherwise
*/
public boolean isConnected() {
return client != null && client.isConnected();
}
/**
* Sends the specified command to the underlying socket
*
* @param command a non-null, non-empty command
* @throws java.io.IOException an exception that occurred while sending
*/
public synchronized void sendCommand(String command) throws IOException {
if (command == null) {
throw new IllegalArgumentException("command cannot be null");
}
if (!isConnected()) {
throw new IOException("Cannot send message - disconnected");
}
logger.debug("Sending Command: '{}'", command);
writer.println(command + "\n"); // as pre spec - each command must have a newline
writer.flush();
}
/**
* This is the runnable that will read from the socket and add messages to the responses queue (to be processed by
* the dispatcher)
*
* @author Tim Roberts
*
*/
private class ResponseReader implements Runnable {
/**
* Whether the reader is currently rRunning
*/
private final AtomicBoolean isRunning = new AtomicBoolean(false);
/**
* Locking to allow proper shutdown of the reader
*/
private final Lock rLock = new ReentrantLock();
private final Condition rRunning = rLock.newCondition();
/**
* Stops the reader. Will wait 5 seconds for the runnable to stop (should stop within 1 second based on the
* setSOTimeout)
*/
public void stopRunning() {
rLock.lock();
try {
if (isRunning.getAndSet(false)) {
if (!rRunning.await(5, TimeUnit.SECONDS)) {
logger.warn("Waited too long for dispatcher to finish");
}
}
} catch (InterruptedException e) {
// shouldn't happen
} finally {
rLock.unlock();
}
}
/**
* Runs the logic to read from the socket until {@link #isRunning} is false. A 'response' is anything that ends
* with a carriage-return/newline combo. Additionally, the special "login" prompts are
* treated as responses for purposes of logging in.
*/
@Override
public void run() {
final StringBuilder sb = new StringBuilder(100);
int c;
isRunning.set(true);
responsesQueue.clear();
while (isRunning.get()) {
try {
// if reader is null, sleep and try again
if (reader == null) {
Thread.sleep(250);
continue;
}
c = reader.read();
if (c == -1) {
responsesQueue.put(new IOException("server closed connection"));
isRunning.set(false);
break;
}
final char ch = (char) c;
sb.append(ch);
if (ch == '\n' || ch == ' ') {
final String str = sb.toString();
if (str.endsWith("\r\n") || str.endsWith("login: ")) {
sb.setLength(0);
final String response = str.substring(0, str.length() - 2);
logger.debug("Received response: {}", response);
responsesQueue.put(response);
}
}
// logger.debug(">>> reading: " + sb + ":" + (int) ch);
} catch (SocketTimeoutException e) {
// do nothing - we expect this (setSOTimeout) to check the _isReading
} catch (InterruptedException e) {
// Do nothing - probably shutting down
} catch (IOException e) {
try {
isRunning.set(false);
responsesQueue.put(e);
} catch (InterruptedException e1) {
// Do nothing - probably shutting down
}
}
}
rLock.lock();
try {
rRunning.signalAll();
} finally {
rLock.unlock();
}
}
}
/**
* The dispatcher runnable is responsible for reading the response queue and dispatching it to the current callable.
* Since the dispatcher is ONLY started when a callable is set, responses may pile up in the queue and be dispatched
* when a callable is set. Unlike the socket reader, this can be assigned to another thread (no state outside of the
* class).
*
* @author Tim Roberts
*/
private class Dispatcher implements Runnable {
/**
* Whether the dispatcher is rRunning or not
*/
private final AtomicBoolean dispatcherRunning = new AtomicBoolean(false);
/**
* Locking to allow proper shutdown of the reader
*/
private final Lock dLock = new ReentrantLock();
private final Condition dRunning = dLock.newCondition();
/**
* Stops the reader. Will wait 5 seconds for the runnable to stop (should stop within 1 second based on the poll
* timeout below)
*/
public void stopRunning() {
dLock.lock();
try {
if (dispatcherRunning.getAndSet(false)) {
if (!dRunning.await(5, TimeUnit.SECONDS)) {
logger.warn("Waited too long for dispatcher to finish");
}
}
} catch (InterruptedException e) {
// do nothing
} finally {
dLock.unlock();
}
}
/**
* Runs the logic to dispatch any responses to the current callback until {@link #isRunning} is false.
*/
@Override
public void run() {
dispatcherRunning.set(true);
while (dispatcherRunning.get()) {
try {
final SocketSessionCallback ssCallback = callback.get();
// if callback is null, we don't want to start dispatching yet.
if (ssCallback == null) {
Thread.sleep(250);
continue;
}
final Object response = responsesQueue.poll(1, TimeUnit.SECONDS);
if (response != null) {
if (response instanceof String) {
try {
logger.debug("Dispatching response: {}", response);
ssCallback.responseReceived((String) response);
} catch (Exception e) {
logger.warn("Exception occurred processing the response '{}': ", response, e);
}
} else if (response instanceof Exception) {
logger.debug("Dispatching exception: {}", response);
ssCallback.responseException((Exception) response);
} else {
logger.error("Unknown response class: {}", response);
}
}
} catch (InterruptedException e) {
// Do nothing
}
}
dLock.lock();
try {
// Signal that we are done
dRunning.signalAll();
} finally {
dLock.unlock();
}
}
}
}

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.grxprg;
/**
* Interface defining a callback from {@link SocketSession} when a response was received (or an exception occurred)
*
* @author Tim Roberts - Initial contribution
*/
public interface SocketSessionCallback {
/**
* Called when a command has completed with the response for the command
*
* @param response a non-null, possibly empty response
*/
public void responseReceived(String response);
/**
* Called when a command finished with an exception
*
* @param e a non-null exception
*/
public void responseException(Exception e);
}

View File

@@ -0,0 +1,405 @@
/**
* 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.BINDING_ID;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.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.core.library.types.OnOffType;
import org.openhab.core.library.types.OpenClosedType;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.Channel;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingStatusDetail;
import org.openhab.core.thing.binding.builder.ChannelBuilder;
import org.openhab.core.thing.binding.builder.ThingBuilder;
import org.openhab.core.thing.type.ChannelTypeUID;
import org.openhab.core.types.Command;
import org.openhab.core.types.RefreshType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Abstract class providing common definitions and methods for derived keypad classes
*
* @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<>();
protected int integrationId;
protected String model;
protected Boolean autoRelease;
protected Boolean advancedChannels = false;
protected Map<Integer, String> componentChannelMap = new HashMap<>(50);
protected abstract void configureComponents(String model);
private final Object asyncInitLock = new Object();
protected KeypadConfig kp;
public BaseKeypadHandler(Thing thing) {
super(thing);
}
/**
* Determine if keypad component with the specified id is a LED. Keypad handlers which do not use a KeypadConfig
* object must override this to provide their own test.
*
* @param id The component id.
* @return True if the component is a LED.
*/
protected boolean isLed(int id) {
return kp.isLed(id);
}
/**
* Determine if keypad component with the specified id is a button. Keypad handlers which do not use a KeypadConfig
* object must override this to provide their own test.
*
* @param id The component id.
* @return True if the component is a button.
*/
protected boolean isButton(int id) {
return kp.isButton(id);
}
/**
* Determine if keypad component with the specified id is a CCI. Keypad handlers which do not use a KeypadConfig
* object must override this to provide their own test.
*
* @param id The component id.
* @return True if the component is a CCI.
*/
protected boolean isCCI(int id) {
return kp.isCCI(id);
}
protected void configureChannels() {
Channel channel;
ChannelTypeUID channelTypeUID;
ChannelUID channelUID;
logger.debug("Configuring channels for keypad {}", integrationId);
List<Channel> channelList = new ArrayList<>();
List<Channel> existingChannels = getThing().getChannels();
if (existingChannels != null && !existingChannels.isEmpty()) {
// Clear existing channels
logger.debug("Clearing existing channels for keypad {}", integrationId);
ThingBuilder thingBuilder = editThing();
thingBuilder.withChannels(channelList);
updateThing(thingBuilder.build());
}
ThingBuilder thingBuilder = editThing();
// add channels for buttons
for (KeypadComponent component : buttonList) {
channelTypeUID = new ChannelTypeUID(BINDING_ID, advancedChannels ? "buttonAdvanced" : "button");
channelUID = new ChannelUID(getThing().getUID(), component.channel());
channel = ChannelBuilder.create(channelUID, "Switch").withType(channelTypeUID)
.withLabel(component.description()).build();
channelList.add(channel);
}
// add channels for LEDs
for (KeypadComponent component : ledList) {
channelTypeUID = new ChannelTypeUID(BINDING_ID, advancedChannels ? "ledIndicatorAdvanced" : "ledIndicator");
channelUID = new ChannelUID(getThing().getUID(), component.channel());
channel = ChannelBuilder.create(channelUID, "Switch").withType(channelTypeUID)
.withLabel(component.description()).build();
channelList.add(channel);
}
// add channels for CCIs (for VCRX or eventually HomeWorks CCI)
for (KeypadComponent component : cciList) {
channelTypeUID = new ChannelTypeUID(BINDING_ID, "cciState");
channelUID = new ChannelUID(getThing().getUID(), component.channel());
channel = ChannelBuilder.create(channelUID, "Contact").withType(channelTypeUID)
.withLabel(component.description()).build();
channelList.add(channel);
}
thingBuilder.withChannels(channelList);
updateThing(thingBuilder.build());
logger.debug("Done configuring channels for keypad {}", integrationId);
}
protected ChannelUID channelFromComponent(int component) {
String channel = null;
// Get channel string from Lutron component ID using HashBiMap
channel = componentChannelMap.get(component);
if (channel == null) {
logger.debug("Unknown component {}", component);
}
return channel == null ? null : new ChannelUID(getThing().getUID(), channel);
}
protected Integer componentFromChannel(ChannelUID channelUID) {
return componentChannelMap.entrySet().stream().filter(e -> e.getValue().equals(channelUID.getId()))
.map(Entry::getKey).findAny().orElse(null);
}
@Override
public int getIntegrationId() {
return integrationId;
}
@Override
public void initialize() {
Number id = (Number) getThing().getConfiguration().get("integrationId");
if (id == null) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "No integrationId");
return;
}
integrationId = id.intValue();
logger.debug("Initializing Keypad Handler for integration ID {}", id);
model = (String) getThing().getConfiguration().get("model");
if (model != null) {
model = model.toUpperCase();
if (model.contains("-")) {
// strip off system prefix if model is of the form "system-model"
String[] modelSplit = model.split("-", 2);
model = modelSplit[1];
}
}
Boolean arParam = (Boolean) getThing().getConfiguration().get("autorelease");
autoRelease = arParam == null ? true : arParam;
// schedule a thread to finish initialization asynchronously since it can take several seconds
scheduler.schedule(this::asyncInitialize, 0, TimeUnit.SECONDS);
}
private void asyncInitialize() {
synchronized (asyncInitLock) {
logger.debug("Async init thread staring for keypad handler {}", integrationId);
buttonList.clear(); // in case we are re-initializing
ledList.clear();
cciList.clear();
componentChannelMap.clear();
configureComponents(model);
// load the channel-id map
for (KeypadComponent component : buttonList) {
componentChannelMap.put(component.id(), component.channel());
}
for (KeypadComponent component : ledList) {
componentChannelMap.put(component.id(), component.channel());
}
for (KeypadComponent component : cciList) {
componentChannelMap.put(component.id(), component.channel());
}
configureChannels();
initDeviceState();
logger.debug("Async init thread finishing for keypad handler {}", integrationId);
}
}
@Override
public void initDeviceState() {
synchronized (asyncInitLock) {
logger.debug("Initializing device state for Keypad {}", integrationId);
Bridge bridge = getBridge();
if (bridge == null) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "No bridge configured");
} else if (bridge.getStatus() == ThingStatus.ONLINE) {
if (ledList.isEmpty()) {
// Device with no LEDs has nothing to query. Assume it is online if bridge is online.
updateStatus(ThingStatus.ONLINE);
} else {
// Query LED states. Method handleUpdate() will set thing status to online when response arrives.
updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.NONE, "Awaiting initial response");
// 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);
}
}
}
} else {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
}
}
}
@Override
public void handleCommand(final ChannelUID channelUID, Command command) {
logger.debug("Handling command {} for channel {}", command, channelUID);
Channel channel = getThing().getChannel(channelUID.getId());
if (channel == null) {
logger.warn("Command received on invalid channel {} for device {}", channelUID, getThing().getUID());
return;
}
Integer componentID = componentFromChannel(channelUID);
if (componentID == null) {
logger.warn("Command received on invalid channel {} for device {}", channelUID, getThing().getUID());
return;
}
// For LEDs, handle RefreshType and OnOffType commands
if (isLed(componentID)) {
if (command instanceof RefreshType) {
queryDevice(componentID, ACTION_LED_STATE);
} else if (command instanceof OnOffType) {
if (command == OnOffType.ON) {
device(componentID, ACTION_LED_STATE, LED_ON);
} else if (command == OnOffType.OFF) {
device(componentID, ACTION_LED_STATE, LED_OFF);
}
} else {
logger.warn("Invalid command {} received for channel {} device {}", command, channelUID,
getThing().getUID());
}
return;
}
// For buttons, handle OnOffType commands
if (isButton(componentID)) {
if (command instanceof OnOffType) {
if (command == OnOffType.ON) {
device(componentID, ACTION_PRESS);
if (autoRelease) {
device(componentID, ACTION_RELEASE);
}
} else if (command == OnOffType.OFF) {
device(componentID, ACTION_RELEASE);
}
} else {
logger.warn("Invalid command type {} received for channel {} device {}", command, channelUID,
getThing().getUID());
}
return;
}
// Contact channels for CCIs are read-only, so ignore commands
if (isCCI(componentID)) {
logger.debug("Invalid command type {} received for channel {} device {}", command, channelUID,
getThing().getUID());
return;
}
}
@Override
public void channelLinked(ChannelUID channelUID) {
logger.debug("Linking keypad {} channel {}", integrationId, channelUID.getId());
Integer id = componentFromChannel(channelUID);
if (id == null) {
logger.warn("Unrecognized channel ID {} linked", channelUID.getId());
return;
}
// if this channel is for an LED, query the Lutron controller for the current state
if (isLed(id)) {
queryDevice(id, ACTION_LED_STATE);
}
// Button and CCI state can't be queried, only monitored for updates.
// Init button state to OFF on channel init.
if (isButton(id)) {
updateState(channelUID, OnOffType.OFF);
}
// Leave CCI channel state undefined on channel init.
}
@Override
public void handleUpdate(LutronCommandType type, String... parameters) {
logger.trace("Handling command {} {} from keypad {}", type, parameters, integrationId);
if (type == LutronCommandType.DEVICE && parameters.length >= 2) {
int component;
try {
component = Integer.parseInt(parameters[0]);
} catch (NumberFormatException e) {
logger.error("Invalid component {} in keypad update event message", parameters[0]);
return;
}
ChannelUID channelUID = channelFromComponent(component);
if (channelUID != null) {
if (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])) {
updateState(channelUID, OnOffType.ON);
} else if (LED_OFF.toString().equals(parameters[2])) {
updateState(channelUID, OnOffType.OFF);
}
} else if (ACTION_PRESS.toString().equals(parameters[1])) {
if (isButton(component)) {
updateState(channelUID, OnOffType.ON);
if (autoRelease) {
updateState(channelUID, OnOffType.OFF);
}
} else { // component is CCI
updateState(channelUID, OpenClosedType.CLOSED);
}
} else if (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])) {
updateState(channelUID, OnOffType.OFF); // Signal a release if we receive a hold code as we will not
// get a subsequent release.
}
} else {
logger.warn("Unable to determine channel for component {} in keypad update event message",
parameters[0]);
}
}
}
}

View File

@@ -0,0 +1,184 @@
/**
* 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.openhab.binding.lutron.internal.config.BlindConfig;
import org.openhab.binding.lutron.internal.protocol.LutronCommandType;
import org.openhab.core.library.types.PercentType;
import org.openhab.core.library.types.StopMoveType;
import org.openhab.core.library.types.UpDownType;
import org.openhab.core.thing.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 with Lutron blinds
*
* @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
private final Logger logger = LoggerFactory.getLogger(BlindHandler.class);
private BlindConfig config;
public BlindHandler(Thing thing) {
super(thing);
}
@Override
public int getIntegrationId() {
if (config == null) {
throw new IllegalStateException("handler configuration not initialized");
}
return config.integrationId;
}
@Override
public void initialize() {
config = getThing().getConfiguration().as(BlindConfig.class);
if (config.integrationId <= 0) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "No integrationId configured");
return;
}
if (config.type == null || (!(BLIND_TYPE_SHEER.equalsIgnoreCase(config.type))
&& !(BLIND_TYPE_VENETIAN.equalsIgnoreCase(config.type)))) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
"Parameter type not set to valid value");
return;
}
String blindType = config.type;
if (BLIND_TYPE_SHEER.equalsIgnoreCase(blindType)) {
tiltMax = 50;
}
logger.debug("Initializing Blind handler with type {} for integration ID {}", blindType, config.integrationId);
initDeviceState();
}
@Override
protected void initDeviceState() {
logger.debug("Initializing device state for Shade {}", 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(ACTION_LIFTLEVEL); // handleUpdate() will set thing status to online when response arrives
queryOutput(ACTION_TILTLEVEL);
} else {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
}
}
@Override
public void channelLinked(ChannelUID channelUID) {
// Refresh state when new item is linked.
if (channelUID.getId().equals(CHANNEL_BLINDLIFTLEVEL)) {
queryOutput(ACTION_LIFTLEVEL);
} else if (channelUID.getId().equals(CHANNEL_BLINDTILTLEVEL)) {
queryOutput(ACTION_TILTLEVEL);
}
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
if (channelUID.getId().equals(CHANNEL_BLINDLIFTLEVEL)) {
handleLiftCommand(command);
} else if (channelUID.getId().equals(CHANNEL_BLINDTILTLEVEL)) {
handleTiltCommand(command);
}
}
private void handleLiftCommand(Command command) {
if (command instanceof PercentType) {
int level = ((PercentType) command).intValue();
output(ACTION_LIFTLEVEL, level, 0);
} else if (command.equals(UpDownType.UP)) {
output(ACTION_STARTRAISINGLIFT);
} else if (command.equals(UpDownType.DOWN)) {
output(ACTION_STARTLOWERINGLIFT);
} else if (command.equals(StopMoveType.STOP)) {
output(ACTION_STOPLIFT);
} else if (command instanceof RefreshType) {
queryOutput(ACTION_LIFTLEVEL);
}
}
private void handleTiltCommand(Command command) {
if (command instanceof PercentType) {
int level = ((PercentType) command).intValue();
output(ACTION_TILTLEVEL, Math.min(level, tiltMax), 0);
} else if (command.equals(UpDownType.UP)) {
output(ACTION_STARTRAISINGTILT);
} else if (command.equals(UpDownType.DOWN)) {
output(ACTION_STARTLOWERINGTILT);
} else if (command.equals(StopMoveType.STOP)) {
output(ACTION_STOPTILT);
} else if (command instanceof RefreshType) {
queryOutput(ACTION_TILTLEVEL);
}
}
@Override
public void handleUpdate(LutronCommandType type, String... parameters) {
if (type == LutronCommandType.OUTPUT && parameters.length >= 2) {
if (getThing().getStatus() == ThingStatus.UNKNOWN) {
updateStatus(ThingStatus.ONLINE);
}
if (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])) {
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) {
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])
&& 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);
updateState(CHANNEL_BLINDLIFTLEVEL, new PercentType(level));
}
}
}
}

View File

@@ -0,0 +1,209 @@
/**
* 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 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.core.library.types.OnOffType;
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 with Lutron contact closure outputs (CCOs).
* e.g. VCRX CCOs and CCO RF module
*
* Note: For a RA2 Pulsed CCO, querying the output state with ?OUTPUT,<id>,1 is meaningless and will always
* return 100 (on). Also, the main repeater will not report ~OUTPUT commands for a pulsed CCO regardless of
* the #MONITORING setting. So this binding supports sending pulses ONLY.
*
* @author Bob Adair - Initial contribution
*
*/
@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;
private double defaultPulse = 0.5; // default pulse length (seconds)
protected enum CcoOutputType {
PULSED,
MAINTAINED
}
protected @Nullable CcoOutputType outputType;
public CcoHandler(Thing thing) {
super(thing);
}
@Override
public int getIntegrationId() {
return integrationId;
}
@Override
public void initialize() {
Number id = (Number) getThing().getConfiguration().get(INTEGRATION_ID);
if (id == null) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "No integrationId");
return;
}
integrationId = id.intValue();
logger.debug("Initializing CCO handler for integration ID {}", id);
// Determine output type from configuration if not pre-defined by subclass
if (outputType == null) {
String oType = (String) getThing().getConfiguration().get(CCO_TYPE);
if (oType == null || oType == CCO_TYPE_PULSED) {
logger.debug("Setting CCO type Pulsed for device {}.", integrationId);
outputType = CcoOutputType.PULSED;
} else if (oType == CCO_TYPE_MAINTAINED) {
logger.debug("Setting CCO type Maintained for device {}.", integrationId);
outputType = CcoOutputType.MAINTAINED;
} else {
logger.warn("Invalid CCO type setting for device {}. Defaulting to Pulsed.", integrationId);
outputType = CcoOutputType.PULSED;
}
}
// If output type pulsed, determine pulse length
if (outputType == CcoOutputType.PULSED) {
Number defaultPulse = (Number) getThing().getConfiguration().get(DEFAULT_PULSE);
if (defaultPulse != null) {
double dp = defaultPulse.doubleValue();
if (dp >= 0 && dp <= 99.0) {
defaultPulse = dp;
logger.debug("Pulse length set to {} seconds for device {}.", defaultPulse, integrationId);
} else {
logger.warn("Invalid pulse length value set. Using default for device {}.", integrationId);
}
} else {
logger.debug("Using default pulse length value for device {}", integrationId);
}
}
initDeviceState();
}
@Override
protected void initDeviceState() {
logger.debug("Initializing device state for CCO {}", integrationId);
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(ACTION_STATE); // 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_SWITCH)) {
logger.debug("switch channel {} linked for CCO {}", channelUID.getId(), integrationId);
if (outputType == CcoOutputType.PULSED) {
// Since this is a pulsed CCO channel state is always OFF
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);
} else {
logger.warn("invalid output type defined for CCO {}", integrationId);
}
} else {
logger.warn("invalid channel {} linked for CCO {}", channelUID.getId(), integrationId);
}
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
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));
updateState(channelUID, OnOffType.OFF);
} else {
output(ACTION_STATE, 100);
}
}
else if (command instanceof OnOffType && command == OnOffType.OFF) {
if (outputType == CcoOutputType.MAINTAINED) {
output(ACTION_STATE, 0);
}
}
else if (command instanceof RefreshType) {
if (outputType == CcoOutputType.MAINTAINED) {
queryOutput(ACTION_STATE);
} else {
updateState(CHANNEL_SWITCH, OnOffType.OFF);
}
} else {
logger.debug("ignoring invalid command on channel {} for CCO {}", channelUID.getId(), integrationId);
}
} else {
logger.debug("ignoring command on invalid channel {} for CCO {}", channelUID.getId(), integrationId);
}
}
@Override
public void handleUpdate(LutronCommandType type, String... parameters) {
logger.debug("Update received for CCO: {} {}", type, StringUtils.join(parameters, ","));
if (outputType == CcoOutputType.MAINTAINED) {
if (type == LutronCommandType.OUTPUT && parameters.length > 1
&& ACTION_STATE.toString().equals(parameters[0])) {
if (getThing().getStatus() == ThingStatus.UNKNOWN) {
updateStatus(ThingStatus.ONLINE);
}
try {
BigDecimal state = new BigDecimal(parameters[1]);
updateState(CHANNEL_SWITCH, state.compareTo(BigDecimal.ZERO) == 0 ? OnOffType.OFF : OnOffType.ON);
} catch (NumberFormatException e) {
logger.warn("Unable to parse update {} {} from CCO {}", type, StringUtils.join(parameters, ","),
integrationId);
return;
}
}
} else {
// Do nothing on receiving updates for pulsed CCO except update online status
if (getThing().getStatus() == ThingStatus.UNKNOWN) {
updateStatus(ThingStatus.ONLINE);
}
}
}
}

View File

@@ -0,0 +1,145 @@
/**
* 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_LIGHTLEVEL;
import java.math.BigDecimal;
import java.util.Collection;
import java.util.Collections;
import java.util.concurrent.atomic.AtomicReference;
import org.openhab.binding.lutron.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.core.library.types.OnOffType;
import org.openhab.core.library.types.PercentType;
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.ThingHandlerService;
import org.openhab.core.types.Command;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Handler responsible for communicating with a light dimmer.
*
* @author Allan Tong - Initial contribution
* @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;
private LutronDuration fadeOutTime;
private final AtomicReference<BigDecimal> lastLightLevel = new AtomicReference<>();
public DimmerHandler(Thing thing) {
super(thing);
}
@Override
public Collection<Class<? extends ThingHandlerService>> getServices() {
return Collections.singletonList(DimmerActions.class);
}
@Override
public int getIntegrationId() {
if (config == null) {
throw new IllegalStateException("handler not initialized");
}
return config.integrationId;
}
@Override
public void initialize() {
config = getThing().getConfiguration().as(DimmerConfig.class);
if (config.integrationId <= 0) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "No integrationId configured");
return;
}
fadeInTime = new LutronDuration(config.fadeInTime);
fadeOutTime = new LutronDuration(config.fadeOutTime);
logger.debug("Initializing Dimmer handler for integration ID {}", getIntegrationId());
initDeviceState();
}
@Override
protected void initDeviceState() {
logger.debug("Initializing device state for Dimmer {}", 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(ACTION_ZONELEVEL); // handleUpdate() will set thing status to online when response arrives
lastLightLevel.set(config.onLevel);
} else {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
}
}
@Override
public void channelLinked(ChannelUID channelUID) {
if (channelUID.getId().equals(CHANNEL_LIGHTLEVEL)) {
// Refresh state when new item is linked.
queryOutput(ACTION_ZONELEVEL);
}
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
if (channelUID.getId().equals(CHANNEL_LIGHTLEVEL)) {
if (command instanceof Number) {
int level = ((Number) command).intValue();
output(ACTION_ZONELEVEL, level, 0.25);
} else if (command.equals(OnOffType.ON)) {
if (config.onToLast) {
output(ACTION_ZONELEVEL, lastLightLevel.get(), fadeInTime);
} else {
output(ACTION_ZONELEVEL, config.onLevel, fadeInTime);
}
} else if (command.equals(OnOffType.OFF)) {
output(ACTION_ZONELEVEL, 0, fadeOutTime);
}
}
}
public void setLightLevel(BigDecimal level, LutronDuration fade, LutronDuration delay) {
int intLevel = level.intValue();
output(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])) {
BigDecimal level = new BigDecimal(parameters[1]);
if (getThing().getStatus() == ThingStatus.UNKNOWN) {
updateStatus(ThingStatus.ONLINE);
}
if (level.compareTo(BigDecimal.ZERO) == 1) { // if (level > 0)
lastLightLevel.set(level);
}
updateState(CHANNEL_LIGHTLEVEL, new PercentType(level));
}
}
}

View File

@@ -0,0 +1,64 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.lutron.internal.handler;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.lutron.internal.discovery.project.ComponentType;
import org.openhab.binding.lutron.internal.keypadconfig.KeypadConfigGrafikEye;
import org.openhab.core.thing.Thing;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Handler responsible for communicating with GRAFIK Eye QS devices in
* a RadioRA 2 or HomeWorks QS System.
*
* Does not communicate with the scene controller, timeclock controller, or wireless
* and EcoSystem occupancy sensors.
*
* @author Bob Adair - Initial contribution
*/
@NonNullByDefault
public class GrafikEyeKeypadHandler extends BaseKeypadHandler {
private final Logger logger = LoggerFactory.getLogger(GrafikEyeKeypadHandler.class);
@Override
protected void configureComponents(@Nullable String model) {
String mod = model == null ? "3COL" : model;
logger.debug("Configuring components for GRAFIK Eye QS");
switch (mod) {
case "3COL":
case "2COL":
case "1COL":
case "0COL":
buttonList = kp.getComponents(mod, ComponentType.BUTTON);
ledList = kp.getComponents(mod, ComponentType.LED);
cciList = kp.getComponents(mod, ComponentType.CCI);
break;
default:
logger.warn("No valid keypad model defined ({}). Assuming model 3COL.", mod);
buttonList = kp.getComponents("3COL", ComponentType.BUTTON);
ledList = kp.getComponents("3COL", ComponentType.LED);
cciList = kp.getComponents("3COL", ComponentType.CCI);
break;
}
}
public GrafikEyeKeypadHandler(Thing thing) {
super(thing);
kp = new KeypadConfigGrafikEye();
}
}

View File

@@ -0,0 +1,180 @@
/**
* 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.util.concurrent.ScheduledFuture;
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.core.library.types.DecimalType;
import org.openhab.core.library.types.OnOffType;
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 with RadioRA 2 Green Mode subsystem
*
* @author Bob Adair - Initial contribution
*/
@NonNullByDefault
public class GreenModeHandler extends LutronHandler {
private static final Integer ACTION_STEP = 1;
public static final int GREENSTEP_MIN = 1;
// poll interval parameters are in minutes
private static final int POLL_INTERVAL_DEFAULT = 15;
private static final int POLL_INTERVAL_MAX = 240;
private static final int POLL_INTERVAL_MIN = 0;
private final Logger logger = LoggerFactory.getLogger(GreenModeHandler.class);
private int integrationId;
private int pollInterval;
private @Nullable ScheduledFuture<?> pollJob;
public GreenModeHandler(Thing thing) {
super(thing);
}
@Override
public int getIntegrationId() {
return integrationId;
}
@Override
public void initialize() {
Number id = (Number) getThing().getConfiguration().get(INTEGRATION_ID);
if (id == null) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "No integrationId");
return;
}
integrationId = id.intValue();
Number pollInterval = (Number) getThing().getConfiguration().get(POLL_INTERVAL);
if (pollInterval == null) {
this.pollInterval = POLL_INTERVAL_DEFAULT;
} else {
this.pollInterval = pollInterval.intValue();
this.pollInterval = Math.min(this.pollInterval, POLL_INTERVAL_MAX);
this.pollInterval = Math.max(this.pollInterval, POLL_INTERVAL_MIN);
}
logger.debug("Initializing Green Mode handler for integration ID {} with poll interval {}", integrationId,
this.pollInterval);
initDeviceState();
}
@Override
protected void initDeviceState() {
logger.debug("Initializing device state for Green Mode subsystem {}", getIntegrationId());
stopPolling();
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");
queryGreenMode(ACTION_STEP); // handleUpdate() will set thing status to online when response arrives
} else {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
}
}
@Override
protected void thingOfflineNotify() {
stopPolling();
}
private void startPolling() {
if (pollInterval > 0 && pollJob == null) {
logger.debug("Scheduling green mode polling job for integration ID {}", integrationId);
pollJob = scheduler.scheduleWithFixedDelay(this::pollState, pollInterval, pollInterval, TimeUnit.MINUTES);
}
}
private void stopPolling() {
if (pollJob != null) {
logger.debug("Canceling green mode polling job for integration ID {}", integrationId);
pollJob.cancel(true);
pollJob = null;
}
}
private synchronized void pollState() {
logger.trace("Executing green mode polling job for integration ID {}", integrationId);
queryGreenMode(ACTION_STEP);
}
@Override
public void channelLinked(ChannelUID channelUID) {
if (channelUID.getId().equals(CHANNEL_STEP)) {
queryGreenMode(ACTION_STEP);
}
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
if (channelUID.getId().equals(CHANNEL_STEP)) {
if (command == OnOffType.ON) {
greenMode(ACTION_STEP, 2);
} else if (command == OnOffType.OFF) {
greenMode(ACTION_STEP, 1);
} else if (command instanceof Number) {
Integer step = new Integer(((Number) command).intValue());
if (step.intValue() >= GREENSTEP_MIN) {
greenMode(ACTION_STEP, step);
}
} else if (command instanceof RefreshType) {
queryGreenMode(ACTION_STEP);
} else {
logger.debug("Ignoring invalid command {} for id {}", command, integrationId);
}
} else {
logger.debug("Ignoring command to invalid channel {} for id {}", channelUID.getId(), integrationId);
}
}
@Override
public void handleUpdate(LutronCommandType type, String... parameters) {
try {
if (type == LutronCommandType.MODE && parameters.length > 1
&& ACTION_STEP.toString().equals(parameters[0])) {
Long step = new Long(parameters[1]);
if (getThing().getStatus() == ThingStatus.UNKNOWN) {
updateStatus(ThingStatus.ONLINE);
startPolling();
}
updateState(CHANNEL_STEP, new DecimalType(step.longValue()));
} else {
logger.debug("Ignoring unexpected update for id {}", integrationId);
}
} catch (NumberFormatException e) {
logger.debug("Encountered number format exception while handling update for greenmode {}", integrationId);
}
}
@Override
public void dispose() {
stopPolling();
}
}

View File

@@ -0,0 +1,494 @@
/**
* 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 java.io.IOException;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.regex.MatchResult;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.commons.lang.StringUtils;
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.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;
import org.slf4j.LoggerFactory;
/**
* Handler responsible for communicating with the main Lutron control hub.
*
* @author Allan Tong - Initial contribution
* @author Bob Adair - Added reconnect and heartbeat config parameters, moved discovery service registration to
* LutronHandlerFactory
*/
public class IPBridgeHandler extends BaseBridgeHandler {
private static final Pattern RESPONSE_REGEX = Pattern
.compile("~(OUTPUT|DEVICE|SYSTEM|TIMECLOCK|MODE|SYSVAR),([0-9\\.:/]+),([0-9,\\.:/]*)\\Z");
private static final String DB_UPDATE_DATE_FORMAT = "MM/dd/yyyy HH:mm:ss";
private static final Integer MONITOR_PROMPT = 12;
private static final Integer MONITOR_SYSVAR = 10;
private static final Integer MONITOR_ENABLE = 1;
private static final Integer MONITOR_DISABLE = 2;
private static final Integer SYSTEM_DBEXPORTDATETIME = 10;
private static final int MAX_LOGIN_ATTEMPTS = 2;
private static final String PROMPT_GNET = "GNET>";
private static final String PROMPT_QNET = "QNET>";
private static final String PROMPT_SAFE = "SAFE>";
private static final String LOGIN_MATCH_REGEX = "(login:|[GQ]NET>|SAFE>)";
private static final String DEFAULT_USER = "lutron";
private static final String DEFAULT_PASSWORD = "integration";
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 final Logger logger = LoggerFactory.getLogger(IPBridgeHandler.class);
private IPBridgeConfig config;
private int reconnectInterval;
private int heartbeatInterval;
private int sendDelay;
private TelnetSession session;
private BlockingQueue<LutronCommand> sendQueue = new LinkedBlockingQueue<>();
private Thread messageSender;
private ScheduledFuture<?> keepAlive;
private ScheduledFuture<?> keepAliveReconnect;
private ScheduledFuture<?> connectRetryJob;
private Date lastDbUpdateDate;
private LutronDeviceDiscoveryService discoveryService;
private final AtomicBoolean requireSysvarMonitoring = new AtomicBoolean(false);
public void setDiscoveryService(LutronDeviceDiscoveryService discoveryService) {
this.discoveryService = discoveryService;
}
public class LutronSafemodeException extends Exception {
private static final long serialVersionUID = 1L;
public LutronSafemodeException(String message) {
super(message);
}
}
public IPBridgeConfig getIPBridgeConfig() {
return config;
}
public IPBridgeHandler(Bridge bridge) {
super(bridge);
this.session = new TelnetSession();
this.session.addListener(new TelnetSessionListener() {
@Override
public void inputAvailable() {
parseUpdates();
}
@Override
public void error(IOException exception) {
}
});
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
}
@Override
public void initialize() {
this.config = getThing().getConfiguration().as(IPBridgeConfig.class);
if (validConfiguration(this.config)) {
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;
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE, "Connecting");
scheduler.submit(this::connect); // start the async connect task
}
}
private boolean validConfiguration(IPBridgeConfig config) {
if (config == null) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "bridge configuration missing");
return false;
}
if (StringUtils.isEmpty(config.ipAddress)) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "bridge address not specified");
return false;
}
return true;
}
private void scheduleConnectRetry(long waitMinutes) {
logger.debug("Scheduling connection retry in {} minutes", waitMinutes);
connectRetryJob = scheduler.schedule(this::connect, waitMinutes, TimeUnit.MINUTES);
}
private synchronized void connect() {
if (this.session.isConnected()) {
return;
}
logger.debug("Connecting to bridge at {}", config.ipAddress);
try {
if (!login(config)) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "invalid username/password");
return;
}
} catch (LutronSafemodeException e) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "main repeater is in safe mode");
disconnect();
scheduleConnectRetry(reconnectInterval); // Possibly a temporary problem. Try again later.
return;
} catch (IOException e) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
disconnect();
scheduleConnectRetry(reconnectInterval); // Possibly a temporary problem. Try again later.
return;
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.HANDLER_INITIALIZING_ERROR, "login interrupted");
disconnect();
return;
}
updateStatus(ThingStatus.ONLINE);
// Disable prompts
sendCommand(new LutronCommand(LutronOperation.EXECUTE, LutronCommandType.MONITORING, -1, MONITOR_PROMPT,
MONITOR_DISABLE));
if (requireSysvarMonitoring.get()) {
setSysvarMonitoring(true);
}
// 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));
messageSender = new Thread(this::sendCommandsThread, "Lutron sender");
messageSender.start();
logger.debug("Starting keepAlive job with interval {}", heartbeatInterval);
keepAlive = scheduler.scheduleWithFixedDelay(this::sendKeepAlive, heartbeatInterval, heartbeatInterval,
TimeUnit.MINUTES);
}
private void sendCommandsThread() {
try {
while (!Thread.currentThread().isInterrupted()) {
LutronCommand command = sendQueue.take();
logger.debug("Sending command {}", command);
try {
session.writeLine(command.toString());
} 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();
// reconnect() will start a new thread; terminate this one
break;
}
if (sendDelay > 0) {
Thread.sleep(sendDelay); // introduce delay to throttle send rate
}
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
private synchronized void disconnect() {
logger.debug("Disconnecting from bridge");
if (connectRetryJob != null) {
connectRetryJob.cancel(true);
}
if (this.keepAlive != null) {
this.keepAlive.cancel(true);
}
if (this.keepAliveReconnect != null) {
// This method can be called from the keepAliveReconnect thread. Make sure
// we don't interrupt ourselves, as that may prevent the reconnection attempt.
this.keepAliveReconnect.cancel(false);
}
if (messageSender != null && messageSender.isAlive()) {
messageSender.interrupt();
}
try {
this.session.close();
} catch (IOException e) {
logger.warn("Error disconnecting: {}", e.getMessage());
}
}
private synchronized void reconnect() {
logger.debug("Keepalive timeout, attempting to reconnect to the bridge");
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.DUTY_CYCLE);
disconnect();
connect();
}
private boolean login(IPBridgeConfig config) throws IOException, InterruptedException, LutronSafemodeException {
this.session.open(config.ipAddress);
this.session.waitFor("login:");
// Sometimes the Lutron Smart Bridge Pro will request login more than once.
for (int attempt = 0; attempt < MAX_LOGIN_ATTEMPTS; attempt++) {
this.session.writeLine(config.user != null ? config.user : DEFAULT_USER);
this.session.waitFor("password:");
this.session.writeLine(config.password != null ? config.password : DEFAULT_PASSWORD);
MatchResult matchResult = this.session.waitFor(LOGIN_MATCH_REGEX);
if (PROMPT_GNET.equals(matchResult.group()) || PROMPT_QNET.equals(matchResult.group())) {
return true;
} else if (PROMPT_SAFE.equals(matchResult.group())) {
logger.warn("Lutron repeater is in safe mode. Unable to connect.");
throw new LutronSafemodeException("Lutron repeater in safe mode");
}
else {
logger.debug("got another login prompt, logging in again");
// we already got the login prompt so go straight to sending user
}
}
return false;
}
void sendCommand(LutronCommand command) {
this.sendQueue.add(command);
}
private LutronHandler findThingHandler(int integrationId) {
for (Thing thing : getThing().getThings()) {
if (thing.getHandler() instanceof LutronHandler) {
LutronHandler handler = (LutronHandler) thing.getHandler();
try {
if (handler != null && handler.getIntegrationId() == integrationId) {
return handler;
}
} catch (IllegalStateException e) {
logger.trace("Handler for id {} not initialized", integrationId);
}
}
}
return null;
}
private void parseUpdates() {
String paramString;
String scrubbedLine;
for (String line : this.session.readLines()) {
if (line.trim().equals("")) {
// Sometimes we get an empty line (possibly only when prompts are disabled). Ignore them.
continue;
}
logger.debug("Received message {}", line);
// System is alive, cancel reconnect task.
if (this.keepAliveReconnect != null) {
this.keepAliveReconnect.cancel(true);
}
Matcher matcher = RESPONSE_REGEX.matcher(line);
boolean responseMatched = matcher.find();
if (!responseMatched) {
// In some cases with Caseta a CLI prompt may be embedded within a received response line.
if (line.contains("NET>")) {
// Try to remove it and re-attempt the regex match.
scrubbedLine = line.replaceAll("[GQ]NET> ", "");
matcher = RESPONSE_REGEX.matcher(scrubbedLine);
responseMatched = matcher.find();
if (responseMatched) {
line = scrubbedLine;
logger.debug("Cleaned response line: {}", scrubbedLine);
}
}
}
if (!responseMatched) {
logger.debug("Ignoring message {}", line);
continue;
} else {
// We have a good response message
LutronCommandType type = LutronCommandType.valueOf(matcher.group(1));
if (type == LutronCommandType.SYSTEM) {
// SYSTEM messages are assumed to be a response to the SYSTEM_DBEXPORTDATETIME
// query. The response returns the last time the device database was updated.
setDbUpdateDate(matcher.group(2), matcher.group(3));
continue;
}
Integer integrationId;
try {
integrationId = Integer.valueOf(matcher.group(2));
} catch (NumberFormatException e1) {
logger.warn("Integer conversion error parsing update: {}", line);
continue;
}
paramString = matcher.group(3);
// Now dispatch update to the proper thing handler
LutronHandler handler = findThingHandler(integrationId);
if (handler != null) {
try {
handler.handleUpdate(type, paramString.split(","));
} catch (NumberFormatException e) {
logger.warn("Number format exception parsing update: {}", line);
} catch (RuntimeException e) {
logger.warn("Runtime exception while processing update: {}", line, e);
}
} else {
logger.debug("No thing configured for integration ID {}", integrationId);
}
}
}
}
private void sendKeepAlive() {
logger.debug("Scheduling keepalive reconnect job");
// Reconnect if no response is received within 30 seconds.
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));
}
private void setDbUpdateDate(String dateString, String timeString) {
try {
Date date = new SimpleDateFormat(DB_UPDATE_DATE_FORMAT).parse(dateString + " " + timeString);
if (this.lastDbUpdateDate == null || date.after(this.lastDbUpdateDate)) {
scanForDevices();
this.lastDbUpdateDate = date;
}
} catch (ParseException e) {
logger.warn("Failed to parse DB update date {} {}", dateString, timeString);
}
}
private void scanForDevices() {
try {
if (discoveryService != null) {
logger.debug("Initiating discovery scan for devices");
discoveryService.startScan(null);
} else {
logger.debug("Unable to initiate discovery because discoveryService is null");
}
} catch (Exception e) {
logger.warn("Error scanning for paired devices: {}", e.getMessage(), e);
}
}
private void setSysvarMonitoring(boolean enable) {
Integer setting = (enable) ? MONITOR_ENABLE : MONITOR_DISABLE;
sendCommand(
new LutronCommand(LutronOperation.EXECUTE, LutronCommandType.MONITORING, -1, MONITOR_SYSVAR, setting));
}
@Override
public void childHandlerInitialized(ThingHandler childHandler, Thing childThing) {
// enable sysvar monitoring the first time a sysvar child thing initializes
if (childHandler instanceof SysvarHandler) {
if (requireSysvarMonitoring.compareAndSet(false, true)) {
setSysvarMonitoring(true);
}
}
}
@Override
public void thingUpdated(Thing thing) {
IPBridgeConfig newConfig = thing.getConfiguration().as(IPBridgeConfig.class);
boolean validConfig = validConfiguration(newConfig);
boolean needsReconnect = validConfig && !this.config.sameConnectionParameters(newConfig);
if (!validConfig || needsReconnect) {
dispose();
}
this.thing = thing;
this.config = newConfig;
if (needsReconnect) {
initialize();
}
}
@Override
public void dispose() {
disconnect();
}
}

View File

@@ -0,0 +1,67 @@
/**
* 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.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.lutron.internal.discovery.project.ComponentType;
import org.openhab.binding.lutron.internal.keypadconfig.KeypadConfigIntlSeetouch;
import org.openhab.core.thing.Thing;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Handler responsible for communicating with Lutron International seeTouch keypads used in
* Homeworks QS systems
*
* @author Bob Adair - Initial contribution
*/
@NonNullByDefault
public class IntlKeypadHandler extends BaseKeypadHandler {
private final Logger logger = LoggerFactory.getLogger(IntlKeypadHandler.class);
@Override
protected void configureComponents(@Nullable String model) {
String mod = model == null ? "Generic" : model;
logger.debug("Configuring components for keypad model {}", model);
switch (mod) {
case "2B":
case "3B":
case "4B":
case "5BRL":
case "6BRL":
case "7BRL":
case "8BRL":
buttonList = kp.getComponents(mod, ComponentType.BUTTON);
ledList = kp.getComponents(mod, ComponentType.LED);
cciList = kp.getComponents(mod, ComponentType.CCI);
break;
default:
logger.warn("No valid keypad model defined ({}). Assuming 10BRL model.", mod);
// fall through
case "Generic":
case "10BRL":
buttonList = kp.getComponents("10BRL", ComponentType.BUTTON);
ledList = kp.getComponents("10BRL", ComponentType.LED);
cciList = kp.getComponents("10BRL", ComponentType.CCI);
break;
}
}
public IntlKeypadHandler(Thing thing) {
super(thing);
kp = new KeypadConfigIntlSeetouch();
}
}

View File

@@ -0,0 +1,110 @@
/**
* 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.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.lutron.internal.discovery.project.ComponentType;
import org.openhab.binding.lutron.internal.keypadconfig.KeypadConfigSeetouch;
import org.openhab.core.thing.Thing;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Handler responsible for communicating with Lutron seeTouch and Hybrid seeTouch keypads used in
* RadioRA2 and Homeworks QS systems
*
* @author Bob Adair - Initial contribution
*/
@NonNullByDefault
public class KeypadHandler extends BaseKeypadHandler {
private final Logger logger = LoggerFactory.getLogger(KeypadHandler.class);
@Override
protected void configureComponents(@Nullable String model) {
String mod = model == null ? "Generic" : model;
logger.debug("Configuring components for keypad model {}", model);
switch (mod) {
case "W1RLD":
case "H1RLD":
case "HN1RLD":
buttonList = kp.getComponents("W1RLD", ComponentType.BUTTON);
ledList = kp.getComponents("W1RLD", ComponentType.LED);
break;
case "W2RLD":
case "H2RLD":
case "HN2RLD":
buttonList = kp.getComponents("W2RLD", ComponentType.BUTTON);
ledList = kp.getComponents("W2RLD", ComponentType.LED);
break;
case "W3S":
case "H3S":
case "HN3S":
buttonList = kp.getComponents("W3S", ComponentType.BUTTON);
ledList = kp.getComponents("W3S", ComponentType.LED);
break;
case "W3BD":
buttonList = kp.getComponents(mod, ComponentType.BUTTON);
ledList = kp.getComponents(mod, ComponentType.LED);
break;
case "W3BRL":
buttonList = kp.getComponents(mod, ComponentType.BUTTON);
ledList = kp.getComponents(mod, ComponentType.LED);
break;
case "W3BSRL":
case "H3BSRL":
case "HN3BSRL":
buttonList = kp.getComponents("W3BSRL", ComponentType.BUTTON);
ledList = kp.getComponents("W3BSRL", ComponentType.LED);
break;
case "W4S":
case "H4S":
case "HN4S":
buttonList = kp.getComponents("W4S", ComponentType.BUTTON);
ledList = kp.getComponents("W4S", ComponentType.LED);
break;
case "W5BRL":
case "H5BRL":
case "HN5BRL":
case "W5BRLIR":
buttonList = kp.getComponents("W5BRL", ComponentType.BUTTON);
ledList = kp.getComponents("W5BRL", ComponentType.LED);
break;
case "W6BRL":
case "H6BRL":
case "HN6BRL":
buttonList = kp.getComponents("W6BRL", ComponentType.BUTTON);
ledList = kp.getComponents("W6BRL", ComponentType.LED);
break;
case "W7B":
buttonList = kp.getComponents(mod, ComponentType.BUTTON);
ledList = kp.getComponents(mod, ComponentType.LED);
break;
default:
logger.warn("No valid keypad model defined ({}). Assuming Generic model.", mod);
// fall through
case "Generic":
buttonList = kp.getComponents("Generic", ComponentType.BUTTON);
ledList = kp.getComponents("Generic", ComponentType.LED);
break;
}
}
public KeypadHandler(Thing thing) {
super(thing);
kp = new KeypadConfigSeetouch();
}
}

View File

@@ -0,0 +1,147 @@
/**
* 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.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.core.thing.Bridge;
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.BaseThingHandler;
import org.slf4j.Logger;
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
*
*/
@NonNullByDefault
public abstract class LutronHandler extends BaseThingHandler {
private final Logger logger = LoggerFactory.getLogger(LutronHandler.class);
public LutronHandler(Thing thing) {
super(thing);
}
public abstract int getIntegrationId();
public abstract void handleUpdate(LutronCommandType type, String... parameters);
/**
* Queries for any device state needed at initialization time or after losing connectivity to the bridge, and
* updates device status. Will be called when bridge status changes to ONLINE and thing has status
* OFFLINE:BRIDGE_OFFLINE.
*/
protected abstract void initDeviceState();
/**
* Called when changing thing status to offline. Subclasses may override to take any needed actions.
*/
protected void thingOfflineNotify() {
}
protected @Nullable IPBridgeHandler getBridgeHandler() {
Bridge bridge = getBridge();
return bridge == null ? null : (IPBridgeHandler) bridge.getHandler();
}
@Override
public void bridgeStatusChanged(ThingStatusInfo bridgeStatusInfo) {
logger.debug("Bridge status changed to {} for lutron device handler {}", bridgeStatusInfo.getStatus(),
getIntegrationId());
if (bridgeStatusInfo.getStatus() == ThingStatus.ONLINE
&& getThing().getStatusInfo().getStatusDetail() == ThingStatusDetail.BRIDGE_OFFLINE) {
initDeviceState();
} else if (bridgeStatusInfo.getStatus() == ThingStatus.OFFLINE) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
thingOfflineNotify();
}
}
private void sendCommand(LutronCommand command) {
IPBridgeHandler bridgeHandler = getBridgeHandler();
if (bridgeHandler == null) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.HANDLER_MISSING_ERROR, "No bridge associated");
thingOfflineNotify();
} else {
bridgeHandler.sendCommand(command);
}
}
protected void output(Object... parameters) {
sendCommand(
new LutronCommand(LutronOperation.EXECUTE, LutronCommandType.OUTPUT, getIntegrationId(), parameters));
}
protected void device(Object... parameters) {
sendCommand(
new LutronCommand(LutronOperation.EXECUTE, LutronCommandType.DEVICE, getIntegrationId(), parameters));
}
protected void timeclock(Object... parameters) {
sendCommand(new LutronCommand(LutronOperation.EXECUTE, LutronCommandType.TIMECLOCK, getIntegrationId(),
parameters));
}
protected void greenMode(Object... parameters) {
sendCommand(new LutronCommand(LutronOperation.EXECUTE, LutronCommandType.MODE, getIntegrationId(), parameters));
}
protected void sysvar(Object... parameters) {
sendCommand(
new LutronCommand(LutronOperation.EXECUTE, LutronCommandType.SYSVAR, getIntegrationId(), parameters));
}
protected void shadegrp(Object... parameters) {
sendCommand(
new LutronCommand(LutronOperation.EXECUTE, LutronCommandType.SHADEGRP, getIntegrationId(), parameters));
}
protected void queryOutput(Object... parameters) {
sendCommand(new LutronCommand(LutronOperation.QUERY, LutronCommandType.OUTPUT, getIntegrationId(), parameters));
}
protected void queryDevice(Object... parameters) {
sendCommand(new LutronCommand(LutronOperation.QUERY, LutronCommandType.DEVICE, getIntegrationId(), parameters));
}
protected void queryTimeclock(Object... parameters) {
sendCommand(
new LutronCommand(LutronOperation.QUERY, LutronCommandType.TIMECLOCK, getIntegrationId(), parameters));
}
protected void queryGreenMode(Object... parameters) {
sendCommand(new LutronCommand(LutronOperation.QUERY, LutronCommandType.MODE, getIntegrationId(), parameters));
}
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));
}
}

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.handler;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.thing.Thing;
/**
* Subclass that configures CcoHandler for Maintained outputs.
*
* @author Bob Adair - Initial contribution
*/
@NonNullByDefault
public class MaintainedCcoHandler extends CcoHandler {
public MaintainedCcoHandler(Thing thing) {
super(thing);
this.outputType = CcoOutputType.MAINTAINED;
}
}

View File

@@ -0,0 +1,93 @@
/**
* 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_OCCUPANCYSTATUS;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.lutron.internal.protocol.LutronCommandType;
import org.openhab.core.library.types.OnOffType;
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;
/**
*
* @author Allan Tong - Initial contribution
* @author Bob Adair - Added initDeviceState method
*/
@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;
public OccupancySensorHandler(Thing thing) {
super(thing);
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
}
@Override
public void initialize() {
Number id = (Number) getThing().getConfiguration().get("integrationId");
if (id == null) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "No integrationId");
return;
}
integrationId = id.intValue();
logger.debug("Initializing Occupancy Sensor handler for integration ID {}", id);
initDeviceState();
}
@Override
protected void initDeviceState() {
logger.debug("Initializing device state for Occupancy Sensor {}", getIntegrationId());
Bridge bridge = getBridge();
if (bridge == null) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "No bridge configured");
} else if (bridge.getStatus() == ThingStatus.ONLINE) {
updateStatus(ThingStatus.ONLINE); // can't poll this device, so assume it is online if the bridge is
} else {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
}
}
@Override
public int getIntegrationId() {
return this.integrationId;
}
@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])) {
updateState(CHANNEL_OCCUPANCYSTATUS, OnOffType.ON);
} else if (STATE_UNOCCUPIED.equals(parameters[1])) {
updateState(CHANNEL_OCCUPANCYSTATUS, OnOffType.OFF);
}
}
}
}

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.handler;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.lutron.internal.discovery.project.ComponentType;
import org.openhab.binding.lutron.internal.keypadconfig.KeypadConfigPalladiom;
import org.openhab.core.thing.Thing;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Handler responsible for communicating with Lutron Palladiom keypads used in
* Homeworks QS systems
*
* @author Bob Adair - Initial contribution
*/
@NonNullByDefault
public class PalladiomKeypadHandler extends BaseKeypadHandler {
private final Logger logger = LoggerFactory.getLogger(PalladiomKeypadHandler.class);
@Override
protected void configureComponents(@Nullable String model) {
String mod = model == null ? "Generic" : model;
logger.debug("Configuring components for keypad model {}", model);
switch (mod) {
case "2W":
case "3W":
case "4W":
case "RW":
case "22W":
case "24W":
case "42W":
case "44W":
case "2RW":
case "4RW":
case "RRW":
buttonList = kp.getComponents(mod, ComponentType.BUTTON);
ledList = kp.getComponents(mod, ComponentType.LED);
cciList = kp.getComponents(mod, ComponentType.CCI);
break;
default:
logger.warn("No valid keypad model defined ({}). Assuming Generic setting.", mod);
// fall through
case "Generic":
buttonList = kp.getComponents("Generic", ComponentType.BUTTON);
ledList = kp.getComponents("Generic", ComponentType.LED);
cciList = kp.getComponents("Generic", ComponentType.CCI);
break;
}
}
public PalladiomKeypadHandler(Thing thing) {
super(thing);
kp = new KeypadConfigPalladiom();
}
}

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.handler;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.lutron.internal.discovery.project.ComponentType;
import org.openhab.binding.lutron.internal.keypadconfig.KeypadConfigPico;
import org.openhab.core.thing.Thing;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Handler responsible for communicating with Lutron Pico keypads
*
* @author Bob Adair - Initial contribution
*/
@NonNullByDefault
public class PicoKeypadHandler extends BaseKeypadHandler {
private final Logger logger = LoggerFactory.getLogger(PicoKeypadHandler.class);
public PicoKeypadHandler(Thing thing) {
super(thing);
kp = new KeypadConfigPico();
}
@Override
protected void configureComponents(@Nullable String model) {
String mod = model == null ? "Generic" : model;
logger.debug("Configuring components for keypad model {}", mod);
switch (mod) {
case "2B":
case "2BRL":
case "3B":
case "4B":
buttonList = kp.getComponents(mod, ComponentType.BUTTON);
break;
default:
logger.warn("No valid keypad model defined ({}). Assuming model 3BRL.", mod);
// fall through
case "Generic":
case "3BRL":
buttonList = kp.getComponents("3BRL", ComponentType.BUTTON);
break;
}
}
}

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.handler;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.thing.Thing;
/**
* Subclass that configures CcoHandler for Pulsed outputs.
*
* @author Bob Adair - Initial contribution
*/
@NonNullByDefault
public class PulsedCcoHandler extends CcoHandler {
public PulsedCcoHandler(Thing thing) {
super(thing);
this.outputType = CcoOutputType.PULSED;
}
}

View File

@@ -0,0 +1,100 @@
/**
* 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 java.util.Arrays;
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.core.thing.Thing;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Handler responsible for communicating with Lutron QS IO Interfaces
*
* @author Bob Adair - Initial contribution
*/
@NonNullByDefault
public class QSIOHandler extends BaseKeypadHandler {
private static enum Component implements KeypadComponent {
CCI1(1, "cci1", "CCI 1", ComponentType.CCI),
CCI2(2, "cci2", "CCI 2", ComponentType.CCI),
CCI3(3, "cci3", "CCI 3", ComponentType.CCI),
CCI4(4, "cci4", "CCI 4", ComponentType.CCI),
CCI5(5, "cci5", "CCI 5", ComponentType.CCI);
private final int id;
private final String channel;
private final String description;
private final ComponentType type;
Component(int id, String channel, String description, ComponentType type) {
this.id = id;
this.channel = channel;
this.description = description;
this.type = type;
}
@Override
public int id() {
return this.id;
}
@Override
public String channel() {
return this.channel;
}
@Override
public String description() {
return this.description;
}
@Override
public ComponentType type() {
return type;
}
}
private final Logger logger = LoggerFactory.getLogger(QSIOHandler.class);
@Override
protected boolean isLed(int id) {
return false;
}
@Override
protected boolean isButton(int id) {
return false;
}
@Override
protected boolean isCCI(int id) {
return (id >= 1 && id <= 5);
}
@Override
protected void configureComponents(@Nullable String model) {
logger.debug("Configuring components for VCRX");
cciList.addAll(Arrays.asList(Component.CCI1, Component.CCI2, Component.CCI3, Component.CCI4, Component.CCI5));
}
public QSIOHandler(Thing thing) {
super(thing);
}
}

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.handler;
import static org.openhab.binding.lutron.internal.LutronBindingConstants.CHANNEL_SHADELEVEL;
import java.math.BigDecimal;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.lutron.internal.protocol.LutronCommandType;
import org.openhab.core.library.types.PercentType;
import org.openhab.core.library.types.StopMoveType;
import org.openhab.core.library.types.UpDownType;
import org.openhab.core.thing.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 with a Lutron Sivoia QS shade
*
* @author Bob Adair - Initial contribution based on Alan Tong's DimmerHandler
*/
@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;
public ShadeHandler(Thing thing) {
super(thing);
}
@Override
public int getIntegrationId() {
return integrationId;
}
@Override
public void initialize() {
Number id = (Number) getThing().getConfiguration().get("integrationId");
if (id == null) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "No integrationId");
return;
}
integrationId = id.intValue();
logger.debug("Initializing Shade handler for integration ID {}", id);
initDeviceState();
}
@Override
protected void initDeviceState() {
logger.debug("Initializing device state for Shade {}", 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(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) {
// Refresh state when new item is linked.
if (channelUID.getId().equals(CHANNEL_SHADELEVEL)) {
queryOutput(ACTION_ZONELEVEL);
}
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
if (channelUID.getId().equals(CHANNEL_SHADELEVEL)) {
if (command instanceof PercentType) {
int level = ((PercentType) command).intValue();
output(ACTION_ZONELEVEL, level, 0);
} else if (command.equals(UpDownType.UP)) {
output(ACTION_STARTRAISING);
} else if (command.equals(UpDownType.DOWN)) {
output(ACTION_STARTLOWERING);
} else if (command.equals(StopMoveType.STOP)) {
output(ACTION_STOP);
} else if (command instanceof RefreshType) {
queryOutput(ACTION_ZONELEVEL);
}
}
}
@Override
public void handleUpdate(LutronCommandType type, String... parameters) {
if (type == LutronCommandType.OUTPUT && parameters.length >= 2) {
if (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])
&& PARAMETER_POSITION_UPDATE.toString().equals(parameters[1]) && parameters.length >= 3) {
BigDecimal level = new BigDecimal(parameters[2]);
logger.trace("Shade {} received position update: {}", getIntegrationId(), level);
updateState(CHANNEL_SHADELEVEL, new PercentType(level));
}
}
}
}

View File

@@ -0,0 +1,111 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.lutron.internal.handler;
import static org.openhab.binding.lutron.internal.LutronBindingConstants.CHANNEL_SWITCH;
import java.math.BigDecimal;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.lutron.internal.protocol.LutronCommandType;
import org.openhab.core.library.types.OnOffType;
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 a switch.
*
* @author Allan Tong - Initial contribution
* @author Bob Adair - Added initDeviceState method
*/
@NonNullByDefault
public class SwitchHandler extends LutronHandler {
private static final Integer ACTION_ZONELEVEL = 1;
private final Logger logger = LoggerFactory.getLogger(SwitchHandler.class);
private int integrationId;
public SwitchHandler(Thing thing) {
super(thing);
}
@Override
public void initialize() {
Number id = (Number) getThing().getConfiguration().get("integrationId");
if (id == null) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "No integrationId");
return;
}
integrationId = id.intValue();
logger.debug("Initializing Switch handler for integration ID {}", id);
initDeviceState();
}
@Override
protected void initDeviceState() {
logger.debug("Initializing device state for Switch {}", 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(ACTION_ZONELEVEL); // handleUpdate() will set thing status to online when response arrives
} else {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
}
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
if (channelUID.getId().equals(CHANNEL_SWITCH)) {
if (command.equals(OnOffType.ON)) {
output(ACTION_ZONELEVEL, 100);
} else if (command.equals(OnOffType.OFF)) {
output(ACTION_ZONELEVEL, 0);
}
}
}
@Override
public int getIntegrationId() {
return this.integrationId;
}
@Override
public void handleUpdate(LutronCommandType type, String... parameters) {
if (type == LutronCommandType.OUTPUT && parameters.length > 1
&& ACTION_ZONELEVEL.toString().equals(parameters[0])) {
BigDecimal level = new BigDecimal(parameters[1]);
if (getThing().getStatus() == ThingStatus.UNKNOWN) {
updateStatus(ThingStatus.ONLINE);
}
postCommand(CHANNEL_SWITCH, level.compareTo(BigDecimal.ZERO) == 0 ? OnOffType.OFF : OnOffType.ON);
}
}
@Override
public void channelLinked(ChannelUID channelUID) {
if (channelUID.getId().equals(CHANNEL_SWITCH)) {
// Refresh state when new item is linked.
queryOutput(ACTION_ZONELEVEL);
}
}
}

View File

@@ -0,0 +1,118 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.lutron.internal.handler;
import static org.openhab.binding.lutron.internal.LutronBindingConstants.CHANNEL_VARSTATE;
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.core.library.types.DecimalType;
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 getting/setting sysvar state for HomeWorks QS
*
* @author Bob Adair - Initial contribution
*
*/
@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;
private int integrationId;
public SysvarHandler(Thing thing) {
super(thing);
}
@Override
public int getIntegrationId() {
SysvarConfig config = this.config;
if (config != null) {
return config.integrationId;
} else {
throw new IllegalStateException("handler not initialized");
}
}
@Override
public void initialize() {
SysvarConfig config = getThing().getConfiguration().as(SysvarConfig.class);
this.config = config;
if (config.integrationId <= 0) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "No integrationId configured");
} else {
integrationId = config.integrationId;
logger.debug("Initializing Sysvar handler for integration ID {}", integrationId);
initDeviceState();
}
}
@Override
protected void initDeviceState() {
logger.debug("Initializing handler state for sysvar id {}", integrationId);
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");
querySysvar(ACTION_GETSETSYSVAR); // 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_VARSTATE)) {
// Refresh state when new item is linked.
querySysvar(ACTION_GETSETSYSVAR);
}
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
if (channelUID.getId().equals(CHANNEL_VARSTATE)) {
if (command instanceof Number) {
int state = ((Number) command).intValue();
sysvar(ACTION_GETSETSYSVAR, state);
}
}
}
@Override
public void handleUpdate(LutronCommandType type, String... parameters) {
if (type == LutronCommandType.SYSVAR && parameters.length > 1
&& ACTION_GETSETSYSVAR.toString().equals(parameters[0])) {
BigDecimal state = new BigDecimal(parameters[1]);
if (getThing().getStatus() == ThingStatus.UNKNOWN) {
updateStatus(ThingStatus.ONLINE);
}
updateState(CHANNEL_VARSTATE, new DecimalType(state));
}
}
}

View File

@@ -0,0 +1,64 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.lutron.internal.handler;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.lutron.internal.discovery.project.ComponentType;
import org.openhab.binding.lutron.internal.keypadconfig.KeypadConfigTabletopSeetouch;
import org.openhab.core.thing.Thing;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Handler responsible for communicating with Lutron Tabletop seeTouch keypads used in RadioRA2 and Homeworks QS systems
* (e.g. RR-T5RL, RR-T10RL, RR-T15RL, etc.)
*
* @author Bob Adair - Initial contribution
*/
@NonNullByDefault
public class TabletopKeypadHandler extends BaseKeypadHandler {
private final Logger logger = LoggerFactory.getLogger(TabletopKeypadHandler.class);
@Override
protected void configureComponents(@Nullable String model) {
String mod = model == null ? "Generic" : model;
logger.debug("Configuring components for keypad model {}", model);
switch (mod) {
case "T5RL":
case "T10RL":
case "T15RL":
case "T5CRL":
case "T10CRL":
case "T15CRL":
buttonList = kp.getComponents(mod, ComponentType.BUTTON);
ledList = kp.getComponents(mod, ComponentType.LED);
break;
default:
logger.warn("No valid keypad model defined ({}). Assuming model T15RL.", mod);
// fall through
case "Generic":
buttonList = kp.getComponents("Generic", ComponentType.BUTTON);
ledList = kp.getComponents("Generic", ComponentType.LED);
break;
}
}
public TabletopKeypadHandler(Thing thing) {
super(thing);
kp = new KeypadConfigTabletopSeetouch();
}
}

View File

@@ -0,0 +1,214 @@
/**
* 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.time.ZoneId;
import java.time.ZonedDateTime;
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.core.library.types.DateTimeType;
import org.openhab.core.library.types.DecimalType;
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 with the RA2 time clock.
*
* @author Bob Adair - Initial contribution
*/
@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;
public TimeclockHandler(Thing thing) {
super(thing);
}
@Override
public int getIntegrationId() {
return integrationId;
}
@Override
public void initialize() {
Number id = (Number) getThing().getConfiguration().get("integrationId");
logger.debug("Initializing timeclock handler");
if (id == null) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "No integrationId");
return;
}
integrationId = id.intValue();
initDeviceState();
}
@Override
protected void initDeviceState() {
logger.debug("Initializing device state for Timeclock {}", 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");
queryTimeclock(ACTION_CLOCKMODE); // handleUpdate() will set thing status to online when response arrives
} else {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
}
}
@Override
public void channelLinked(ChannelUID channelUID) {
logger.debug("Handling channel link request for timeclock {}", integrationId);
if (channelUID.getId().equals(CHANNEL_CLOCKMODE)) {
queryTimeclock(ACTION_CLOCKMODE);
}
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
String channelID = channelUID.getId();
logger.debug("Handling timeclock command {} on channel {}", command, channelID);
if (channelUID.getId().equals(CHANNEL_CLOCKMODE)) {
if (command instanceof DecimalType) {
Integer mode = new Integer(((DecimalType) command).intValue());
timeclock(ACTION_CLOCKMODE, mode);
} else if (command instanceof RefreshType) {
queryTimeclock(ACTION_CLOCKMODE);
} else {
logger.debug("Invalid command type for clockmode channnel");
}
} else if (channelUID.getId().equals(CHANNEL_EXECEVENT)) {
if (command instanceof DecimalType) {
Integer index = new Integer(((DecimalType) command).intValue());
timeclock(ACTION_EXECEVENT, index);
} else {
logger.debug("Invalid command type for execevent channnel");
}
} else if (channelUID.getId().equals(CHANNEL_SUNRISE)) {
if (command instanceof RefreshType) {
queryTimeclock(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);
} else {
logger.debug("Invalid command type for sunset channnel");
}
} else if (channelUID.getId().equals(CHANNEL_ENABLEEVENT)) {
if (command instanceof DecimalType) {
Integer index = new Integer(((DecimalType) command).intValue());
timeclock(ACTION_SETEVENT, index, EVENT_ENABLE);
} else {
logger.debug("Invalid command type for enableevent channnel");
}
} else if (channelUID.getId().equals(CHANNEL_DISABLEEVENT)) {
if (command instanceof DecimalType) {
Integer index = new Integer(((DecimalType) command).intValue());
timeclock(ACTION_SETEVENT, index, EVENT_DISABLE);
} else {
logger.debug("Invalid command type for disableevent channnel");
}
} else {
logger.debug("Command received on invalid channel");
}
}
private @Nullable Calendar parseLutronTime(final String timeString) {
Integer hour, minute;
Calendar calendar = Calendar.getInstance();
try {
String hh = timeString.split(":", 2)[0];
String mm = timeString.split(":", 2)[1];
hour = Integer.parseInt(hh);
minute = Integer.parseInt(mm);
} catch (NumberFormatException | IndexOutOfBoundsException exception) {
logger.warn("Invaid time format received from timeclock {}", integrationId);
return null;
}
calendar.set(Calendar.HOUR_OF_DAY, hour);
calendar.set(Calendar.MINUTE, minute);
return calendar;
}
@Override
public void handleUpdate(LutronCommandType type, String... parameters) {
if (type != LutronCommandType.TIMECLOCK) {
return;
}
logger.debug("Handling update received from timeclock {}", integrationId);
try {
if (parameters.length >= 2 && ACTION_CLOCKMODE.toString().equals(parameters[0])) {
Integer mode = new Integer(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])) {
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])) {
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])) {
Integer index = new Integer(parameters[1]);
updateState(CHANNEL_EXECEVENT, new DecimalType(index));
} else if (parameters.length >= 3 && ACTION_SETEVENT.toString().equals(parameters[0])) {
Integer index = new Integer(parameters[1]);
Integer state = new Integer(parameters[2]);
if (state.equals(EVENT_ENABLE)) {
updateState(CHANNEL_ENABLEEVENT, new DecimalType(index));
} else if (state.equals(EVENT_DISABLE)) {
updateState(CHANNEL_DISABLEEVENT, new DecimalType(index));
}
}
} catch (NumberFormatException e) {
logger.debug("Encountered number format exception while handling update for timeclock {}", integrationId);
return;
}
}
}

View File

@@ -0,0 +1,125 @@
/**
* 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 java.util.Arrays;
import java.util.List;
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.core.thing.Thing;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Handler responsible for communicating with Lutron VCRX visor control receiver
*
* @author Bob Adair - Initial contribution
*/
@NonNullByDefault
public class VcrxHandler extends BaseKeypadHandler {
private static enum Component implements KeypadComponent {
BUTTON1(1, "button1", "Button 1", ComponentType.BUTTON),
BUTTON2(2, "button2", "Button 2", ComponentType.BUTTON),
BUTTON3(3, "button3", "Button 3", ComponentType.BUTTON),
BUTTON4(4, "button4", "Button 4", ComponentType.BUTTON),
BUTTON5(5, "button5", "Button 5", ComponentType.BUTTON),
BUTTON6(6, "button6", "Button 6", ComponentType.BUTTON),
CCI1(30, "cci1", "CCI 1", ComponentType.CCI),
CCI2(31, "cci2", "CCI 2", ComponentType.CCI),
CCI3(32, "cci3", "CCI 3", ComponentType.CCI),
CCI4(33, "cci4", "CCI 4", ComponentType.CCI),
LED1(81, "led1", "LED 1", ComponentType.LED),
LED2(82, "led2", "LED 2", ComponentType.LED),
LED3(83, "led3", "LED 3", ComponentType.LED),
LED4(84, "led4", "LED 4", ComponentType.LED),
LED5(85, "led5", "LED 5", ComponentType.LED),
LED6(86, "led6", "LED 6", ComponentType.LED);
private final int id;
private final String channel;
private final String description;
private final ComponentType type;
Component(int id, String channel, String description, ComponentType type) {
this.id = id;
this.channel = channel;
this.description = description;
this.type = type;
}
@Override
public int id() {
return this.id;
}
@Override
public String channel() {
return this.channel;
}
@Override
public String description() {
return this.description;
}
@Override
public ComponentType type() {
return type;
}
}
private static final List<Component> BUTTON_GROUP = Arrays.asList(Component.BUTTON1, Component.BUTTON2,
Component.BUTTON3, Component.BUTTON4, Component.BUTTON5, Component.BUTTON6);
private static final List<Component> LED_GROUP = Arrays.asList(Component.LED1, Component.LED2, Component.LED3,
Component.LED4, Component.LED5, Component.LED6);
private static final List<Component> CCI_GROUP = Arrays.asList(Component.CCI1, Component.CCI2, Component.CCI3,
Component.CCI4);
private final Logger logger = LoggerFactory.getLogger(VcrxHandler.class);
@Override
protected boolean isLed(int id) {
return (id >= 81 && id <= 86);
}
@Override
protected boolean isButton(int id) {
return (id >= 1 && id <= 6);
}
@Override
protected boolean isCCI(int id) {
return (id >= 30 && id <= 33);
}
@Override
protected void configureComponents(@Nullable String model) {
logger.debug("Configuring components for VCRX");
buttonList.addAll(BUTTON_GROUP);
ledList.addAll(LED_GROUP);
cciList.addAll(CCI_GROUP);
}
public VcrxHandler(Thing thing) {
super(thing);
}
}

View File

@@ -0,0 +1,104 @@
/**
* 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.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.lutron.internal.KeypadComponent;
import org.openhab.binding.lutron.internal.discovery.project.ComponentType;
import org.openhab.core.thing.Thing;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Handler responsible for communicating with the virtual buttons on the RadioRA2 main repeater
*
* @author Bob Adair - Initial contribution
*/
@NonNullByDefault
public class VirtualKeypadHandler extends BaseKeypadHandler {
private static final String MODEL_OPTION_CASETA = "Caseta";
private static final String MODEL_OPTION_OTHER = "Other";
private class Component implements KeypadComponent {
private final int id;
private final String channel;
private final String description;
private final ComponentType type;
Component(int id, String channel, String description, ComponentType type) {
this.id = id;
this.channel = channel;
this.description = description;
this.type = type;
}
@Override
public int id() {
return id;
}
@Override
public String channel() {
return channel;
}
@Override
public String description() {
return description;
}
@Override
public ComponentType type() {
return type;
}
}
private final Logger logger = LoggerFactory.getLogger(VirtualKeypadHandler.class);
@Override
protected boolean isLed(int id) {
return (id >= 101 && id <= 200);
}
@Override
protected boolean isButton(int id) {
return (id >= 1 && id <= 100);
}
@Override
protected boolean isCCI(int id) {
return false;
}
@Override
protected void configureComponents(@Nullable String model) {
String mod = model == null ? MODEL_OPTION_OTHER : model;
logger.debug("Configuring components for virtual keypad for model {}", mod);
boolean caseta = mod.equalsIgnoreCase(MODEL_OPTION_CASETA);
for (int x = 1; x <= 100; x++) {
buttonList.add(new Component(x, String.format("button%d", x), "Virtual Button", ComponentType.BUTTON));
if (!caseta) { // Caseta scene buttons have no virtual LEDs
ledList.add(new Component(x + 100, String.format("led%d", x), "Virtual LED", ComponentType.LED));
}
}
}
public VirtualKeypadHandler(Thing thing) {
super(thing);
// Mark all channels "Advanced" since most are unlikely to be used in any particular config
advancedChannels = true;
}
}

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.handler;
import java.util.Arrays;
import java.util.List;
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.core.thing.Thing;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Handler responsible for communicating with the Lutron Wallbox Input Closure Interface (WCI)
*
* @author Bob Adair - Initial contribution
*/
@NonNullByDefault
public class WciHandler extends BaseKeypadHandler {
private static enum Component implements KeypadComponent {
BUTTON1(1, "button1", "Button 1", ComponentType.BUTTON),
BUTTON2(2, "button2", "Button 2", ComponentType.BUTTON),
BUTTON3(3, "button3", "Button 3", ComponentType.BUTTON),
BUTTON4(4, "button4", "Button 4", ComponentType.BUTTON),
BUTTON5(5, "button5", "Button 5", ComponentType.BUTTON),
BUTTON6(6, "button6", "Button 6", ComponentType.BUTTON),
BUTTON7(7, "button7", "Button 7", ComponentType.BUTTON),
BUTTON8(8, "button8", "Button 8", ComponentType.BUTTON),
LED1(81, "led1", "LED 1", ComponentType.LED),
LED2(82, "led2", "LED 2", ComponentType.LED),
LED3(83, "led3", "LED 3", ComponentType.LED),
LED4(84, "led4", "LED 4", ComponentType.LED),
LED5(85, "led5", "LED 5", ComponentType.LED),
LED6(86, "led6", "LED 6", ComponentType.LED),
LED7(87, "led7", "LED 7", ComponentType.LED),
LED8(88, "led8", "LED 8", ComponentType.LED);
private final int id;
private final String channel;
private final String description;
private final ComponentType type;
Component(int id, String channel, String description, ComponentType type) {
this.id = id;
this.channel = channel;
this.description = description;
this.type = type;
}
@Override
public int id() {
return this.id;
}
@Override
public String channel() {
return this.channel;
}
@Override
public String description() {
return this.description;
}
@Override
public ComponentType type() {
return type;
}
}
private static final List<KeypadComponent> BUTTON_LIST = Arrays.asList(Component.BUTTON1, Component.BUTTON2,
Component.BUTTON3, Component.BUTTON4, Component.BUTTON5, Component.BUTTON6, Component.BUTTON7,
Component.BUTTON8);
private static final List<KeypadComponent> LED_LIST = Arrays.asList(Component.LED1, Component.LED2, Component.LED3,
Component.LED4, Component.LED5, Component.LED6, Component.LED7, Component.LED8);
private final Logger logger = LoggerFactory.getLogger(WciHandler.class);
@Override
protected boolean isLed(int id) {
return (id >= 81 && id <= 88);
}
@Override
protected boolean isButton(int id) {
return (id >= 1 && id <= 8);
}
@Override
protected boolean isCCI(int id) {
return false;
}
@Override
protected void configureComponents(@Nullable String model) {
logger.trace("Configuring components for WCI");
buttonList.addAll(BUTTON_LIST);
ledList.addAll(LED_LIST);
}
public WciHandler(Thing thing) {
super(thing);
}
}

View File

@@ -0,0 +1,27 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.lutron.internal.hw;
import static org.openhab.binding.lutron.internal.LutronBindingConstants.BINDING_ID;
import org.openhab.core.thing.ThingTypeUID;
/**
* Defines common constants, which are used across the whole binding.
*
* @author Andrew Shilliday - Initial contribution
*/
public class HwConstants {
public static final ThingTypeUID THING_TYPE_HWSERIALBRIDGE = new ThingTypeUID(BINDING_ID, "hwserialbridge");
public static final ThingTypeUID THING_TYPE_HWDIMMER = new ThingTypeUID(BINDING_ID, "hwdimmer");
}

View File

@@ -0,0 +1,51 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.lutron.internal.hw;
/**
* Configuration settings for a {@link org.openhab.binding.lutron.handler.HWDimmerHandler}.
*
* @author Andrew Shilliday - Initial contribution
*/
public class HwDimmerConfig {
private static final int DEFAULT_FADE = 1;
private static final int DEFAULT_LEVEL = 100;
private String address;
private Integer fadeTime = DEFAULT_FADE;
private Integer defaultLevel = DEFAULT_LEVEL;
public String getAddress() {
return this.address;
}
public void setAddress(String address) {
this.address = address;
}
public Integer getFadeTime() {
return this.fadeTime;
}
public void setFadeTime(Integer fadeTime) {
this.fadeTime = fadeTime;
}
public Integer getDefaultLevel() {
return defaultLevel;
}
public void setDefaultLevel(Integer defaultLevel) {
this.defaultLevel = defaultLevel;
}
}

View File

@@ -0,0 +1,118 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.lutron.internal.hw;
import static org.openhab.binding.lutron.internal.LutronBindingConstants.CHANNEL_LIGHTLEVEL;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.PercentType;
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.BaseThingHandler;
import org.openhab.core.types.Command;
/**
* This class extends the BaseThingHandler to support HomeWorks Dimmer modules.
*
* @author Andrew Shilliday - Initial contribution
*
*/
public class HwDimmerHandler extends BaseThingHandler {
private String address;
private Integer fadeTime = 1;
private Integer defaultLevel = 100;
public HwDimmerHandler(Thing thing) {
super(thing);
}
@Override
public void initialize() {
HwDimmerConfig config = getThing().getConfiguration().as(HwDimmerConfig.class);
address = config.getAddress();
if (address == null || address.isEmpty()) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Address not set");
return;
}
fadeTime = config.getFadeTime();
defaultLevel = config.getDefaultLevel();
if (getThing().getBridgeUID() == null) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "No bridge configured");
return;
}
updateStatus(ThingStatus.ONLINE);
queryLevel();
}
public String getAddress() {
return address;
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
if (channelUID.getId().equals(CHANNEL_LIGHTLEVEL)) {
if (command instanceof Number) {
int level = ((Number) command).intValue();
outputLevel(level);
} else if (command.equals(OnOffType.ON)) {
outputLevel(defaultLevel);
} else if (command.equals(OnOffType.OFF)) {
outputLevel(0);
}
}
}
private HwSerialBridgeHandler getBridgeHandler() {
Bridge bridge = getBridge();
if (bridge == null) {
return null;
} else if (!(bridge.getHandler() instanceof HwSerialBridgeHandler)) {
return null;
} else {
return (HwSerialBridgeHandler) bridge.getHandler();
}
}
private void queryLevel() {
HwSerialBridgeHandler bridgeHandler = getBridgeHandler();
if (bridgeHandler == null) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.HANDLER_MISSING_ERROR, "No bridge associated");
return;
}
String cmd = String.format("RDL, %s", address);
bridgeHandler.sendCommand(cmd);
}
private void outputLevel(Number level) {
HwSerialBridgeHandler bridgeHandler = getBridgeHandler();
if (bridgeHandler == null) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.HANDLER_MISSING_ERROR, "No bridge associated");
return;
}
String cmd = String.format("FADEDIM, %s, %s, 0, %s", level, fadeTime, address);
bridgeHandler.sendCommand(cmd);
}
public void handleLevelChange(Integer level) {
updateState(CHANNEL_LIGHTLEVEL, new PercentType(level));
}
}

View File

@@ -0,0 +1,119 @@
/**
* 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.hw;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.atomic.AtomicBoolean;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.lutron.internal.LutronHandlerFactory;
import org.openhab.core.config.discovery.AbstractDiscoveryService;
import org.openhab.core.config.discovery.DiscoveryResult;
import org.openhab.core.config.discovery.DiscoveryResultBuilder;
import org.openhab.core.config.discovery.DiscoveryService;
import org.openhab.core.thing.ThingUID;
import org.openhab.core.thing.binding.ThingHandler;
import org.openhab.core.thing.binding.ThingHandlerService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
*
* The Discovery Service for Lutron HomeWorks processors. There is no great way to automatically
* discover modules in the legacy HomeWorks processor (that I know of) so this service simply iterates
* through possible addresses and asks for status on that address. If it's a valid module, the processor will return
* with the dimmer status and it will be discovered.
*
* @author Andrew Shilliday - Initial contribution
*/
public class HwDiscoveryService extends AbstractDiscoveryService implements DiscoveryService, ThingHandlerService {
private Logger logger = LoggerFactory.getLogger(HwDiscoveryService.class);
private final AtomicBoolean isScanning = new AtomicBoolean(false);
private @NonNullByDefault({}) HwSerialBridgeHandler handler;
public HwDiscoveryService() {
super(LutronHandlerFactory.HW_DISCOVERABLE_DEVICE_TYPES_UIDS, 10);
}
@Override
public void setThingHandler(@Nullable ThingHandler handler) {
if (handler instanceof HwSerialBridgeHandler) {
this.handler = (HwSerialBridgeHandler) handler;
this.handler.setDiscoveryService(this);
}
}
@Override
public @Nullable ThingHandler getThingHandler() {
return handler;
}
@Override
public void activate() {
super.activate(null);
}
@Override
public void deactivate() {
super.deactivate();
}
@Override
protected void startScan() {
scheduler.submit(() -> {
if (isScanning.compareAndSet(false, true)) {
try {
logger.debug("Starting scan for HW Dimmers");
for (int m = 1; m <= 8; m++) { // Modules
for (int o = 1; o <= 4; o++) { // Outputs
String address = String.format("[01:01:00:%02d:%02d]", m, o);
handler.sendCommand("RDL, " + address);
Thread.sleep(5);
}
}
} catch (InterruptedException e) {
logger.debug("Scan interrupted");
} finally {
isScanning.set(false);
}
}
});
}
/**
* Called by the bridge when it receives a status update for a dimmer that is not registered.
*/
public void declareUnknownDimmer(String address) {
if (address == null) {
logger.info("Discovered HomeWorks dimmer with no address");
return;
}
String addressUid = address.replaceAll("[\\[\\]]", "").replaceAll(":", "-");
ThingUID bridgeUID = this.handler.getThing().getUID();
ThingUID uid = new ThingUID(HwConstants.THING_TYPE_HWDIMMER, bridgeUID, addressUid);
Map<String, Object> props = new HashMap<>();
props.put("address", address);
DiscoveryResult result = DiscoveryResultBuilder.create(uid).withBridge(bridgeUID).withProperties(props)
.withRepresentationProperty("address").build();
thingDiscovered(result);
logger.debug("Discovered {}", uid);
}
}

View File

@@ -0,0 +1,53 @@
/**
* 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.hw;
/**
* Configuration settings for an {@link org.openhab.binding.lutron.handler.HWSerialBridgeHandler}.
*
* @author Andrew Shilliday - Initial contribution
*/
public class HwSerialBridgeConfig {
public static final String SERIAL_PORT = "serialPort";
public static final String BAUD = "baudRate";
public static final String UPDATE_TIME = "updateTime";
public static final Integer DEFAULT_BAUD = 9600;
private String serialPort;
private Integer baudRate = DEFAULT_BAUD;
private Boolean updateTime;
public String getSerialPort() {
return serialPort;
}
public void setSerialPort(String serialPort) {
this.serialPort = serialPort;
}
public Integer getBaudRate() {
return baudRate;
}
public void setBaudRate(Integer baudRate) {
this.baudRate = baudRate;
}
public Boolean getUpdateTime() {
return updateTime;
}
public void setUpdateTime(Boolean updateTime) {
this.updateTime = updateTime;
}
}

View File

@@ -0,0 +1,272 @@
/**
* 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.hw;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.Collection;
import java.util.Collections;
import java.util.TooManyListenersException;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import org.openhab.core.io.transport.serial.PortInUseException;
import org.openhab.core.io.transport.serial.SerialPort;
import org.openhab.core.io.transport.serial.SerialPortEvent;
import org.openhab.core.io.transport.serial.SerialPortEventListener;
import org.openhab.core.io.transport.serial.SerialPortIdentifier;
import org.openhab.core.io.transport.serial.SerialPortManager;
import org.openhab.core.io.transport.serial.UnsupportedCommOperationException;
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.ThingHandlerService;
import org.openhab.core.types.Command;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
*
* This is the main handler for HomeWorks RS232 Processors.
*
* @author Andrew Shilliday - Initial contribution
*
*/
public class HwSerialBridgeHandler extends BaseBridgeHandler implements SerialPortEventListener {
private final Logger logger = LoggerFactory.getLogger(HwSerialBridgeHandler.class);
private final DateTimeFormatter dateFormat = DateTimeFormatter.ofPattern("MM/dd/yyyy");
private final DateTimeFormatter timeFormat = DateTimeFormatter.ofPattern("HH:mm:ss");
private String serialPortName;
private int baudRate;
private Boolean updateTime;
private ScheduledFuture<?> updateTimeJob;
private HwDiscoveryService discoveryService;
private final SerialPortManager serialPortManager;
private SerialPort serialPort;
private OutputStreamWriter serialOutput;
private BufferedReader serialInput;
public HwSerialBridgeHandler(Bridge bridge, SerialPortManager serialPortManager) {
super(bridge);
this.serialPortManager = serialPortManager;
}
@Override
public void initialize() {
logger.debug("Initializing the Lutron HomeWorks RS232 bridge handler");
HwSerialBridgeConfig configuration = getConfigAs(HwSerialBridgeConfig.class);
serialPortName = configuration.getSerialPort();
updateTime = configuration.getUpdateTime();
if (configuration.getBaudRate() == null) {
baudRate = HwSerialBridgeConfig.DEFAULT_BAUD;
} else {
baudRate = configuration.getBaudRate().intValue();
}
if (serialPortName == null) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Serial port not specified");
return;
}
logger.debug("Lutron HomeWorks RS232 Bridge Handler Initializing.");
logger.debug(" Serial Port: {},", serialPortName);
logger.debug(" Baud: {},", baudRate);
scheduler.execute(() -> openConnection());
}
public void setDiscoveryService(HwDiscoveryService discoveryService) {
this.discoveryService = discoveryService;
}
private void openConnection() {
SerialPortIdentifier portIdentifier = serialPortManager.getIdentifier(serialPortName);
if (portIdentifier == null) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Invalid port: " + serialPortName);
return;
}
try {
logger.info("Connecting to Lutron HomeWorks Processor using {}.", serialPortName);
serialPort = portIdentifier.open(this.getClass().getName(), 2000);
logger.debug("Connection established using {}. Configuring IO parameters. ", serialPortName);
int db = SerialPort.DATABITS_8, sb = SerialPort.STOPBITS_1, p = SerialPort.PARITY_NONE;
serialPort.setSerialPortParams(baudRate, db, sb, p);
serialPort.enableReceiveThreshold(1);
serialPort.disableReceiveTimeout();
serialOutput = new OutputStreamWriter(serialPort.getOutputStream(), "US-ASCII");
serialInput = new BufferedReader(new InputStreamReader(serialPort.getInputStream(), "US-ASCII"));
serialPort.addEventListener(this);
serialPort.notifyOnDataAvailable(true);
logger.debug("Sending monitoring commands.");
sendCommand("PROMPTOFF");
sendCommand("KBMOFF");
sendCommand("KLMOFF");
sendCommand("GSMOFF");
sendCommand("DLMON"); // Turn on dimmer monitoring
updateStatus(ThingStatus.ONLINE);
if (updateTime) {
startUpdateProcessorTimeJob();
}
} catch (PortInUseException portInUseException) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Port in use: " + serialPortName);
} catch (UnsupportedCommOperationException | IOException e) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Communication error");
} catch (TooManyListenersException e) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
"Too many listeners to serial port.");
}
}
private void startUpdateProcessorTimeJob() {
if (updateTimeJob != null) {
logger.debug("Canceling old scheduled job");
updateTimeJob.cancel(false);
updateTimeJob = null;
}
updateTimeJob = scheduler.scheduleWithFixedDelay(() -> updateProcessorTime(), 0, 1, TimeUnit.DAYS);
}
private void updateProcessorTime() {
LocalDate date = LocalDate.now();
String dateString = date.format(dateFormat);
String timeString = date.format(timeFormat);
logger.debug("Updating HomeWorks processor date and time to {} {}", dateString, timeString);
if (!this.getBridge().getStatus().equals(ThingStatus.ONLINE)) {
logger.warn("HomeWorks Bridge is offline and cannot update time on HomeWorks processor.");
if (updateTimeJob != null) {
updateTimeJob.cancel(false);
updateTimeJob = null;
}
return;
}
sendCommand("SD, " + dateString);
sendCommand("ST, " + timeString);
}
@Override
public Collection<Class<? extends ThingHandlerService>> getServices() {
return Collections.singleton(HwDiscoveryService.class);
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
logger.debug("Unexpected command for HomeWorks Bridge: {} - {}", channelUID, command);
}
private void handleIncomingMessage(String line) {
if (line == null || line.isEmpty()) {
return;
}
logger.debug("Received message from HomeWorks processor: {}", line);
String[] data = line.replaceAll("\\s", "").toUpperCase().split(",");
if ("DL".equals(data[0])) {
try {
String address = data[1];
Integer level = Integer.parseInt(data[2]);
HwDimmerHandler handler = findHandler(address);
if (handler == null) {
discoveryService.declareUnknownDimmer(address);
} else {
handler.handleLevelChange(level);
}
} catch (RuntimeException e) {
logger.error("Error parsing incoming message", e);
}
}
}
private HwDimmerHandler findHandler(String address) {
for (Thing thing : getThing().getThings()) {
if (thing.getHandler() instanceof HwDimmerHandler) {
HwDimmerHandler handler = (HwDimmerHandler) thing.getHandler();
if (address.equals(handler.getAddress())) {
return handler;
}
}
}
return null;
}
/**
* Receives Serial Port Events and reads Serial Port Data.
*
* @param serialPortEvent
*/
@Override
public void serialEvent(SerialPortEvent serialPortEvent) {
if (serialPortEvent.getEventType() == SerialPortEvent.DATA_AVAILABLE) {
try {
while (true) {
String messageLine = serialInput.readLine();
if (messageLine == null) {
break;
}
handleIncomingMessage(messageLine);
}
} catch (IOException e) {
logger.debug("Error reading from serial port: {}", e.getMessage(), e);
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Error reading from port");
}
}
}
public void sendCommand(String command) {
try {
logger.debug("HomeWorks bridge sending command: {}", command);
serialOutput.write(command + "\r");
serialOutput.flush();
} catch (IOException e) {
logger.debug("Error writing to serial port: {}", e.getMessage(), e);
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Error writing to port.");
}
}
@Override
public void dispose() {
logger.info("HomeWorks bridge being disposed.");
if (serialPort != null) {
serialPort.close();
}
serialPort = null;
serialInput = null;
serialOutput = null;
if (updateTimeJob != null) {
updateTimeJob.cancel(false);
}
logger.debug("Finished disposing bridge.");
}
}

View File

@@ -0,0 +1,142 @@
/**
* 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.keypadconfig;
import java.util.Collections;
import java.util.HashMap;
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.KeypadComponent;
import org.openhab.binding.lutron.internal.discovery.project.ComponentType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Abstract base class for keypad configuration definition classes
*
* @author Bob Adair - Initial contribution
*/
@NonNullByDefault
public abstract class KeypadConfig {
private final Logger logger = LoggerFactory.getLogger(KeypadConfig.class);
protected final HashMap<String, @Nullable List<KeypadComponent>> modelData = new HashMap<>();
public abstract boolean isCCI(int id);
public abstract boolean isButton(int id);
public abstract boolean isLed(int id);
/**
* Get a list of all {@link KeypadComponent}s for the specified keypad model
*
* @param model The keypad model for which to return components.
* @return List of components. Will be empty if no components match.
*/
public List<KeypadComponent> getComponents(String model) {
return getComponents(model, null);
}
/**
* Get a list of {@link KeypadComponent}s of the specified type for the specified keypad model
*
* @param model The keypad model for which to return components.
* @param type The component type to include, or null for all components.
* @return List of components. Will be empty if no components match.
*/
public List<KeypadComponent> getComponents(String model, @Nullable ComponentType type) {
List<KeypadComponent> filteredList = new LinkedList<>();
List<KeypadComponent> cList = modelData.get(model);
if (cList == null) {
logger.debug("Keypad components lookup using invalid keypad model: {}", model);
return filteredList;
} else if (type == null) {
return cList;
} else {
for (KeypadComponent i : cList) {
if (i.type() == type) {
filteredList.add(i);
}
}
return filteredList;
}
}
/**
* Get a list of all component IDs for the specified keypad model
*
* @param model The keypad model for which to return component IDs.
* @return List of component IDs. Will be empty if no components match.
*/
public @Nullable List<Integer> getComponentIds(String model) {
return getComponentIds(model, null);
}
/**
* Get a list of component IDs of the specified type for the specified keypad model
*
* @param model The keypad model for which to return component IDs.
* @param type The component type to include, or null for all components.
* @return List of component IDs. Will be empty if no components match.
*/
public List<Integer> getComponentIds(String model, @Nullable ComponentType type) {
List<Integer> idList = new LinkedList<>();
List<KeypadComponent> cList = modelData.get(model);
if (cList == null) {
logger.debug("Keypad component IDs lookup using invalid keypad model: {}", model);
} else {
for (KeypadComponent i : cList) {
if (type == null || i.type() == type) {
idList.add(i.id());
}
}
}
return idList;
}
/**
* Determine keypad model from list of button component IDs
*
* @param buttonIds List of button component IDs for a keypad. Must be in ascending order.
* @return String containing the keypad model, or null if no models match.
*/
public @Nullable String determineModelFromComponentIds(List<Integer> buttonIds) {
for (String k : modelData.keySet()) {
List<Integer> modelButtonIds = getComponentIds(k, ComponentType.BUTTON);
Collections.sort(modelButtonIds); // make sure button IDs are in ascending order for comparison
if (modelButtonIds.equals(buttonIds)) {
return k;
}
}
return null;
}
/**
* Utility routine to concatenate multiple lists of {@link KeypadComponent}s
*
* @param lists Lists to concatenate
* @return Concatenated list
*/
@SafeVarargs
protected static final List<KeypadComponent> combinedList(final List<KeypadComponent>... lists) {
List<KeypadComponent> newlist = new LinkedList<>();
for (List<KeypadComponent> list : lists) {
newlist.addAll(list);
}
return newlist;
}
}

View File

@@ -0,0 +1,156 @@
/**
* 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.keypadconfig;
import java.util.Arrays;
import java.util.List;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.lutron.internal.KeypadComponent;
import org.openhab.binding.lutron.internal.discovery.project.ComponentType;
/**
* Keypad configuration definition for Tabletop seeTouch line
*
* @author Bob Adair - Initial contribution
*/
@NonNullByDefault
public final class KeypadConfigGrafikEye extends KeypadConfig {
private static enum Component implements KeypadComponent {
BUTTON1(70, "button1", "Button 1", ComponentType.BUTTON), // Scene button 1
BUTTON2(71, "button2", "Button 2", ComponentType.BUTTON), // Scene button 2
BUTTON3(76, "button3", "Button 3", ComponentType.BUTTON), // Scene button 3
BUTTON4(77, "button4", "Button 4", ComponentType.BUTTON), // Scene button 4
BUTTON5(83, "button5", "Button 5", ComponentType.BUTTON), // Scene button 5/Off
BUTTON10(38, "button10", "Button 10", ComponentType.BUTTON), // Col 1
BUTTON11(39, "button11", "Button 11", ComponentType.BUTTON), // Col 1
BUTTON12(40, "button12", "Button 12", ComponentType.BUTTON), // Col 1
LOWER1(41, "buttonlower1", "Lower button col 1", ComponentType.BUTTON), // Col 1 lower
RAISE1(47, "buttonraise1", "Raise button col 1", ComponentType.BUTTON), // Col 1 raise
BUTTON20(44, "button20", "Button 20", ComponentType.BUTTON), // Col 2
BUTTON21(45, "button21", "Button 21", ComponentType.BUTTON), // Col 2
BUTTON22(46, "button22", "Button 22", ComponentType.BUTTON), // Col 2
LOWER2(52, "buttonlower2", "Lower button col 2", ComponentType.BUTTON), // Col 2 lower
RAISE2(53, "buttonraise2", "Raise button col 2", ComponentType.BUTTON), // Col 2 raise
BUTTON30(50, "button30", "Button 30", ComponentType.BUTTON), // Col 3
BUTTON31(51, "button31", "Button 31", ComponentType.BUTTON), // Col 3
BUTTON32(56, "button32", "Button 32", ComponentType.BUTTON), // Col 3
LOWER3(57, "buttonlower3", "Lower button col 3", ComponentType.BUTTON), // Col 3 lower
RAISE3(58, "buttonraise3", "Raise button col 3", ComponentType.BUTTON), // Col 3 raise
CCI1(163, "cci1", "CCI 1", ComponentType.CCI),
LED1(201, "led1", "LED 1", ComponentType.LED), // Scene button LEDs
LED2(210, "led2", "LED 2", ComponentType.LED),
LED3(219, "led3", "LED 3", ComponentType.LED),
LED4(228, "led4", "LED 4", ComponentType.LED),
LED5(237, "led5", "LED 5", ComponentType.LED),
LED10(174, "led10", "LED 10", ComponentType.LED), // Col 1 LEDs
LED11(175, "led11", "LED 11", ComponentType.LED),
LED12(211, "led12", "LED 12", ComponentType.LED),
LED20(183, "led20", "LED 20", ComponentType.LED), // Col 2 LEDs
LED21(184, "led21", "LED 21", ComponentType.LED),
LED22(220, "led22", "LED 22", ComponentType.LED),
LED30(192, "led30", "LED 30", ComponentType.LED), // Col 3 LEDs
LED31(193, "led31", "LED 31", ComponentType.LED),
LED32(229, "led32", "LED 32", ComponentType.LED);
private final int id;
private final String channel;
private final String description;
private final ComponentType type;
Component(int id, String channel, String description, ComponentType type) {
this.id = id;
this.channel = channel;
this.description = description;
this.type = type;
}
@Override
public int id() {
return id;
}
@Override
public String channel() {
return channel;
}
@Override
public String description() {
return description;
}
@Override
public ComponentType type() {
return type;
}
}
private static final List<KeypadComponent> SCENE_BUTTON_GROUP = Arrays.asList(Component.BUTTON1, Component.BUTTON2,
Component.BUTTON3, Component.BUTTON4, Component.BUTTON5);
private static final List<KeypadComponent> SCENE_LED_GROUP = Arrays.asList(Component.LED1, Component.LED2,
Component.LED3, Component.LED4, Component.LED5);
private static final List<KeypadComponent> CCI_GROUP = Arrays.asList(Component.CCI1);
private static final List<KeypadComponent> COL1_BUTTON_GROUP = Arrays.asList(Component.BUTTON10, Component.BUTTON11,
Component.BUTTON12, Component.LOWER1, Component.RAISE1);
private static final List<KeypadComponent> COL2_BUTTON_GROUP = Arrays.asList(Component.BUTTON20, Component.BUTTON21,
Component.BUTTON22, Component.LOWER2, Component.RAISE2);
private static final List<KeypadComponent> COL3_BUTTON_GROUP = Arrays.asList(Component.BUTTON30, Component.BUTTON31,
Component.BUTTON32, Component.LOWER3, Component.RAISE3);
private static final List<KeypadComponent> COL1_LED_GROUP = Arrays.asList(Component.LED10, Component.LED11,
Component.LED12);
private static final List<KeypadComponent> COL2_LED_GROUP = Arrays.asList(Component.LED20, Component.LED21,
Component.LED22);
private static final List<KeypadComponent> COL3_LED_GROUP = Arrays.asList(Component.LED30, Component.LED31,
Component.LED32);
@Override
public boolean isLed(int id) {
return (id >= 174 && id <= 237);
}
@Override
public boolean isButton(int id) {
return (id >= 38 && id <= 83);
}
@Override
public boolean isCCI(int id) {
return (id == 163);
}
public KeypadConfigGrafikEye() {
modelData.put("0COL", combinedList(SCENE_BUTTON_GROUP, CCI_GROUP, SCENE_LED_GROUP));
modelData.put("1COL",
combinedList(SCENE_BUTTON_GROUP, COL1_BUTTON_GROUP, CCI_GROUP, SCENE_LED_GROUP, COL1_LED_GROUP));
modelData.put("2COL", combinedList(SCENE_BUTTON_GROUP, COL1_BUTTON_GROUP, COL2_BUTTON_GROUP, CCI_GROUP,
SCENE_LED_GROUP, COL1_LED_GROUP, COL2_LED_GROUP));
modelData.put("3COL", combinedList(SCENE_BUTTON_GROUP, COL1_BUTTON_GROUP, COL2_BUTTON_GROUP, COL3_BUTTON_GROUP,
CCI_GROUP, SCENE_LED_GROUP, COL1_LED_GROUP, COL2_LED_GROUP, COL3_LED_GROUP));
}
}

View File

@@ -0,0 +1,145 @@
/**
* 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.keypadconfig;
import java.util.Arrays;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.lutron.internal.KeypadComponent;
import org.openhab.binding.lutron.internal.discovery.project.ComponentType;
/**
* Keypad configuration definition for International seeTouch line
*
* @author Bob Adair - Initial contribution
*/
@NonNullByDefault
public final class KeypadConfigIntlSeetouch extends KeypadConfig {
private static enum Component implements KeypadComponent {
BUTTON1(1, "button1", "Button 1", ComponentType.BUTTON),
BUTTON2(2, "button2", "Button 2", ComponentType.BUTTON),
BUTTON3(3, "button3", "Button 3", ComponentType.BUTTON),
BUTTON4(4, "button4", "Button 4", ComponentType.BUTTON),
BUTTON5(5, "button5", "Button 5", ComponentType.BUTTON),
BUTTON6(6, "button6", "Button 6", ComponentType.BUTTON),
BUTTON7(7, "button7", "Button 7", ComponentType.BUTTON),
BUTTON8(8, "button8", "Button 8", ComponentType.BUTTON),
BUTTON9(9, "button9", "Button 9", ComponentType.BUTTON),
BUTTON10(10, "button10", "Button 10", ComponentType.BUTTON),
LOWER1(18, "buttonlower", "Lower button", ComponentType.BUTTON),
RAISE1(19, "buttonraise", "Raise button", ComponentType.BUTTON),
CCI1(25, "cci1", "", ComponentType.CCI),
CCI2(26, "cci2", "", ComponentType.CCI),
LED1(81, "led1", "LED 1", ComponentType.LED),
LED2(82, "led2", "LED 2", ComponentType.LED),
LED3(83, "led3", "LED 3", ComponentType.LED),
LED4(84, "led4", "LED 4", ComponentType.LED),
LED5(85, "led5", "LED 5", ComponentType.LED),
LED6(86, "led6", "LED 6", ComponentType.LED),
LED7(87, "led7", "LED 7", ComponentType.LED),
LED8(88, "led8", "LED 8", ComponentType.LED),
LED9(89, "led9", "LED 9", ComponentType.LED),
LED10(90, "led10", "LED 10", ComponentType.LED);
private final int id;
private final String channel;
private final String description;
private final ComponentType type;
Component(int id, String channel, String description, ComponentType type) {
this.id = id;
this.channel = channel;
this.description = description;
this.type = type;
}
@Override
public int id() {
return id;
}
@Override
public String channel() {
return channel;
}
@Override
public String description() {
return description;
}
@Override
public ComponentType type() {
return type;
}
}
@Override
public boolean isLed(int id) {
return (id >= 81 && id <= 90);
}
@Override
public boolean isButton(int id) {
return ((id >= 1 && id <= 10) || (id >= 18 && id <= 19));
}
@Override
public boolean isCCI(int id) {
return (id >= 25 && id <= 26);
}
public KeypadConfigIntlSeetouch() {
modelData.put("2B", Arrays.asList(Component.BUTTON7, Component.BUTTON9, Component.LED7, Component.LED9,
Component.CCI1, Component.CCI2));
modelData.put("3B", Arrays.asList(Component.BUTTON6, Component.BUTTON8, Component.BUTTON10, Component.LED6,
Component.LED8, Component.LED10, Component.CCI1, Component.CCI2));
modelData.put("4B", Arrays.asList(Component.BUTTON2, Component.BUTTON4, Component.BUTTON7, Component.BUTTON9,
Component.LED2, Component.LED4, Component.LED7, Component.LED9, Component.CCI1, Component.CCI2));
modelData.put("5BRL",
Arrays.asList(Component.BUTTON6, Component.BUTTON7, Component.BUTTON8, Component.BUTTON9,
Component.BUTTON10, Component.LOWER1, Component.RAISE1, Component.LED6, Component.LED7,
Component.LED8, Component.LED9, Component.LED10, Component.CCI1, Component.CCI2));
modelData.put("6BRL",
Arrays.asList(Component.BUTTON1, Component.BUTTON3, Component.BUTTON5, Component.BUTTON6,
Component.BUTTON8, Component.BUTTON10, Component.LOWER1, Component.RAISE1, Component.LED1,
Component.LED3, Component.LED5, Component.LED6, Component.LED8, Component.LED10, Component.CCI1,
Component.CCI2));
modelData.put("7BRL",
Arrays.asList(Component.BUTTON2, Component.BUTTON4, Component.BUTTON6, Component.BUTTON7,
Component.BUTTON8, Component.BUTTON9, Component.BUTTON10, Component.LOWER1, Component.RAISE1,
Component.LED2, Component.LED4, Component.LED6, Component.LED7, Component.LED8, Component.LED9,
Component.LED10, Component.CCI1, Component.CCI2));
modelData.put("8BRL", Arrays.asList(Component.BUTTON1, Component.BUTTON3, Component.BUTTON5, Component.BUTTON6,
Component.BUTTON7, Component.BUTTON8, Component.BUTTON9, Component.BUTTON10, Component.LOWER1,
Component.RAISE1, Component.LED1, Component.LED3, Component.LED5, Component.LED6, Component.LED7,
Component.LED8, Component.LED9, Component.LED10, Component.CCI1, Component.CCI2));
modelData.put("10BRL",
Arrays.asList(Component.BUTTON1, Component.BUTTON2, Component.BUTTON3, Component.BUTTON4,
Component.BUTTON5, Component.BUTTON6, Component.BUTTON7, Component.BUTTON8, Component.BUTTON9,
Component.BUTTON10, Component.LOWER1, Component.RAISE1, Component.LED1, Component.LED2,
Component.LED3, Component.LED4, Component.LED5, Component.LED6, Component.LED7, Component.LED8,
Component.LED9, Component.LED10, Component.CCI1, Component.CCI2));
}
}

View File

@@ -0,0 +1,155 @@
/**
* 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.keypadconfig;
import java.util.Arrays;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.lutron.internal.KeypadComponent;
import org.openhab.binding.lutron.internal.discovery.project.ComponentType;
/**
* Keypad configuration definition for Palladiom keypad line
*
* @author Bob Adair - Initial contribution
*/
@NonNullByDefault
public final class KeypadConfigPalladiom extends KeypadConfig {
private static enum Component implements KeypadComponent {
BUTTON1(1, "button1", "Button 1", ComponentType.BUTTON),
BUTTON2(2, "button2", "Button 2", ComponentType.BUTTON),
BUTTON3(3, "button3", "Button 3", ComponentType.BUTTON),
BUTTON4(4, "button4", "Button 4", ComponentType.BUTTON),
BUTTON5(5, "button5", "Button 5", ComponentType.BUTTON),
BUTTON6(6, "button6", "Button 6", ComponentType.BUTTON),
BUTTON7(7, "button7", "Button 7", ComponentType.BUTTON),
BUTTON8(8, "button8", "Button 8", ComponentType.BUTTON),
LOWER1(16, "buttonlower1", "Lower button 1", ComponentType.BUTTON),
RAISE1(17, "buttonraise1", "Raise button 2", ComponentType.BUTTON),
LOWER2(18, "buttonlower2", "Lower button 3", ComponentType.BUTTON),
RAISE2(19, "buttonraise2", "Raise button 4", ComponentType.BUTTON),
LED1(81, "led1", "LED 1", ComponentType.LED),
LED2(82, "led2", "LED 2", ComponentType.LED),
LED3(83, "led3", "LED 3", ComponentType.LED),
LED4(84, "led4", "LED 4", ComponentType.LED),
LED5(85, "led5", "LED 5", ComponentType.LED),
LED6(86, "led6", "LED 6", ComponentType.LED),
LED7(87, "led7", "LED 7", ComponentType.LED),
LED8(88, "led8", "LED 8", ComponentType.LED);
private final int id;
private final String channel;
private final String description;
private final ComponentType type;
Component(int id, String channel, String description, ComponentType type) {
this.id = id;
this.channel = channel;
this.description = description;
this.type = type;
}
@Override
public int id() {
return id;
}
@Override
public String channel() {
return channel;
}
@Override
public String description() {
return description;
}
@Override
public ComponentType type() {
return type;
}
}
@Override
public boolean isLed(int id) {
return (id >= 81 && id <= 88);
}
@Override
public boolean isButton(int id) {
return ((id >= 1 && id <= 8) || (id >= 16 && id <= 19));
}
@Override
public boolean isCCI(int id) {
return false;
}
public KeypadConfigPalladiom() {
modelData.put("2W", Arrays.asList(Component.BUTTON1, Component.BUTTON4, Component.LED1, Component.LED4));
modelData.put("3W", Arrays.asList(Component.BUTTON1, Component.BUTTON2, Component.BUTTON4, Component.LED1,
Component.LED2, Component.LED4));
modelData.put("4W", Arrays.asList(Component.BUTTON1, Component.BUTTON2, Component.BUTTON3, Component.BUTTON4,
Component.LED1, Component.LED2, Component.LED3, Component.LED4));
modelData.put("RW", Arrays.asList(Component.BUTTON1, Component.BUTTON2, Component.BUTTON3, Component.LOWER1,
Component.RAISE1, Component.LED1, Component.LED2, Component.LED3));
modelData.put("22W", Arrays.asList(Component.BUTTON1, Component.BUTTON4, Component.BUTTON5, Component.BUTTON8,
Component.LED1, Component.LED4, Component.LED5, Component.LED8));
modelData.put("24W",
Arrays.asList(Component.BUTTON1, Component.BUTTON2, Component.BUTTON3, Component.BUTTON4,
Component.BUTTON5, Component.BUTTON8, Component.LED1, Component.LED2, Component.LED3,
Component.LED4, Component.LED5, Component.LED8));
modelData.put("42W",
Arrays.asList(Component.BUTTON1, Component.BUTTON4, Component.BUTTON5, Component.BUTTON6,
Component.BUTTON7, Component.BUTTON8, Component.LED1, Component.LED4, Component.LED5,
Component.LED6, Component.LED7, Component.LED8));
modelData.put("44W",
Arrays.asList(Component.BUTTON1, Component.BUTTON2, Component.BUTTON3, Component.BUTTON4,
Component.BUTTON5, Component.BUTTON6, Component.BUTTON7, Component.BUTTON8, Component.LED1,
Component.LED2, Component.LED3, Component.LED4, Component.LED5, Component.LED6, Component.LED7,
Component.LED8));
modelData.put("2RW",
Arrays.asList(Component.BUTTON1, Component.BUTTON2, Component.BUTTON3, Component.BUTTON5,
Component.BUTTON8, Component.LOWER1, Component.RAISE1, Component.LED1, Component.LED2,
Component.LED3, Component.LED5, Component.LED8));
modelData.put("4RW",
Arrays.asList(Component.BUTTON1, Component.BUTTON2, Component.BUTTON3, Component.BUTTON5,
Component.BUTTON6, Component.BUTTON7, Component.BUTTON8, Component.LOWER1, Component.RAISE1,
Component.LED1, Component.LED2, Component.LED3, Component.LED5, Component.LED6, Component.LED7,
Component.LED8));
modelData.put("RRW",
Arrays.asList(Component.BUTTON1, Component.BUTTON2, Component.BUTTON3, Component.BUTTON5,
Component.BUTTON6, Component.BUTTON7, Component.LOWER1, Component.RAISE1, Component.LOWER2,
Component.RAISE2, Component.LED1, Component.LED2, Component.LED3, Component.LED5,
Component.LED6, Component.LED7));
// Superset of all models
modelData.put("Generic", Arrays.asList(Component.BUTTON1, Component.BUTTON2, Component.BUTTON3,
Component.BUTTON4, Component.BUTTON5, Component.BUTTON6, Component.BUTTON7, Component.BUTTON8,
Component.LOWER1, Component.RAISE1, Component.LOWER2, Component.RAISE2, Component.LED1, Component.LED2,
Component.LED3, Component.LED4, Component.LED5, Component.LED6, Component.LED7, Component.LED8));
}
}

View File

@@ -0,0 +1,101 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.lutron.internal.keypadconfig;
import java.util.Arrays;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.lutron.internal.KeypadComponent;
import org.openhab.binding.lutron.internal.discovery.project.ComponentType;
/**
* Keypad configuration definition for Pico models
*
* @author Bob Adair - Initial contribution
*/
@NonNullByDefault
public final class KeypadConfigPico extends KeypadConfig {
private static enum Component implements KeypadComponent {
// Buttons for 2B, 2BRL, 3B, and 3BRL models
BUTTON1(2, "button1", "Button 1", ComponentType.BUTTON),
BUTTON2(3, "button2", "Button 2", ComponentType.BUTTON),
BUTTON3(4, "button3", "Button 3", ComponentType.BUTTON),
RAISE(5, "buttonraise", "Raise Button", ComponentType.BUTTON),
LOWER(6, "buttonlower", "Lower Button", ComponentType.BUTTON),
// Buttons for PJ2-4B model
BUTTON1_4B(8, "button01", "Button 1", ComponentType.BUTTON),
BUTTON2_4B(9, "button02", "Button 2", ComponentType.BUTTON),
BUTTON3_4B(10, "button03", "Button 3", ComponentType.BUTTON),
BUTTON4_4B(11, "button04", "Button 4", ComponentType.BUTTON);
private final int id;
private final String channel;
private final String description;
private final ComponentType type;
Component(int id, String channel, String description, ComponentType type) {
this.id = id;
this.channel = channel;
this.description = description;
this.type = type;
}
@Override
public int id() {
return id;
}
@Override
public String channel() {
return channel;
}
@Override
public String description() {
return description;
}
@Override
public ComponentType type() {
return type;
}
}
@Override
public boolean isLed(int id) {
return false;
}
@Override
public boolean isButton(int id) {
return (id >= 2 && id <= 11);
}
@Override
public boolean isCCI(int id) {
return false;
}
public KeypadConfigPico() {
modelData.put("2B", Arrays.asList(Component.BUTTON1, Component.BUTTON3));
modelData.put("2BRL", Arrays.asList(Component.BUTTON1, Component.BUTTON3, Component.RAISE, Component.LOWER));
modelData.put("3B", Arrays.asList(Component.BUTTON1, Component.BUTTON2, Component.BUTTON3));
modelData.put("3BRL", Arrays.asList(Component.BUTTON1, Component.BUTTON2, Component.BUTTON3, Component.RAISE,
Component.LOWER));
modelData.put("4B",
Arrays.asList(Component.BUTTON1_4B, Component.BUTTON2_4B, Component.BUTTON3_4B, Component.BUTTON4_4B));
}
}

View File

@@ -0,0 +1,153 @@
/**
* 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.keypadconfig;
import java.util.Arrays;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.lutron.internal.KeypadComponent;
import org.openhab.binding.lutron.internal.discovery.project.ComponentType;
/**
* Keypad configuration definition for seeTouch and Hybrid seeTouch
*
* @author Bob Adair - Initial contribution
*/
@NonNullByDefault
public final class KeypadConfigSeetouch extends KeypadConfig {
private static enum Component implements KeypadComponent {
BUTTON1(1, "button1", "Button 1", ComponentType.BUTTON),
BUTTON2(2, "button2", "Button 2", ComponentType.BUTTON),
BUTTON3(3, "button3", "Button 3", ComponentType.BUTTON),
BUTTON4(4, "button4", "Button 4", ComponentType.BUTTON),
BUTTON5(5, "button5", "Button 5", ComponentType.BUTTON),
BUTTON6(6, "button6", "Button 6", ComponentType.BUTTON),
BUTTON7(7, "button7", "Button 7", ComponentType.BUTTON),
LOWER1(16, "buttontoplower", "Top lower button", ComponentType.BUTTON),
RAISE1(17, "buttontopraise", "Top raise button", ComponentType.BUTTON),
LOWER2(18, "buttonbottomlower", "Bottom lower button", ComponentType.BUTTON),
RAISE2(19, "buttonbottomraise", "Bottom raise button", ComponentType.BUTTON),
// CCI1(25, "cci1", "CCI 1", ComponentType.CCI), // listed in spec but currently unused in binding
// CCI2(26, "cci2", "CCI 2", ComponentType.CCI), // listed in spec but currently unused in binding
LED1(81, "led1", "LED 1", ComponentType.LED),
LED2(82, "led2", "LED 2", ComponentType.LED),
LED3(83, "led3", "LED 3", ComponentType.LED),
LED4(84, "led4", "LED 4", ComponentType.LED),
LED5(85, "led5", "LED 5", ComponentType.LED),
LED6(86, "led6", "LED 6", ComponentType.LED),
LED7(87, "led7", "LED 7", ComponentType.LED);
private final int id;
private final String channel;
private final String description;
private final ComponentType type;
Component(int id, String channel, String description, ComponentType type) {
this.id = id;
this.channel = channel;
this.description = description;
this.type = type;
}
@Override
public int id() {
return id;
}
@Override
public String channel() {
return channel;
}
@Override
public String description() {
return description;
}
@Override
public ComponentType type() {
return type;
}
}
@Override
public boolean isLed(int id) {
return (id >= 81 && id <= 87);
}
@Override
public boolean isButton(int id) {
return ((id >= 1 && id <= 7) || (id >= 16 && id <= 19));
}
@Override
public boolean isCCI(int id) {
return false;
}
public KeypadConfigSeetouch() {
modelData.put("W1RLD",
Arrays.asList(Component.BUTTON1, Component.BUTTON2, Component.BUTTON3, Component.BUTTON5,
Component.BUTTON6, Component.LOWER2, Component.RAISE2, Component.LED1, Component.LED2,
Component.LED3, Component.LED5, Component.LED6));
modelData.put("W2RLD",
Arrays.asList(Component.BUTTON1, Component.BUTTON2, Component.BUTTON5, Component.BUTTON6,
Component.LOWER1, Component.RAISE1, Component.LOWER2, Component.RAISE2, Component.LED1,
Component.LED2, Component.LED5, Component.LED6));
modelData.put("W3S", Arrays.asList(Component.BUTTON1, Component.BUTTON2, Component.BUTTON3, Component.BUTTON6,
Component.LOWER2, Component.RAISE2, Component.LED1, Component.LED2, Component.LED3, Component.LED6));
modelData.put("W3BD",
Arrays.asList(Component.BUTTON1, Component.BUTTON2, Component.BUTTON3, Component.BUTTON5,
Component.BUTTON6, Component.BUTTON7, Component.LED1, Component.LED2, Component.LED3,
Component.LED5, Component.LED6, Component.LED7));
modelData.put("W3BRL", Arrays.asList(Component.BUTTON2, Component.BUTTON3, Component.BUTTON4, Component.LOWER2,
Component.RAISE2, Component.LED2, Component.LED3, Component.LED4));
modelData.put("W3BSRL", Arrays.asList(Component.BUTTON1, Component.BUTTON3, Component.BUTTON5, Component.LOWER2,
Component.RAISE2, Component.LED1, Component.LED3, Component.LED5));
modelData.put("W4S",
Arrays.asList(Component.BUTTON1, Component.BUTTON2, Component.BUTTON3, Component.BUTTON4,
Component.BUTTON6, Component.LOWER2, Component.RAISE2, Component.LED1, Component.LED2,
Component.LED3, Component.LED4, Component.LED6));
modelData.put("W5BRL",
Arrays.asList(Component.BUTTON1, Component.BUTTON2, Component.BUTTON3, Component.BUTTON4,
Component.BUTTON5, Component.LOWER2, Component.RAISE2, Component.LED1, Component.LED2,
Component.LED3, Component.LED4, Component.LED5));
modelData.put("W6BRL",
Arrays.asList(Component.BUTTON1, Component.BUTTON2, Component.BUTTON3, Component.BUTTON4,
Component.BUTTON5, Component.BUTTON6, Component.LOWER2, Component.RAISE2, Component.LED1,
Component.LED2, Component.LED3, Component.LED4, Component.LED5, Component.LED6));
modelData.put("W7B",
Arrays.asList(Component.BUTTON1, Component.BUTTON2, Component.BUTTON3, Component.BUTTON4,
Component.BUTTON5, Component.BUTTON6, Component.BUTTON7, Component.LED1, Component.LED2,
Component.LED3, Component.LED4, Component.LED5, Component.LED6, Component.LED7));
modelData.put("Generic",
Arrays.asList(Component.BUTTON1, Component.BUTTON2, Component.BUTTON3, Component.BUTTON4,
Component.BUTTON5, Component.BUTTON6, Component.BUTTON7, Component.LOWER1, Component.RAISE1,
Component.LOWER2, Component.RAISE2, Component.LED1, Component.LED2, Component.LED3,
Component.LED4, Component.LED5, Component.LED6, Component.LED7));
}
}

View File

@@ -0,0 +1,168 @@
/**
* 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.keypadconfig;
import java.util.Arrays;
import java.util.List;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.lutron.internal.KeypadComponent;
import org.openhab.binding.lutron.internal.discovery.project.ComponentType;
/**
* Keypad configuration definition for Tabletop seeTouch line
*
* @author Bob Adair - Initial contribution
*/
@NonNullByDefault
public final class KeypadConfigTabletopSeetouch extends KeypadConfig {
private static enum Component implements KeypadComponent {
BUTTON1(1, "button1", "Button 1", ComponentType.BUTTON),
BUTTON2(2, "button2", "Button 2", ComponentType.BUTTON),
BUTTON3(3, "button3", "Button 3", ComponentType.BUTTON),
BUTTON4(4, "button4", "Button 4", ComponentType.BUTTON),
BUTTON5(5, "button5", "Button 5", ComponentType.BUTTON),
BUTTON6(6, "button6", "Button 6", ComponentType.BUTTON),
BUTTON7(7, "button7", "Button 7", ComponentType.BUTTON),
BUTTON8(8, "button8", "Button 8", ComponentType.BUTTON),
BUTTON9(9, "button9", "Button 9", ComponentType.BUTTON),
BUTTON10(10, "button10", "Button 10", ComponentType.BUTTON),
BUTTON11(11, "button11", "Button 11", ComponentType.BUTTON),
BUTTON12(12, "button12", "Button 12", ComponentType.BUTTON),
BUTTON13(13, "button13", "Button 13", ComponentType.BUTTON),
BUTTON14(14, "button14", "Button 14", ComponentType.BUTTON),
BUTTON15(15, "button15", "Button 15", ComponentType.BUTTON),
BUTTON16(16, "button16", "Button 16", ComponentType.BUTTON),
BUTTON17(17, "button17", "Button 17", ComponentType.BUTTON),
LOWER1(20, "buttonlower1", "Lower button 1", ComponentType.BUTTON),
RAISE1(21, "buttonraise1", "Raise button 1", ComponentType.BUTTON),
LOWER2(22, "buttonlower2", "Lower button 2", ComponentType.BUTTON),
RAISE2(23, "buttonraise2", "Raise button 2", ComponentType.BUTTON),
LOWER3(24, "buttonlower3", "Lower button 3", ComponentType.BUTTON),
RAISE3(25, "buttonraise3", "Raise button 3", ComponentType.BUTTON),
LED1(81, "led1", "LED 1", ComponentType.LED),
LED2(82, "led2", "LED 2", ComponentType.LED),
LED3(83, "led3", "LED 3", ComponentType.LED),
LED4(84, "led4", "LED 4", ComponentType.LED),
LED5(85, "led5", "LED 5", ComponentType.LED),
LED6(86, "led6", "LED 6", ComponentType.LED),
LED7(87, "led7", "LED 7", ComponentType.LED),
LED8(88, "led8", "LED 8", ComponentType.LED),
LED9(89, "led9", "LED 9", ComponentType.LED),
LED10(90, "led10", "LED 10", ComponentType.LED),
LED11(91, "led11", "LED 11", ComponentType.LED),
LED12(92, "led12", "LED 12", ComponentType.LED),
LED13(93, "led13", "LED 13", ComponentType.LED),
LED14(94, "led14", "LED 14", ComponentType.LED),
LED15(95, "led15", "LED 15", ComponentType.LED),
LED16(96, "led16", "LED 16", ComponentType.LED),
LED17(97, "led17", "LED 17", ComponentType.LED);
private final int id;
private final String channel;
private final String description;
private final ComponentType type;
Component(int id, String channel, String description, ComponentType type) {
this.id = id;
this.channel = channel;
this.description = description;
this.type = type;
}
@Override
public int id() {
return id;
}
@Override
public String channel() {
return channel;
}
@Override
public String description() {
return description;
}
@Override
public ComponentType type() {
return type;
}
}
private static final List<KeypadComponent> BUTTON_GROUP1 = Arrays.asList(Component.BUTTON1, Component.BUTTON2,
Component.BUTTON3, Component.BUTTON4, Component.BUTTON5);
private static final List<KeypadComponent> BUTTON_GROUP2 = Arrays.asList(Component.BUTTON6, Component.BUTTON7,
Component.BUTTON8, Component.BUTTON9, Component.BUTTON10);
private static final List<KeypadComponent> BUTTON_GROUP3 = Arrays.asList(Component.BUTTON11, Component.BUTTON12,
Component.BUTTON13, Component.BUTTON14, Component.BUTTON15);
private static final List<KeypadComponent> BUTTON_GROUPBOTTOM_RL = Arrays.asList(Component.BUTTON16,
Component.BUTTON17, Component.LOWER3, Component.RAISE3);
private static final List<KeypadComponent> BUTTON_GROUPBOTTOM_CRL = Arrays.asList(Component.LOWER1,
Component.RAISE1, Component.LOWER2, Component.RAISE2, Component.LOWER3, Component.RAISE3);
private static final List<KeypadComponent> BUTTON_GROUPBOTTOM_GENERIC = Arrays.asList(Component.BUTTON16,
Component.BUTTON17, Component.LOWER1, Component.RAISE1, Component.LOWER2, Component.RAISE2,
Component.LOWER3, Component.RAISE3);
private static final List<KeypadComponent> LED_GROUP1 = Arrays.asList(Component.LED1, Component.LED2,
Component.LED3, Component.LED4, Component.LED5);
private static final List<KeypadComponent> LED_GROUP2 = Arrays.asList(Component.LED6, Component.LED7,
Component.LED8, Component.LED9, Component.LED10);
private static final List<KeypadComponent> LED_GROUP3 = Arrays.asList(Component.LED11, Component.LED12,
Component.LED13, Component.LED14, Component.LED15);
private static final List<KeypadComponent> LED_GROUPBOTTOM_RL = Arrays.asList(Component.LED16, Component.LED17);
@Override
public boolean isLed(int id) {
return (id >= 81 && id <= 97);
}
@Override
public boolean isButton(int id) {
return (id >= 1 && id <= 25);
}
@Override
public boolean isCCI(int id) {
return false;
}
public KeypadConfigTabletopSeetouch() {
modelData.put("T5RL", combinedList(BUTTON_GROUP1, BUTTON_GROUPBOTTOM_RL, LED_GROUP1, LED_GROUPBOTTOM_RL));
modelData.put("T10RL", combinedList(BUTTON_GROUP1, BUTTON_GROUP2, BUTTON_GROUPBOTTOM_RL, LED_GROUP1, LED_GROUP2,
LED_GROUPBOTTOM_RL));
modelData.put("T15RL", combinedList(BUTTON_GROUP1, BUTTON_GROUP2, BUTTON_GROUP3, BUTTON_GROUPBOTTOM_RL,
LED_GROUP1, LED_GROUP2, LED_GROUP3, LED_GROUPBOTTOM_RL));
modelData.put("T5CRL", combinedList(BUTTON_GROUP1, BUTTON_GROUPBOTTOM_CRL, LED_GROUP1));
modelData.put("T10CRL",
combinedList(BUTTON_GROUP1, BUTTON_GROUP2, BUTTON_GROUPBOTTOM_CRL, LED_GROUP1, LED_GROUP2));
modelData.put("T15CRL", combinedList(BUTTON_GROUP1, BUTTON_GROUP2, BUTTON_GROUP3, BUTTON_GROUPBOTTOM_CRL,
LED_GROUP1, LED_GROUP2, LED_GROUP3));
modelData.put("Generic", combinedList(BUTTON_GROUP1, BUTTON_GROUP2, BUTTON_GROUP3, BUTTON_GROUPBOTTOM_GENERIC,
LED_GROUP1, LED_GROUP2, LED_GROUP3, LED_GROUPBOTTOM_RL)); // Superset of all models
}
}

View File

@@ -0,0 +1,275 @@
/**
* 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.net;
import java.io.BufferedReader;
import java.io.Closeable;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintStream;
import java.nio.CharBuffer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.regex.MatchResult;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.commons.net.telnet.InvalidTelnetOptionException;
import org.apache.commons.net.telnet.SuppressGAOptionHandler;
import org.apache.commons.net.telnet.TelnetClient;
import org.apache.commons.net.telnet.TelnetInputListener;
import org.apache.commons.net.telnet.TelnetOptionHandler;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* A single telnet session.
*
* @author Allan Tong - Initial contribution
* @author Bob Adair - Fix to readInput and added debug logging
*/
@NonNullByDefault
public class TelnetSession implements Closeable {
private static final int BUFSIZE = 8192;
private final Logger logger = LoggerFactory.getLogger(TelnetSession.class);
private TelnetClient telnetClient;
private @Nullable BufferedReader reader;
private @Nullable PrintStream outstream;
private CharBuffer charBuffer;
private List<TelnetSessionListener> listeners = new ArrayList<>();
private @Nullable TelnetOptionHandler suppressGAOptionHandler;
public TelnetSession() {
logger.trace("Creating new TelnetSession");
this.telnetClient = new TelnetClient();
this.charBuffer = CharBuffer.allocate(BUFSIZE);
this.telnetClient.setReaderThread(true);
this.telnetClient.registerInputListener(new TelnetInputListener() {
@Override
public void telnetInputAvailable() {
try {
readInput();
} catch (IOException e) {
notifyInputError(e);
}
}
});
}
public void addListener(TelnetSessionListener listener) {
this.listeners.add(listener);
}
public void clearListeners() {
this.listeners.clear();
}
private void notifyInputAvailable() {
for (TelnetSessionListener listener : this.listeners) {
listener.inputAvailable();
}
}
private void notifyInputError(IOException exception) {
logger.debug("TelnetSession notifyInputError: {}", exception.getMessage());
for (TelnetSessionListener listener : this.listeners) {
listener.error(exception);
}
}
public void open(String host) throws IOException {
open(host, 23);
}
public void open(String host, int port) throws IOException {
// Synchronized block prevents listener thread from attempting to read input before we're ready.
synchronized (this.charBuffer) {
logger.trace("TelnetSession open called");
try {
telnetClient.connect(host, port);
telnetClient.setKeepAlive(true);
} catch (IOException e) {
logger.debug("TelnetSession open: error connecting: {}", e.getMessage());
throw (e);
}
if (this.suppressGAOptionHandler == null) {
// Only do this once.
this.suppressGAOptionHandler = new SuppressGAOptionHandler(true, true, true, true);
try {
this.telnetClient.addOptionHandler(this.suppressGAOptionHandler);
} catch (InvalidTelnetOptionException e) {
// Should never happen. Wrap it inside IOException so as not to declare another throwable.
logger.debug("TelnetSession open: error adding telnet option handler: {}", e.getMessage());
throw new IOException(e);
}
}
this.reader = new BufferedReader(new InputStreamReader(this.telnetClient.getInputStream()));
this.outstream = new PrintStream(this.telnetClient.getOutputStream());
}
}
@Override
public void close() throws IOException {
synchronized (charBuffer) {
logger.trace("TelnetSession close called");
try {
if (telnetClient.isConnected()) {
telnetClient.disconnect();
}
} catch (IOException e) {
logger.debug("TelnetSession close: error disconnecting: {}", e.getMessage());
throw (e);
} finally {
reader = null;
outstream = null;
}
}
}
public boolean isConnected() {
synchronized (charBuffer) {
return reader != null;
}
}
private void readInput() throws IOException {
synchronized (charBuffer) {
if (reader != null) {
try {
reader.read(charBuffer);
} catch (IOException e) {
logger.debug("TelnetSession readInput: error reading: {}", e.getMessage());
throw (e);
}
charBuffer.notifyAll();
if (charBuffer.position() > 0) {
notifyInputAvailable();
}
} else {
logger.debug("TelnetSession readInput: reader is null - session is closed");
throw new IOException("Session is closed");
}
}
}
public MatchResult waitFor(String prompt) throws InterruptedException {
return waitFor(prompt, 0);
}
public MatchResult waitFor(String prompt, long timeout) throws InterruptedException {
Pattern regex = Pattern.compile(prompt);
long startTime = timeout > 0 ? System.currentTimeMillis() : 0;
logger.trace("TelnetSession waitFor called with {} {}", prompt, timeout);
synchronized (this.charBuffer) {
this.charBuffer.flip();
String bufdata = this.charBuffer.toString();
int n = bufdata.lastIndexOf('\n');
String lastLine;
if (n != -1) {
lastLine = bufdata.substring(n + 1);
} else {
lastLine = bufdata;
}
Matcher matcher = regex.matcher(lastLine);
while (!matcher.find()) {
long elapsed = timeout > 0 ? (System.currentTimeMillis() - startTime) : 0;
if (timeout > 0 && elapsed >= timeout) {
break;
}
this.charBuffer.clear();
this.charBuffer.put(lastLine);
this.charBuffer.wait(timeout - elapsed);
this.charBuffer.flip();
bufdata = this.charBuffer.toString();
n = bufdata.lastIndexOf('\n');
if (n != -1) {
lastLine = bufdata.substring(n + 1);
} else {
lastLine = bufdata;
}
matcher = regex.matcher(lastLine);
}
this.charBuffer.clear();
return matcher.toMatchResult();
}
}
public Iterable<String> readLines() {
synchronized (this.charBuffer) {
this.charBuffer.flip();
String bufdata = this.charBuffer.toString();
int n = bufdata.lastIndexOf('\n');
String leftover;
String[] lines = null;
if (n != -1) {
leftover = bufdata.substring(n + 1);
bufdata = bufdata.substring(0, n).trim();
lines = bufdata.split("\r\n");
} else {
leftover = bufdata;
}
this.charBuffer.clear();
this.charBuffer.put(leftover);
return lines == null ? Collections.<String> emptyList() : Arrays.asList(lines);
}
}
public void writeLine(String line) throws IOException {
synchronized (charBuffer) {
logger.trace("TelnetSession writeLine called with {}", line);
PrintStream out = outstream;
if (out == null) {
logger.debug("TelnetSession writeLine: outstream is null - session is closed");
throw new IOException("Session is closed");
}
out.print(line + "\r\n");
if (out.checkError()) {
logger.debug("TelnetSession writeLine: error writing to outstream");
throw new IOException("Could not write to stream");
}
}
}
}

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.net;
import java.io.IOException;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Listener for telnet session events.
*
* @author Allan Tong - Initial contribution
*/
@NonNullByDefault
public interface TelnetSessionListener {
void inputAvailable();
void error(IOException exception);
}

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;
/**
* 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,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.protocol;
/**
* Type of command in the Lutron integration protocol.
*
* @author Allan Tong - Initial contribution
* @author Bob Adair - Added additional commands
*
*/
public enum LutronCommandType {
AREA,
DEVICE,
GROUP,
MODE,
MONITORING,
OUTPUT,
SHADEGRP,
SYSTEM,
SYSVAR,
TIMECLOCK,
}

View File

@@ -0,0 +1,140 @@
/**
* 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.math.BigDecimal;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Holds time durations used by the Lutron protocols
*
* @author Bob Adair - Initial contribution
*
*/
@NonNullByDefault
public class LutronDuration {
public static final int MAX_SECONDS = 360000 - 1;
public static final int MAX_HUNDREDTHS = 99;
private static final Pattern PATTERN_SS = Pattern.compile("^(\\d{1,2})$");
private static final Pattern PATTERN_SSDEC = Pattern.compile("^(\\d{1,2})\\.(\\d{2})$");
private static final Pattern PATTERN_MMSS = Pattern.compile("^(\\d{1,2}):(\\d{2})$");
private static final Pattern PATTERN_HHMMSS = Pattern.compile("^(\\d{1,2}):(\\d{2}):(\\d{2})$");
public final Integer seconds;
public final Integer hundredths;
/**
* Constructor accepting duration in seconds
*/
public LutronDuration(Integer seconds) {
if (seconds < 0 || seconds > MAX_SECONDS) {
throw new IllegalArgumentException("Invalid duration");
}
this.seconds = seconds;
this.hundredths = 0;
}
/**
* Constructor accepting duration in seconds and hundredths of seconds
*/
public LutronDuration(Integer seconds, Integer hundredths) {
if (seconds < 0 || seconds > MAX_SECONDS || hundredths < 0 || hundredths > MAX_HUNDREDTHS) {
throw new IllegalArgumentException("Invalid duration");
}
this.seconds = seconds;
this.hundredths = hundredths;
}
/**
* Constructor accepting duration in seconds as a BigDecimal
*/
public LutronDuration(BigDecimal seconds) {
if (seconds.compareTo(BigDecimal.ZERO) == -1 || seconds.compareTo(new BigDecimal(MAX_SECONDS)) == 1) {
new IllegalArgumentException("Invalid duration");
}
this.seconds = seconds.intValue();
BigDecimal fractional = seconds.subtract(new BigDecimal(seconds.intValue()));
this.hundredths = fractional.movePointRight(2).intValue();
}
/**
* Constructor accepting duration in seconds as a Double
*/
public LutronDuration(Double seconds) {
this(new BigDecimal(seconds).setScale(2, BigDecimal.ROUND_HALF_UP));
}
/**
* Constructor accepting duration string of the format: SS.ss, SS, MM:SS, or HH:MM:SS
*/
public LutronDuration(String duration) {
Matcher matcherSS = PATTERN_SS.matcher(duration);
if (matcherSS.find()) {
Integer seconds = Integer.valueOf(matcherSS.group(1));
this.seconds = seconds;
this.hundredths = 0;
return;
}
Matcher matcherSSDec = PATTERN_SSDEC.matcher(duration);
if (matcherSSDec.find()) {
this.seconds = Integer.valueOf(matcherSSDec.group(1));
this.hundredths = Integer.valueOf(matcherSSDec.group(2));
return;
}
Matcher matcherMMSS = PATTERN_MMSS.matcher(duration);
if (matcherMMSS.find()) {
Integer minutes = Integer.valueOf(matcherMMSS.group(1));
Integer seconds = Integer.valueOf(matcherMMSS.group(2));
this.seconds = minutes * 60 + seconds;
this.hundredths = 0;
return;
}
Matcher matcherHHMMSS = PATTERN_HHMMSS.matcher(duration);
if (matcherHHMMSS.find()) {
Integer hours = Integer.valueOf(matcherHHMMSS.group(1));
Integer minutes = Integer.valueOf(matcherHHMMSS.group(2));
Integer seconds = Integer.valueOf(matcherHHMMSS.group(3));
this.seconds = hours * 60 * 60 + minutes * 60 + seconds;
this.hundredths = 0;
return;
}
throw new IllegalArgumentException("Invalid duration");
}
public String asLipString() {
if (seconds < 100) {
if (hundredths == 0) {
return String.valueOf(seconds);
} else {
return String.format("%d.%02d", seconds, hundredths);
}
} else if (seconds < 3600) {
return String.format("%d:%02d", seconds / 60, seconds % 60);
} else {
return String.format("%d:%02d:%02d", seconds / 3600, (seconds % 3600) / 60, (seconds % 60));
}
}
public String asLeapString() {
return ""; // TBD
}
@Override
public String toString() {
return asLipString();
}
}

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;
/**
* Requested operation of a command to the Lutron integration protocol.
*
* @author Allan Tong - Initial contribution
*
*/
public enum LutronOperation {
EXECUTE("#"),
QUERY("?");
private final String operationChar;
LutronOperation(String operationChar) {
this.operationChar = operationChar;
}
@Override
public String toString() {
return this.operationChar;
}
}

View File

@@ -0,0 +1,126 @@
/**
* 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.radiora;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.TooManyListenersException;
import org.openhab.binding.lutron.internal.radiora.protocol.RadioRAFeedback;
import org.openhab.core.io.transport.serial.PortInUseException;
import org.openhab.core.io.transport.serial.SerialPort;
import org.openhab.core.io.transport.serial.SerialPortEvent;
import org.openhab.core.io.transport.serial.SerialPortEventListener;
import org.openhab.core.io.transport.serial.SerialPortIdentifier;
import org.openhab.core.io.transport.serial.SerialPortManager;
import org.openhab.core.io.transport.serial.UnsupportedCommOperationException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* RS232 connection to the RadioRA Classic system.
*
* @author Jeff Lauterbach - Initial Contribution
*
*/
public class RS232Connection implements RadioRAConnection, SerialPortEventListener {
private final Logger logger = LoggerFactory.getLogger(RS232Connection.class);
protected SerialPortManager serialPortManager;
protected SerialPort serialPort;
protected BufferedReader inputReader;
protected RadioRAFeedbackListener listener;
protected RS232MessageParser parser = new RS232MessageParser();
public RS232Connection(SerialPortManager serialPortManager) {
super();
this.serialPortManager = serialPortManager;
}
@Override
public void open(String portName, int baud) throws RadioRAConnectionException {
SerialPortIdentifier portIdentifier = serialPortManager.getIdentifier(portName);
if (portIdentifier == null) {
throw new RadioRAConnectionException(String.format("Port not found", portName));
}
try {
serialPort = portIdentifier.open("openhab", 5000);
serialPort.notifyOnDataAvailable(true);
serialPort.setSerialPortParams(baud, SerialPort.DATABITS_8, SerialPort.STOPBITS_1, SerialPort.PARITY_NONE);
serialPort.addEventListener(this);
inputReader = new BufferedReader(new InputStreamReader(serialPort.getInputStream()));
} catch (PortInUseException e) {
throw new RadioRAConnectionException(String.format("Port %s already in use", portIdentifier.getName()));
} catch (UnsupportedCommOperationException e) {
throw new RadioRAConnectionException("Error initializing - Failed to set serial port params");
} catch (TooManyListenersException e) {
throw new RadioRAConnectionException("Error initializing - Failed to add event listener");
} catch (IOException e) {
throw new RadioRAConnectionException("Error initializing - Failed to get input stream");
}
}
@Override
public void write(String command) {
logger.debug("Writing to serial port: {}", command.toString());
try {
serialPort.getOutputStream().write(command.getBytes());
} catch (IOException e) {
logger.debug("An error occurred writing to serial port", e);
}
}
@Override
public void disconnect() {
serialPort.close();
}
@Override
public void serialEvent(SerialPortEvent ev) {
switch (ev.getEventType()) {
case SerialPortEvent.DATA_AVAILABLE:
try {
if (!inputReader.ready()) {
logger.debug("Serial Data Available but input reader not ready");
return;
}
String message = inputReader.readLine();
logger.debug("Msg Received: {}", message);
RadioRAFeedback feedback = parser.parse(message);
if (feedback != null) {
logger.debug("Msg Parsed as {}", feedback.getClass().getName());
listener.handleRadioRAFeedback(feedback);
}
logger.debug("Finished handling feedback");
} catch (IOException e) {
logger.debug("IOException occurred", e);
}
break;
default:
logger.debug("Unhandled SerialPortEvent raised [{}]", ev.getEventType());
break;
}
}
@Override
public void setListener(RadioRAFeedbackListener listener) {
this.listener = listener;
}
}

View File

@@ -0,0 +1,64 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.lutron.internal.radiora;
import org.openhab.binding.lutron.internal.radiora.protocol.LEDMapFeedback;
import org.openhab.binding.lutron.internal.radiora.protocol.LocalZoneChangeFeedback;
import org.openhab.binding.lutron.internal.radiora.protocol.RadioRAFeedback;
import org.openhab.binding.lutron.internal.radiora.protocol.ZoneMapFeedback;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* This class handles decoding message types from RadioRA
*
* @author Jeff Lauterbach - Initial Contribution
*
*/
public class RS232MessageParser {
private Logger logger = LoggerFactory.getLogger(RS232MessageParser.class);
public RadioRAFeedback parse(String msg) {
String prefix = parsePrefix(msg);
switch (prefix) {
case "LMP":
return new LEDMapFeedback(msg);
case "LZC":
return new LocalZoneChangeFeedback(msg);
case "ZMP":
return new ZoneMapFeedback(msg);
case "!":
// No action to take when this message is received but handle
// it to prevent the the default log statement from occurring.
break;
default:
logger.debug("Unhandled msg received from RS232 [{}]", msg);
break;
}
return null;
}
protected String parsePrefix(String msg) {
String[] arr = msg.split(",");
if (arr.length < 1) {
logger.debug("Unexpected msg received from RS232 [{}]", msg);
return "";
}
return arr[0];
}
}

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.radiora;
/**
* Interface to the RadioRA Classic system
*
* @author Jeff Lauterbach - Initial Contribution
*
*/
public interface RadioRAConnection {
public void open(String portName, int baud) throws RadioRAConnectionException;
public void disconnect();
public void write(String command);
public void setListener(RadioRAFeedbackListener listener);
}

View File

@@ -0,0 +1,28 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.lutron.internal.radiora;
/**
* Thrown when an attempt to open a RadioRA Connection fails.
*
* @author Jeff Lauterbach - Initial Contribution
*
*/
public class RadioRAConnectionException extends Exception {
private static final long serialVersionUID = 1L;
public RadioRAConnectionException(String message) {
super(message);
}
}

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.radiora;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.lutron.internal.LutronBindingConstants;
import org.openhab.core.thing.ThingTypeUID;
/**
* The {@link RadioRAConstants} class defines common constants for RadioRA classic devices
*
* @author Jeff Lauterbach - Initial contribution
*/
@NonNullByDefault
public class RadioRAConstants {
// List of all Thing Type UIDs
public static final ThingTypeUID THING_TYPE_RS232 = new ThingTypeUID(LutronBindingConstants.BINDING_ID, "ra-rs232");
public static final ThingTypeUID THING_TYPE_DIMMER = new ThingTypeUID(LutronBindingConstants.BINDING_ID,
"ra-dimmer");
public static final ThingTypeUID THING_TYPE_SWITCH = new ThingTypeUID(LutronBindingConstants.BINDING_ID,
"ra-switch");
public static final ThingTypeUID THING_TYPE_PHANTOM = new ThingTypeUID(LutronBindingConstants.BINDING_ID,
"ra-phantomButton");
}

View File

@@ -0,0 +1,26 @@
/**
* 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.radiora;
import org.openhab.binding.lutron.internal.radiora.protocol.RadioRAFeedback;
/**
* Interface for handling feedback messages from RadioRA system
*
* @author Jeff Lauterbach - Initial Contribution
*
*/
public interface RadioRAFeedbackListener {
void handleRadioRAFeedback(RadioRAFeedback feedback);
}

View File

@@ -0,0 +1,51 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.lutron.internal.radiora.config;
import java.math.BigDecimal;
/**
* Configuration class for Dimmer type
*
* @author Jeff Lauterbach - Initial contribution
*
*/
public class DimmerConfig {
private int zoneNumber;
private BigDecimal fadeOutSec;
private BigDecimal fadeInSec;
public int getZoneNumber() {
return zoneNumber;
}
public void setZoneNumber(int zoneNumber) {
this.zoneNumber = zoneNumber;
}
public BigDecimal getFadeOutSec() {
return fadeOutSec;
}
public void setFadeOutSec(BigDecimal fadeOutSec) {
this.fadeOutSec = fadeOutSec;
}
public BigDecimal getFadeInSec() {
return fadeInSec;
}
public void setFadeInSec(BigDecimal fadeInSec) {
this.fadeInSec = fadeInSec;
}
}

View File

@@ -0,0 +1,43 @@
/**
* 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.radiora.config;
import java.math.BigDecimal;
/**
* Configuration class for PhantomButton thing type.
*
* @author Jeff Lauterbach - Initial Contribution
*
*/
public class PhantomButtonConfig {
private int buttonNumber;
private BigDecimal fadeSec;
public int getButtonNumber() {
return buttonNumber;
}
public void setButtonNumber(int buttonNumber) {
this.buttonNumber = buttonNumber;
}
public BigDecimal getFadeSec() {
return fadeSec;
}
public void setFadeSec(BigDecimal fadeSec) {
this.fadeSec = fadeSec;
}
}

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.radiora.config;
/**
* Configuration class for RS232 thing type.
*
* @author Jeff Lauterbach - Initial Contribution
*
*/
public class RS232Config {
private String portName;
private int baud = 9600;
private int zoneMapQueryInterval = 60;
public String getPortName() {
return portName;
}
public void setPortName(String portName) {
this.portName = portName;
}
public int getBaud() {
return baud;
}
public void setBaud(int baud) {
this.baud = baud;
}
public int getZoneMapQueryInterval() {
return zoneMapQueryInterval;
}
public void setZoneMapQueryInterval(int zoneMapQueryInterval) {
this.zoneMapQueryInterval = zoneMapQueryInterval;
}
}

View File

@@ -0,0 +1,32 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.lutron.internal.radiora.config;
/**
* Configuration class for Switch thing type.
*
* @author Jeff Lauterbach - Initial Contribution
*
*/
public class SwitchConfig {
private int zoneNumber;
public int getZoneNumber() {
return zoneNumber;
}
public void setZoneNumber(int zoneNumber) {
this.zoneNumber = zoneNumber;
}
}

View File

@@ -0,0 +1,138 @@
/**
* 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.radiora.handler;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import org.openhab.binding.lutron.internal.LutronBindingConstants;
import org.openhab.binding.lutron.internal.radiora.config.DimmerConfig;
import org.openhab.binding.lutron.internal.radiora.protocol.LocalZoneChangeFeedback;
import org.openhab.binding.lutron.internal.radiora.protocol.RadioRAFeedback;
import org.openhab.binding.lutron.internal.radiora.protocol.SetDimmerLevelCommand;
import org.openhab.binding.lutron.internal.radiora.protocol.SetSwitchLevelCommand;
import org.openhab.binding.lutron.internal.radiora.protocol.ZoneMapFeedback;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.PercentType;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.types.Command;
import org.openhab.core.types.State;
/**
* Handler for RadioRA dimmers
*
* @author Jeff Lauterbach - Initial Contribution
*
*/
public class DimmerHandler extends LutronHandler {
/**
* Used to internally keep track of dimmer level. This helps us better respond
* to external dimmer changes since RadioRA protocol does not send dimmer
* levels in their messages.
*/
private AtomicInteger lastKnownIntensity = new AtomicInteger(100);
private AtomicBoolean switchEnabled = new AtomicBoolean(false);
public DimmerHandler(Thing thing) {
super(thing);
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
DimmerConfig config = getConfigAs(DimmerConfig.class);
if (LutronBindingConstants.CHANNEL_LIGHTLEVEL.equals(channelUID.getId())) {
if (command instanceof PercentType) {
int intensity = ((PercentType) command).intValue();
SetDimmerLevelCommand cmd = new SetDimmerLevelCommand(config.getZoneNumber(), intensity);
getRS232Handler().sendCommand(cmd);
updateInternalState(intensity);
}
if (command instanceof OnOffType) {
OnOffType onOffCmd = (OnOffType) command;
SetSwitchLevelCommand cmd = new SetSwitchLevelCommand(config.getZoneNumber(), onOffCmd);
getRS232Handler().sendCommand(cmd);
updateInternalState(onOffCmd);
}
}
}
@Override
public void handleUpdate(ChannelUID channelUID, State newState) {
if (LutronBindingConstants.CHANNEL_LIGHTLEVEL.equals(channelUID.getId())) {
PercentType percent = (PercentType) newState.as(PercentType.class);
updateInternalState(percent.intValue());
}
}
@Override
public void handleFeedback(RadioRAFeedback feedback) {
if (feedback instanceof LocalZoneChangeFeedback) {
handleLocalZoneChangeFeedback((LocalZoneChangeFeedback) feedback);
} else if (feedback instanceof ZoneMapFeedback) {
handleZoneMapFeedback((ZoneMapFeedback) feedback);
}
}
private void handleZoneMapFeedback(ZoneMapFeedback feedback) {
char value = feedback.getZoneValue(getConfigAs(DimmerConfig.class).getZoneNumber());
if (value == '1') {
turnDimmerOnToLastKnownIntensity();
} else if (value == '0') {
turnDimmerOff();
}
}
private void handleLocalZoneChangeFeedback(LocalZoneChangeFeedback feedback) {
if (feedback.getZoneNumber() == getConfigAs(DimmerConfig.class).getZoneNumber()) {
if (LocalZoneChangeFeedback.State.ON.equals(feedback.getState())) {
turnDimmerOnToLastKnownIntensity();
} else if (LocalZoneChangeFeedback.State.OFF.equals(feedback.getState())) {
turnDimmerOff();
}
}
}
private void turnDimmerOnToLastKnownIntensity() {
if (!switchEnabled.get()) {
updateState(LutronBindingConstants.CHANNEL_LIGHTLEVEL, new PercentType(lastKnownIntensity.get()));
}
switchEnabled.set(true);
}
private void turnDimmerOff() {
updateState(LutronBindingConstants.CHANNEL_LIGHTLEVEL, PercentType.ZERO);
switchEnabled.set(false);
}
private void updateInternalState(int intensity) {
if (intensity > 0) {
lastKnownIntensity.set(intensity);
}
switchEnabled.set(intensity > 0);
}
private void updateInternalState(OnOffType type) {
switchEnabled.set(OnOffType.ON.equals(type));
}
}

View File

@@ -0,0 +1,51 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.lutron.internal.radiora.handler;
import org.openhab.binding.lutron.internal.radiora.protocol.RadioRAFeedback;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingStatusDetail;
import org.openhab.core.thing.binding.BaseThingHandler;
import org.openhab.core.thing.binding.ThingHandler;
/**
* Base class for non bridge handlers for Lutron RadioRA devices
*
* @author Jeff Lauterbach - Initial Contribution
*
*/
public abstract class LutronHandler extends BaseThingHandler {
public LutronHandler(Thing thing) {
super(thing);
}
public RS232Handler getRS232Handler() {
Bridge bridge = getBridge();
if (bridge == null) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_UNINITIALIZED, "Unable to get bridge");
return null;
}
ThingHandler th = bridge.getHandler();
if (th instanceof RS232Handler) {
return (RS232Handler) th;
} else {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Bridge not properly configured.");
return null;
}
}
public abstract void handleFeedback(RadioRAFeedback feedback);
}

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.radiora.handler;
import org.openhab.binding.lutron.internal.LutronBindingConstants;
import org.openhab.binding.lutron.internal.radiora.config.PhantomButtonConfig;
import org.openhab.binding.lutron.internal.radiora.protocol.ButtonPressCommand;
import org.openhab.binding.lutron.internal.radiora.protocol.LEDMapFeedback;
import org.openhab.binding.lutron.internal.radiora.protocol.RadioRAFeedback;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.types.Command;
/**
* Handler for RadioRA Phantom buttons
*
* @author Jeff Lauterbach - Initial Contribution
*
*/
public class PhantomButtonHandler extends LutronHandler {
public PhantomButtonHandler(Thing thing) {
super(thing);
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
if (channelUID.getId().equals(LutronBindingConstants.CHANNEL_SWITCH)) {
if (command instanceof OnOffType) {
ButtonPressCommand cmd = new ButtonPressCommand(
getConfigAs(PhantomButtonConfig.class).getButtonNumber(),
ButtonPressCommand.ButtonState.valueOf(command.toString()));
getRS232Handler().sendCommand(cmd);
}
}
}
@Override
public void handleFeedback(RadioRAFeedback feedback) {
if (feedback instanceof LEDMapFeedback) {
handleLEDMapFeedback((LEDMapFeedback) feedback);
}
}
private void handleLEDMapFeedback(LEDMapFeedback feedback) {
boolean zoneEnabled = feedback.getZoneValue(getConfigAs(PhantomButtonConfig.class).getButtonNumber()) == '1';
updateState(LutronBindingConstants.CHANNEL_SWITCH, zoneEnabled ? OnOffType.ON : OnOffType.OFF);
}
}

View File

@@ -0,0 +1,125 @@
/**
* 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.radiora.handler;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import org.openhab.binding.lutron.internal.radiora.RS232Connection;
import org.openhab.binding.lutron.internal.radiora.RadioRAConnection;
import org.openhab.binding.lutron.internal.radiora.RadioRAConnectionException;
import org.openhab.binding.lutron.internal.radiora.RadioRAFeedbackListener;
import org.openhab.binding.lutron.internal.radiora.config.RS232Config;
import org.openhab.binding.lutron.internal.radiora.protocol.RadioRACommand;
import org.openhab.binding.lutron.internal.radiora.protocol.RadioRAFeedback;
import org.openhab.binding.lutron.internal.radiora.protocol.ZoneMapInquiryCommand;
import org.openhab.core.io.transport.serial.SerialPortManager;
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;
import org.slf4j.LoggerFactory;
/**
* The {@link RS232Handler} is responsible for handling commands, which are
* sent to one of the channels.
*
* @author Jeff Lauterbach - Initial contribution
*/
public class RS232Handler extends BaseBridgeHandler implements RadioRAFeedbackListener {
private Logger logger = LoggerFactory.getLogger(RS232Handler.class);
private RadioRAConnection connection;
private ScheduledFuture<?> zoneMapScheduledTask;
public RS232Handler(Bridge bridge, SerialPortManager serialPortManager) {
super(bridge);
this.connection = new RS232Connection(serialPortManager);
this.connection.setListener(this);
}
@Override
public void dispose() {
if (zoneMapScheduledTask != null) {
zoneMapScheduledTask.cancel(true);
}
if (connection != null) {
connection.disconnect();
}
}
@Override
public void initialize() {
connectToRS232();
scheduleZoneMapQuery();
}
protected void connectToRS232() {
RS232Config config = getConfigAs(RS232Config.class);
String portName = config.getPortName();
int baud = config.getBaud();
logger.debug("Attempting to connect to RS232 on port {}", portName);
try {
connection.open(portName, baud);
} catch (RadioRAConnectionException e) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.HANDLER_INITIALIZING_ERROR, e.getMessage());
return;
}
logger.debug("Connected successfully");
updateStatus(ThingStatus.ONLINE);
}
protected void scheduleZoneMapQuery() {
RS232Config config = getConfigAs(RS232Config.class);
logger.debug("Scheduling zone map query at {} second inverval", config.getZoneMapQueryInterval());
Runnable task = () -> sendCommand(new ZoneMapInquiryCommand());
zoneMapScheduledTask = this.scheduler.scheduleWithFixedDelay(task, 3, config.getZoneMapQueryInterval(),
TimeUnit.SECONDS);
}
public void sendCommand(RadioRACommand command) {
connection.write(command.toString());
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
}
@Override
public void handleRadioRAFeedback(RadioRAFeedback feedback) {
for (Thing thing : getThing().getThings()) {
ThingHandler handler = thing.getHandler();
if (handler instanceof LutronHandler) {
((LutronHandler) handler).handleFeedback(feedback);
} else {
logger.debug("Unexpected - Thing {} is not a LutronHandler", thing.getClass());
}
}
}
}

View File

@@ -0,0 +1,82 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.lutron.internal.radiora.handler;
import org.openhab.binding.lutron.internal.LutronBindingConstants;
import org.openhab.binding.lutron.internal.radiora.config.SwitchConfig;
import org.openhab.binding.lutron.internal.radiora.protocol.LocalZoneChangeFeedback;
import org.openhab.binding.lutron.internal.radiora.protocol.RadioRAFeedback;
import org.openhab.binding.lutron.internal.radiora.protocol.SetSwitchLevelCommand;
import org.openhab.binding.lutron.internal.radiora.protocol.ZoneMapFeedback;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.types.Command;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Handler for RadioRA switches
*
* @author Jeff Lauterbach - Initial Contribution
*
*/
public class SwitchHandler extends LutronHandler {
private Logger logger = LoggerFactory.getLogger(SwitchHandler.class);
public SwitchHandler(Thing thing) {
super(thing);
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
if (LutronBindingConstants.CHANNEL_SWITCH.equals(channelUID.getId())) {
if (command instanceof OnOffType) {
SetSwitchLevelCommand cmd = new SetSwitchLevelCommand(getConfigAs(SwitchConfig.class).getZoneNumber(),
(OnOffType) command);
getRS232Handler().sendCommand(cmd);
}
}
}
@Override
public void handleFeedback(RadioRAFeedback feedback) {
if (feedback instanceof LocalZoneChangeFeedback) {
handleLocalZoneChangeFeedback((LocalZoneChangeFeedback) feedback);
} else if (feedback instanceof ZoneMapFeedback) {
handleZoneMapFeedback((ZoneMapFeedback) feedback);
}
}
private void handleZoneMapFeedback(ZoneMapFeedback feedback) {
char value = feedback.getZoneValue(getConfigAs(SwitchConfig.class).getZoneNumber());
if (value == '1') {
updateState(LutronBindingConstants.CHANNEL_SWITCH, OnOffType.ON);
} else if (value == '0') {
updateState(LutronBindingConstants.CHANNEL_SWITCH, OnOffType.OFF);
}
}
private void handleLocalZoneChangeFeedback(LocalZoneChangeFeedback feedback) {
if (feedback.getZoneNumber() == getConfigAs(SwitchConfig.class).getZoneNumber()) {
if (LocalZoneChangeFeedback.State.CHG.equals(feedback.getState())) {
logger.debug("Not Implemented Yet - CHG state received from Local Zone Change Feedback.");
}
updateState(LutronBindingConstants.CHANNEL_SWITCH, OnOffType.valueOf(feedback.getState().toString()));
}
}
}

View File

@@ -0,0 +1,63 @@
/**
* 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.radiora.protocol;
import java.util.ArrayList;
import java.util.List;
/**
* Button Press (BP) Command.
* Trigger a Phantom Button Press on the RadioRA Serial Device.
*
* @author Jeff Lauterbach - Initial Contribution
*
*/
public class ButtonPressCommand extends RadioRACommand {
public enum ButtonState {
OFF,
ON,
TOG
}
private int buttonNumber; // 1 to 15, 16 ALL ON, 17 ALL OFF
private ButtonState state; // ON/OFF/TOG
private Integer fadeSec; // 0 to 240 (optional)
public ButtonPressCommand(int buttonNumber, ButtonState state) {
this.buttonNumber = buttonNumber;
this.state = state;
}
public void setFadeSeconds(int seconds) {
this.fadeSec = seconds;
}
@Override
public String getCommand() {
return "BP";
}
@Override
public List<String> getArgs() {
List<String> args = new ArrayList<>();
args.add(String.valueOf(buttonNumber));
args.add(String.valueOf(state));
if (fadeSec != null) {
args.add(String.valueOf(fadeSec));
}
return args;
}
}

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.radiora.protocol;
/**
* Feedback (LMP) that gives the state of all phantom LEDs
* <p>
* <b>Syntax:</b>
*
* <pre>
* {@code
* LMP,<LED States>
* }
* </pre>
*
* <b>Example:</b>
* <p>
* Phantom LEDs 1 and 5 are ON, all others are OFF
*
* <pre>
* LMP,100010000000000
* </pre>
*
* @author Jeff Lauterbach - Initial Contribution
*
*/
public class LEDMapFeedback extends RadioRAFeedback {
private String bitmap; // 15 bit String of (0,1). 1 is ON, 0 is OFF
public LEDMapFeedback(String msg) {
String[] params = parse(msg, 1);
bitmap = params[1];
}
public String getBitmap() {
return bitmap;
}
public char getZoneValue(int zone) {
if (zone < 1 || zone > bitmap.length()) {
return '0';
}
return bitmap.charAt(zone - 1);
}
}

View File

@@ -0,0 +1,68 @@
/**
* 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.radiora.protocol;
/**
* Feedback for when a device was changed locally (not through Master Control)
* <p>
* <b>Syntax:</b>
*
* <pre>
* {@code
* LZC,<Zone Number>,<State>
* }
* </pre>
*
* <b>Examples:</b>
* <p>
* Dimmer 1 changed from 100% to 50%
*
* <pre>
* LZC,01,CHG
* </pre>
*
* Dimmer 4 changed from OFF to 25%
*
* <pre>
* LZC,04,ON
* </pre>
*
* @author Jeff Lauterbach - Initial Contribution
*
*/
public class LocalZoneChangeFeedback extends RadioRAFeedback {
private int zoneNumber; // 1 to 32
private State state; // ON, OFF, CHG
public enum State {
ON,
OFF,
CHG
}
public LocalZoneChangeFeedback(String msg) {
String[] params = parse(msg, 2);
zoneNumber = Integer.parseInt(params[1].trim());
state = State.valueOf(params[2].trim().toUpperCase());
}
public State getState() {
return state;
}
public int getZoneNumber() {
return zoneNumber;
}
}

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.radiora.protocol;
import java.util.List;
/**
* Abstract base class for commands.
*
* @author Jeff Lauterbach - Initial Contribution
*
*/
public abstract class RadioRACommand {
protected static final String FIELD_SEPARATOR = ",";
protected static final String CMD_TERMINATOR = "\r";
public abstract String getCommand();
public abstract List<String> getArgs();
@Override
public String toString() {
StringBuilder str = new StringBuilder();
str.append(getCommand());
for (String arg : getArgs()) {
str.append(FIELD_SEPARATOR);
str.append(arg);
}
str.append(CMD_TERMINATOR);
return str.toString();
}
}

View File

@@ -0,0 +1,31 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.lutron.internal.radiora.protocol;
/**
* Base class for Feedback from RadioRA
*
* @author Jeff Lauterbach - Initial Contribution
*
*/
public class RadioRAFeedback {
public String[] parse(String msg, int numParams) {
String[] params = msg.split(",");
if (params.length < numParams + 1) {
throw new IllegalStateException("Invalid message format: " + msg);
}
return params;
}
}

View File

@@ -0,0 +1,57 @@
/**
* 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.radiora.protocol;
import java.util.ArrayList;
import java.util.List;
/**
* Set Dimmer Level (SDL)
* Set an individual Dimmers light level.
*
* @author Jeff Lauterbach - Initial Contribution
*
*/
public class SetDimmerLevelCommand extends RadioRACommand {
private int zoneNumber; // 1 to 32
private int dimmerLevel; // 0 to 100
private Integer fadeSec; // 0 to 240 (optional)
public SetDimmerLevelCommand(int zoneNumber, int dimmerLevel) {
this.zoneNumber = zoneNumber;
this.dimmerLevel = dimmerLevel;
}
public void setFadeSeconds(int seconds) {
fadeSec = seconds;
}
@Override
public String getCommand() {
return "SDL";
}
@Override
public List<String> getArgs() {
List<String> args = new ArrayList<>();
args.add(String.valueOf(zoneNumber));
args.add(String.valueOf(dimmerLevel));
if (fadeSec != null) {
args.add(String.valueOf(fadeSec));
}
return args;
}
}

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.radiora.protocol;
import java.util.ArrayList;
import java.util.List;
import org.openhab.core.library.types.OnOffType;
/**
* Set Switch Level (SSL)
* Turn an individual Switch ON or OFF.
*
* @author Jeff Lauterbach - Initial Contribution
*
*/
public class SetSwitchLevelCommand extends RadioRACommand {
private int zoneNumber; // 1 to 32
private OnOffType state; // ON/OFF
private Integer delaySec; // 0 to 240 (optional)
public SetSwitchLevelCommand(int zoneNumber, OnOffType state) {
this.zoneNumber = zoneNumber;
this.state = state;
}
public void setDelaySeconds(int seconds) {
this.delaySec = seconds;
}
@Override
public String getCommand() {
return "SSL";
}
@Override
public List<String> getArgs() {
List<String> args = new ArrayList<>();
args.add(String.valueOf(zoneNumber));
args.add(String.valueOf(state));
if (delaySec != null) {
args.add(String.valueOf(delaySec));
}
return args;
}
}

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.radiora.protocol;
/**
* Feedback that gives the state of all zones
* <p>
* <b>Syntax:</b>
*
* <pre>
* {@code
* ZMP,<Zone States>
* }
* </pre>
*
* <b>Example:</b>
* <p>
* Zones 2 and 9 are ON, all others are OFF, and Zones 31 and 32 are unassigned.
*
* <pre>
* ZMP,010000001000000000000000000000XX
* </pre>
*
* @author Jeff Lauterbach - Initial Contribution
*
*/
public class ZoneMapFeedback extends RadioRAFeedback {
private String zoneStates; // 32 bit String of (0,1,X)
public ZoneMapFeedback(String msg) {
String[] params = parse(msg, 1);
zoneStates = params[1];
}
public String getZoneStates() {
return zoneStates;
}
public char getZoneValue(int zone) {
if (zone < 1 || zone > zoneStates.length()) {
return 'X';
}
return zoneStates.charAt(zone - 1);
}
}

Some files were not shown because too many files have changed in this diff Show More