2 * Copyright (c) 2010-2020 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
7 * This program and the accompanying materials are made available under the
8 * terms of the Eclipse Public License 2.0 which is available at
9 * http://www.eclipse.org/legal/epl-2.0
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.lutron.internal.handler;
15 import static org.openhab.binding.lutron.internal.LutronBindingConstants.BINDING_ID;
17 import java.util.ArrayList;
18 import java.util.HashMap;
19 import java.util.List;
21 import java.util.Map.Entry;
22 import java.util.concurrent.TimeUnit;
24 import org.openhab.binding.lutron.internal.KeypadComponent;
25 import org.openhab.binding.lutron.internal.keypadconfig.KeypadConfig;
26 import org.openhab.binding.lutron.internal.protocol.LutronCommandType;
27 import org.openhab.core.library.types.OnOffType;
28 import org.openhab.core.library.types.OpenClosedType;
29 import org.openhab.core.thing.Bridge;
30 import org.openhab.core.thing.Channel;
31 import org.openhab.core.thing.ChannelUID;
32 import org.openhab.core.thing.Thing;
33 import org.openhab.core.thing.ThingStatus;
34 import org.openhab.core.thing.ThingStatusDetail;
35 import org.openhab.core.thing.binding.builder.ChannelBuilder;
36 import org.openhab.core.thing.binding.builder.ThingBuilder;
37 import org.openhab.core.thing.type.ChannelTypeUID;
38 import org.openhab.core.types.Command;
39 import org.openhab.core.types.RefreshType;
40 import org.slf4j.Logger;
41 import org.slf4j.LoggerFactory;
44 * Abstract class providing common definitions and methods for derived keypad classes
46 * @author Bob Adair - Initial contribution, based partly on Allan Tong's KeypadHandler class
48 public abstract class BaseKeypadHandler extends LutronHandler {
50 protected static final Integer ACTION_PRESS = 3;
51 protected static final Integer ACTION_RELEASE = 4;
52 protected static final Integer ACTION_HOLD = 5;
53 protected static final Integer ACTION_LED_STATE = 9;
55 protected static final Integer LED_OFF = 0;
56 protected static final Integer LED_ON = 1;
57 protected static final Integer LED_FLASH = 2; // Same as 1 on RA2 keypads
58 protected static final Integer LED_RAPIDFLASH = 3; // Same as 1 on RA2 keypads
60 private final Logger logger = LoggerFactory.getLogger(BaseKeypadHandler.class);
62 protected List<KeypadComponent> buttonList = new ArrayList<>();
63 protected List<KeypadComponent> ledList = new ArrayList<>();
64 protected List<KeypadComponent> cciList = new ArrayList<>();
66 protected int integrationId;
67 protected String model;
68 protected Boolean autoRelease;
69 protected Boolean advancedChannels = false;
71 protected Map<Integer, String> componentChannelMap = new HashMap<>(50);
73 protected abstract void configureComponents(String model);
75 private final Object asyncInitLock = new Object();
77 protected KeypadConfig kp;
79 public BaseKeypadHandler(Thing thing) {
84 * Determine if keypad component with the specified id is a LED. Keypad handlers which do not use a KeypadConfig
85 * object must override this to provide their own test.
87 * @param id The component id.
88 * @return True if the component is a LED.
90 protected boolean isLed(int id) {
95 * Determine if keypad component with the specified id is a button. Keypad handlers which do not use a KeypadConfig
96 * object must override this to provide their own test.
98 * @param id The component id.
99 * @return True if the component is a button.
101 protected boolean isButton(int id) {
102 return kp.isButton(id);
106 * Determine if keypad component with the specified id is a CCI. Keypad handlers which do not use a KeypadConfig
107 * object must override this to provide their own test.
109 * @param id The component id.
110 * @return True if the component is a CCI.
112 protected boolean isCCI(int id) {
116 protected void configureChannels() {
118 ChannelTypeUID channelTypeUID;
119 ChannelUID channelUID;
121 logger.debug("Configuring channels for keypad {}", integrationId);
123 List<Channel> channelList = new ArrayList<>();
124 List<Channel> existingChannels = getThing().getChannels();
126 if (existingChannels != null && !existingChannels.isEmpty()) {
127 // Clear existing channels
128 logger.debug("Clearing existing channels for keypad {}", integrationId);
129 ThingBuilder thingBuilder = editThing();
130 thingBuilder.withChannels(channelList);
131 updateThing(thingBuilder.build());
134 ThingBuilder thingBuilder = editThing();
136 // add channels for buttons
137 for (KeypadComponent component : buttonList) {
138 channelTypeUID = new ChannelTypeUID(BINDING_ID, advancedChannels ? "buttonAdvanced" : "button");
139 channelUID = new ChannelUID(getThing().getUID(), component.channel());
140 channel = ChannelBuilder.create(channelUID, "Switch").withType(channelTypeUID)
141 .withLabel(component.description()).build();
142 channelList.add(channel);
145 // add channels for LEDs
146 for (KeypadComponent component : ledList) {
147 channelTypeUID = new ChannelTypeUID(BINDING_ID, advancedChannels ? "ledIndicatorAdvanced" : "ledIndicator");
148 channelUID = new ChannelUID(getThing().getUID(), component.channel());
149 channel = ChannelBuilder.create(channelUID, "Switch").withType(channelTypeUID)
150 .withLabel(component.description()).build();
151 channelList.add(channel);
154 // add channels for CCIs (for VCRX or eventually HomeWorks CCI)
155 for (KeypadComponent component : cciList) {
156 channelTypeUID = new ChannelTypeUID(BINDING_ID, "cciState");
157 channelUID = new ChannelUID(getThing().getUID(), component.channel());
158 channel = ChannelBuilder.create(channelUID, "Contact").withType(channelTypeUID)
159 .withLabel(component.description()).build();
160 channelList.add(channel);
163 thingBuilder.withChannels(channelList);
164 updateThing(thingBuilder.build());
165 logger.debug("Done configuring channels for keypad {}", integrationId);
168 protected ChannelUID channelFromComponent(int component) {
169 String channel = null;
171 // Get channel string from Lutron component ID using HashBiMap
172 channel = componentChannelMap.get(component);
173 if (channel == null) {
174 logger.debug("Unknown component {}", component);
176 return channel == null ? null : new ChannelUID(getThing().getUID(), channel);
179 protected Integer componentFromChannel(ChannelUID channelUID) {
180 return componentChannelMap.entrySet().stream().filter(e -> e.getValue().equals(channelUID.getId()))
181 .map(Entry::getKey).findAny().orElse(null);
185 public int getIntegrationId() {
186 return integrationId;
190 public void initialize() {
191 Number id = (Number) getThing().getConfiguration().get("integrationId");
193 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "No integrationId");
196 integrationId = id.intValue();
198 logger.debug("Initializing Keypad Handler for integration ID {}", id);
200 model = (String) getThing().getConfiguration().get("model");
202 model = model.toUpperCase();
203 if (model.contains("-")) {
204 // strip off system prefix if model is of the form "system-model"
205 String[] modelSplit = model.split("-", 2);
206 model = modelSplit[1];
210 Boolean arParam = (Boolean) getThing().getConfiguration().get("autorelease");
211 autoRelease = arParam == null ? true : arParam;
213 // schedule a thread to finish initialization asynchronously since it can take several seconds
214 scheduler.schedule(this::asyncInitialize, 0, TimeUnit.SECONDS);
217 private void asyncInitialize() {
218 synchronized (asyncInitLock) {
219 logger.debug("Async init thread staring for keypad handler {}", integrationId);
221 buttonList.clear(); // in case we are re-initializing
224 componentChannelMap.clear();
226 configureComponents(model);
228 // load the channel-id map
229 for (KeypadComponent component : buttonList) {
230 componentChannelMap.put(component.id(), component.channel());
232 for (KeypadComponent component : ledList) {
233 componentChannelMap.put(component.id(), component.channel());
235 for (KeypadComponent component : cciList) {
236 componentChannelMap.put(component.id(), component.channel());
243 logger.debug("Async init thread finishing for keypad handler {}", integrationId);
248 public void initDeviceState() {
249 synchronized (asyncInitLock) {
250 logger.debug("Initializing device state for Keypad {}", integrationId);
251 Bridge bridge = getBridge();
252 if (bridge == null) {
253 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "No bridge configured");
254 } else if (bridge.getStatus() == ThingStatus.ONLINE) {
255 if (ledList.isEmpty()) {
256 // Device with no LEDs has nothing to query. Assume it is online if bridge is online.
257 updateStatus(ThingStatus.ONLINE);
259 // Query LED states. Method handleUpdate() will set thing status to online when response arrives.
260 updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.NONE, "Awaiting initial response");
261 // To reduce query volume, query only 1st LED and LEDs with linked channels.
262 for (KeypadComponent component : ledList) {
263 if (component.id() == ledList.get(0).id() || isLinked(channelFromComponent(component.id()))) {
264 queryDevice(component.id(), ACTION_LED_STATE);
269 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
275 public void handleCommand(final ChannelUID channelUID, Command command) {
276 logger.debug("Handling command {} for channel {}", command, channelUID);
278 Channel channel = getThing().getChannel(channelUID.getId());
279 if (channel == null) {
280 logger.warn("Command received on invalid channel {} for device {}", channelUID, getThing().getUID());
284 Integer componentID = componentFromChannel(channelUID);
285 if (componentID == null) {
286 logger.warn("Command received on invalid channel {} for device {}", channelUID, getThing().getUID());
290 // For LEDs, handle RefreshType and OnOffType commands
291 if (isLed(componentID)) {
292 if (command instanceof RefreshType) {
293 queryDevice(componentID, ACTION_LED_STATE);
294 } else if (command instanceof OnOffType) {
295 if (command == OnOffType.ON) {
296 device(componentID, ACTION_LED_STATE, LED_ON);
297 } else if (command == OnOffType.OFF) {
298 device(componentID, ACTION_LED_STATE, LED_OFF);
301 logger.warn("Invalid command {} received for channel {} device {}", command, channelUID,
302 getThing().getUID());
307 // For buttons, handle OnOffType commands
308 if (isButton(componentID)) {
309 if (command instanceof OnOffType) {
310 if (command == OnOffType.ON) {
311 device(componentID, ACTION_PRESS);
313 device(componentID, ACTION_RELEASE);
315 } else if (command == OnOffType.OFF) {
316 device(componentID, ACTION_RELEASE);
319 logger.warn("Invalid command type {} received for channel {} device {}", command, channelUID,
320 getThing().getUID());
325 // Contact channels for CCIs are read-only, so ignore commands
326 if (isCCI(componentID)) {
327 logger.debug("Invalid command type {} received for channel {} device {}", command, channelUID,
328 getThing().getUID());
334 public void channelLinked(ChannelUID channelUID) {
335 logger.debug("Linking keypad {} channel {}", integrationId, channelUID.getId());
337 Integer id = componentFromChannel(channelUID);
339 logger.warn("Unrecognized channel ID {} linked", channelUID.getId());
343 // if this channel is for an LED, query the Lutron controller for the current state
345 queryDevice(id, ACTION_LED_STATE);
347 // Button and CCI state can't be queried, only monitored for updates.
348 // Init button state to OFF on channel init.
350 updateState(channelUID, OnOffType.OFF);
352 // Leave CCI channel state undefined on channel init.
356 public void handleUpdate(LutronCommandType type, String... parameters) {
357 logger.trace("Handling command {} {} from keypad {}", type, parameters, integrationId);
358 if (type == LutronCommandType.DEVICE && parameters.length >= 2) {
362 component = Integer.parseInt(parameters[0]);
363 } catch (NumberFormatException e) {
364 logger.error("Invalid component {} in keypad update event message", parameters[0]);
368 ChannelUID channelUID = channelFromComponent(component);
370 if (channelUID != null) {
371 if (ACTION_LED_STATE.toString().equals(parameters[1]) && parameters.length >= 3) {
372 if (getThing().getStatus() == ThingStatus.UNKNOWN) {
373 updateStatus(ThingStatus.ONLINE); // set thing status online if this is an initial response
375 if (LED_ON.toString().equals(parameters[2])) {
376 updateState(channelUID, OnOffType.ON);
377 } else if (LED_OFF.toString().equals(parameters[2])) {
378 updateState(channelUID, OnOffType.OFF);
380 } else if (ACTION_PRESS.toString().equals(parameters[1])) {
381 if (isButton(component)) {
382 updateState(channelUID, OnOffType.ON);
384 updateState(channelUID, OnOffType.OFF);
386 } else { // component is CCI
387 updateState(channelUID, OpenClosedType.CLOSED);
389 } else if (ACTION_RELEASE.toString().equals(parameters[1])) {
390 if (isButton(component)) {
391 updateState(channelUID, OnOffType.OFF);
392 } else { // component is CCI
393 updateState(channelUID, OpenClosedType.OPEN);
395 } else if (ACTION_HOLD.toString().equals(parameters[1])) {
396 updateState(channelUID, OnOffType.OFF); // Signal a release if we receive a hold code as we will not
397 // get a subsequent release.
400 logger.warn("Unable to determine channel for component {} in keypad update event message",