2 * Copyright (c) 2010-2024 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.DeviceCommand;
27 import org.openhab.binding.lutron.internal.protocol.lip.LutronCommandType;
28 import org.openhab.binding.lutron.internal.protocol.lip.TargetType;
29 import org.openhab.core.library.types.OnOffType;
30 import org.openhab.core.library.types.OpenClosedType;
31 import org.openhab.core.thing.Bridge;
32 import org.openhab.core.thing.Channel;
33 import org.openhab.core.thing.ChannelUID;
34 import org.openhab.core.thing.Thing;
35 import org.openhab.core.thing.ThingStatus;
36 import org.openhab.core.thing.ThingStatusDetail;
37 import org.openhab.core.thing.binding.builder.ChannelBuilder;
38 import org.openhab.core.thing.binding.builder.ThingBuilder;
39 import org.openhab.core.thing.type.ChannelTypeUID;
40 import org.openhab.core.types.Command;
41 import org.openhab.core.types.RefreshType;
42 import org.slf4j.Logger;
43 import org.slf4j.LoggerFactory;
46 * Abstract class providing common definitions and methods for derived keypad classes
48 * @author Bob Adair - Initial contribution, based partly on Allan Tong's KeypadHandler class
50 public abstract class BaseKeypadHandler extends LutronHandler {
51 private final Logger logger = LoggerFactory.getLogger(BaseKeypadHandler.class);
53 protected List<KeypadComponent> buttonList = new ArrayList<>();
54 protected List<KeypadComponent> ledList = new ArrayList<>();
55 protected List<KeypadComponent> cciList = new ArrayList<>();
57 Map<Integer, Integer> leapButtonMap;
59 protected int integrationId;
60 protected String model;
61 protected Boolean autoRelease;
62 protected Boolean advancedChannels = false;
64 protected Map<Integer, String> componentChannelMap = new HashMap<>(50);
66 protected abstract void configureComponents(String model);
68 private final Object asyncInitLock = new Object();
70 protected KeypadConfig kp;
71 protected TargetType commandTargetType = TargetType.KEYPAD; // For LEAP bridge
73 public BaseKeypadHandler(Thing thing) {
78 * Determine if keypad component with the specified id is a LED. Keypad handlers which do not use a KeypadConfig
79 * object must override this to provide their own test.
81 * @param id The component id.
82 * @return True if the component is a LED.
84 protected boolean isLed(int id) {
89 * Determine if keypad component with the specified id is a button. Keypad handlers which do not use a KeypadConfig
90 * object must override this to provide their own test.
92 * @param id The component id.
93 * @return True if the component is a button.
95 protected boolean isButton(int id) {
96 return kp.isButton(id);
100 * Determine if keypad component with the specified id is a CCI. Keypad handlers which do not use a KeypadConfig
101 * object must override this to provide their own test.
103 * @param id The component id.
104 * @return True if the component is a CCI.
106 protected boolean isCCI(int id) {
110 protected void configureChannels() {
112 ChannelTypeUID channelTypeUID;
113 ChannelUID channelUID;
115 logger.debug("Configuring channels for keypad {}", integrationId);
117 List<Channel> channelList = new ArrayList<>();
118 List<Channel> existingChannels = getThing().getChannels();
120 if (!existingChannels.isEmpty()) {
121 // Clear existing channels
122 logger.debug("Clearing existing channels for keypad {}", integrationId);
123 ThingBuilder thingBuilder = editThing();
124 thingBuilder.withChannels(channelList);
125 updateThing(thingBuilder.build());
128 ThingBuilder thingBuilder = editThing();
130 // add channels for buttons
131 for (KeypadComponent component : buttonList) {
132 channelTypeUID = new ChannelTypeUID(BINDING_ID, advancedChannels ? "buttonAdvanced" : "button");
133 channelUID = new ChannelUID(getThing().getUID(), component.channel());
134 channel = ChannelBuilder.create(channelUID, "Switch").withType(channelTypeUID)
135 .withLabel(component.description()).build();
136 channelList.add(channel);
139 // add channels for LEDs
140 for (KeypadComponent component : ledList) {
141 channelTypeUID = new ChannelTypeUID(BINDING_ID, advancedChannels ? "ledIndicatorAdvanced" : "ledIndicator");
142 channelUID = new ChannelUID(getThing().getUID(), component.channel());
143 channel = ChannelBuilder.create(channelUID, "Switch").withType(channelTypeUID)
144 .withLabel(component.description()).build();
145 channelList.add(channel);
148 // add channels for CCIs (for VCRX or eventually HomeWorks CCI)
149 for (KeypadComponent component : cciList) {
150 channelTypeUID = new ChannelTypeUID(BINDING_ID, "cciState");
151 channelUID = new ChannelUID(getThing().getUID(), component.channel());
152 channel = ChannelBuilder.create(channelUID, "Contact").withType(channelTypeUID)
153 .withLabel(component.description()).build();
154 channelList.add(channel);
157 thingBuilder.withChannels(channelList);
158 updateThing(thingBuilder.build());
159 logger.debug("Done configuring channels for keypad {}", integrationId);
162 protected ChannelUID channelFromComponent(int component) {
163 String channel = null;
165 // Get channel string from Lutron component ID using HashBiMap
166 channel = componentChannelMap.get(component);
167 if (channel == null) {
168 logger.debug("Unknown component {}", component);
170 return channel == null ? null : new ChannelUID(getThing().getUID(), channel);
173 protected Integer componentFromChannel(ChannelUID channelUID) {
174 return componentChannelMap.entrySet().stream().filter(e -> e.getValue().equals(channelUID.getId()))
175 .map(Entry::getKey).findAny().orElse(null);
179 public int getIntegrationId() {
180 return integrationId;
184 public void initialize() {
185 Number id = (Number) getThing().getConfiguration().get("integrationId");
187 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "No integrationId");
190 integrationId = id.intValue();
192 logger.debug("Initializing Keypad Handler for integration ID {}", id);
194 model = (String) getThing().getConfiguration().get("model");
196 model = model.toUpperCase();
197 if (model.contains("-")) {
198 // strip off system prefix if model is of the form "system-model"
199 String[] modelSplit = model.split("-", 2);
200 model = modelSplit[1];
204 Boolean arParam = (Boolean) getThing().getConfiguration().get("autorelease");
205 autoRelease = arParam == null ? true : arParam;
207 // schedule a thread to finish initialization asynchronously since it can take several seconds
208 scheduler.schedule(this::asyncInitialize, 0, TimeUnit.SECONDS);
211 private void asyncInitialize() {
212 synchronized (asyncInitLock) {
213 logger.debug("Async init thread staring for keypad handler {}", integrationId);
215 buttonList.clear(); // in case we are re-initializing
218 componentChannelMap.clear();
220 configureComponents(model);
222 // load the channel-id map
223 for (KeypadComponent component : buttonList) {
224 componentChannelMap.put(component.id(), component.channel());
226 for (KeypadComponent component : ledList) {
227 componentChannelMap.put(component.id(), component.channel());
229 for (KeypadComponent component : cciList) {
230 componentChannelMap.put(component.id(), component.channel());
237 logger.debug("Async init thread finishing for keypad handler {}", integrationId);
242 public void initDeviceState() {
243 synchronized (asyncInitLock) {
244 logger.debug("Initializing device state for Keypad {}", integrationId);
245 Bridge bridge = getBridge();
246 if (bridge == null) {
247 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "No bridge configured");
248 } else if (bridge.getStatus() == ThingStatus.ONLINE) {
249 if (ledList.isEmpty()) {
250 // Device with no LEDs has nothing to query. Assume it is online if bridge is online.
251 updateStatus(ThingStatus.ONLINE);
253 // Query LED states. Method handleUpdate() will set thing status to online when response arrives.
254 updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.NONE, "Awaiting initial response");
255 // To reduce query volume, query only 1st LED and LEDs with linked channels.
256 for (KeypadComponent component : ledList) {
257 if (component.id() == ledList.get(0).id() || isLinked(channelFromComponent(component.id()))) {
258 queryDevice(commandTargetType, component.id(), DeviceCommand.ACTION_LED_STATE);
263 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
269 public void handleCommand(final ChannelUID channelUID, Command command) {
270 logger.debug("Handling command {} for channel {}", command, channelUID);
272 Channel channel = getThing().getChannel(channelUID.getId());
273 if (channel == null) {
274 logger.warn("Command received on invalid channel {} for device {}", channelUID, getThing().getUID());
278 Integer componentID = componentFromChannel(channelUID);
279 if (componentID == null) {
280 logger.warn("Command received on invalid channel {} for device {}", channelUID, getThing().getUID());
284 // For LEDs, handle RefreshType and OnOffType commands
285 if (isLed(componentID)) {
286 if (command instanceof RefreshType) {
287 queryDevice(commandTargetType, componentID, DeviceCommand.ACTION_LED_STATE);
288 } else if (command instanceof OnOffType) {
289 if (command == OnOffType.ON) {
290 device(commandTargetType, componentID, null, DeviceCommand.ACTION_LED_STATE, DeviceCommand.LED_ON);
291 } else if (command == OnOffType.OFF) {
292 device(commandTargetType, componentID, null, DeviceCommand.ACTION_LED_STATE, DeviceCommand.LED_OFF);
295 logger.warn("Invalid command {} received for channel {} device {}", command, channelUID,
296 getThing().getUID());
301 // For buttons, handle OnOffType commands
302 if (isButton(componentID)) {
303 if (command instanceof OnOffType) {
304 // Annotate commands with LEAP button number for LEAP bridge
305 Integer leapComponent = (this.leapButtonMap == null) ? null : leapButtonMap.get(componentID);
306 if (command == OnOffType.ON) {
307 device(commandTargetType, componentID, leapComponent, DeviceCommand.ACTION_PRESS, null);
309 device(commandTargetType, componentID, leapComponent, DeviceCommand.ACTION_RELEASE, null);
311 } else if (command == OnOffType.OFF) {
312 device(commandTargetType, componentID, leapComponent, DeviceCommand.ACTION_RELEASE, null);
315 logger.warn("Invalid command type {} received for channel {} device {}", command, channelUID,
316 getThing().getUID());
321 // Contact channels for CCIs are read-only, so ignore commands
322 if (isCCI(componentID)) {
323 logger.debug("Invalid command type {} received for channel {} device {}", command, channelUID,
324 getThing().getUID());
330 public void channelLinked(ChannelUID channelUID) {
331 logger.debug("Linking keypad {} channel {}", integrationId, channelUID.getId());
333 Integer id = componentFromChannel(channelUID);
335 logger.warn("Unrecognized channel ID {} linked", channelUID.getId());
339 // if this channel is for an LED, query the Lutron controller for the current state
341 queryDevice(commandTargetType, id, DeviceCommand.ACTION_LED_STATE);
343 // Button and CCI state can't be queried, only monitored for updates.
344 // Init button state to OFF on channel init.
346 updateState(channelUID, OnOffType.OFF);
348 // Leave CCI channel state undefined on channel init.
352 public void handleUpdate(LutronCommandType type, String... parameters) {
353 logger.trace("Handling command {} {} from keypad {}", type, parameters, integrationId);
354 if (type == LutronCommandType.DEVICE && parameters.length >= 2) {
358 component = Integer.parseInt(parameters[0]);
359 } catch (NumberFormatException e) {
360 logger.error("Invalid component {} in keypad update event message", parameters[0]);
364 ChannelUID channelUID = channelFromComponent(component);
366 if (channelUID != null) {
367 if (DeviceCommand.ACTION_LED_STATE.toString().equals(parameters[1]) && parameters.length >= 3) {
368 if (getThing().getStatus() == ThingStatus.UNKNOWN) {
369 updateStatus(ThingStatus.ONLINE); // set thing status online if this is an initial response
371 if (DeviceCommand.LED_ON.toString().equals(parameters[2])) {
372 updateState(channelUID, OnOffType.ON);
373 } else if (DeviceCommand.LED_OFF.toString().equals(parameters[2])) {
374 updateState(channelUID, OnOffType.OFF);
376 } else if (DeviceCommand.ACTION_PRESS.toString().equals(parameters[1])) {
377 if (isButton(component)) {
378 updateState(channelUID, OnOffType.ON);
380 updateState(channelUID, OnOffType.OFF);
382 } else { // component is CCI
383 updateState(channelUID, OpenClosedType.CLOSED);
385 } else if (DeviceCommand.ACTION_RELEASE.toString().equals(parameters[1])) {
386 if (isButton(component)) {
387 updateState(channelUID, OnOffType.OFF);
388 } else { // component is CCI
389 updateState(channelUID, OpenClosedType.OPEN);
391 } else if (DeviceCommand.ACTION_HOLD.toString().equals(parameters[1])) {
392 updateState(channelUID, OnOffType.OFF); // Signal a release if we receive a hold code as we will not
393 // get a subsequent release.
396 logger.warn("Unable to determine channel for component {} in keypad update event message",