]> git.basschouten.com Git - openhab-addons.git/blob
28b15357128cf1478719ec14a17aca58cc8bb052
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2023 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
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
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.binding.lutron.internal.handler;
14
15 import static org.openhab.binding.lutron.internal.LutronBindingConstants.BINDING_ID;
16
17 import java.util.ArrayList;
18 import java.util.HashMap;
19 import java.util.List;
20 import java.util.Map;
21 import java.util.Map.Entry;
22 import java.util.concurrent.TimeUnit;
23
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;
44
45 /**
46  * Abstract class providing common definitions and methods for derived keypad classes
47  *
48  * @author Bob Adair - Initial contribution, based partly on Allan Tong's KeypadHandler class
49  */
50 public abstract class BaseKeypadHandler extends LutronHandler {
51     private final Logger logger = LoggerFactory.getLogger(BaseKeypadHandler.class);
52
53     protected List<KeypadComponent> buttonList = new ArrayList<>();
54     protected List<KeypadComponent> ledList = new ArrayList<>();
55     protected List<KeypadComponent> cciList = new ArrayList<>();
56
57     Map<Integer, Integer> leapButtonMap;
58
59     protected int integrationId;
60     protected String model;
61     protected Boolean autoRelease;
62     protected Boolean advancedChannels = false;
63
64     protected Map<Integer, String> componentChannelMap = new HashMap<>(50);
65
66     protected abstract void configureComponents(String model);
67
68     private final Object asyncInitLock = new Object();
69
70     protected KeypadConfig kp;
71     protected TargetType commandTargetType = TargetType.KEYPAD; // For LEAP bridge
72
73     public BaseKeypadHandler(Thing thing) {
74         super(thing);
75     }
76
77     /**
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.
80      *
81      * @param id The component id.
82      * @return True if the component is a LED.
83      */
84     protected boolean isLed(int id) {
85         return kp.isLed(id);
86     }
87
88     /**
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.
91      *
92      * @param id The component id.
93      * @return True if the component is a button.
94      */
95     protected boolean isButton(int id) {
96         return kp.isButton(id);
97     }
98
99     /**
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.
102      *
103      * @param id The component id.
104      * @return True if the component is a CCI.
105      */
106     protected boolean isCCI(int id) {
107         return kp.isCCI(id);
108     }
109
110     protected void configureChannels() {
111         Channel channel;
112         ChannelTypeUID channelTypeUID;
113         ChannelUID channelUID;
114
115         logger.debug("Configuring channels for keypad {}", integrationId);
116
117         List<Channel> channelList = new ArrayList<>();
118         List<Channel> existingChannels = getThing().getChannels();
119
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());
126         }
127
128         ThingBuilder thingBuilder = editThing();
129
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);
137         }
138
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);
146         }
147
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);
155         }
156
157         thingBuilder.withChannels(channelList);
158         updateThing(thingBuilder.build());
159         logger.debug("Done configuring channels for keypad {}", integrationId);
160     }
161
162     protected ChannelUID channelFromComponent(int component) {
163         String channel = null;
164
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);
169         }
170         return channel == null ? null : new ChannelUID(getThing().getUID(), channel);
171     }
172
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);
176     }
177
178     @Override
179     public int getIntegrationId() {
180         return integrationId;
181     }
182
183     @Override
184     public void initialize() {
185         Number id = (Number) getThing().getConfiguration().get("integrationId");
186         if (id == null) {
187             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "No integrationId");
188             return;
189         }
190         integrationId = id.intValue();
191
192         logger.debug("Initializing Keypad Handler for integration ID {}", id);
193
194         model = (String) getThing().getConfiguration().get("model");
195         if (model != null) {
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];
201             }
202         }
203
204         Boolean arParam = (Boolean) getThing().getConfiguration().get("autorelease");
205         autoRelease = arParam == null ? true : arParam;
206
207         // schedule a thread to finish initialization asynchronously since it can take several seconds
208         scheduler.schedule(this::asyncInitialize, 0, TimeUnit.SECONDS);
209     }
210
211     private void asyncInitialize() {
212         synchronized (asyncInitLock) {
213             logger.debug("Async init thread staring for keypad handler {}", integrationId);
214
215             buttonList.clear(); // in case we are re-initializing
216             ledList.clear();
217             cciList.clear();
218             componentChannelMap.clear();
219
220             configureComponents(model);
221
222             // load the channel-id map
223             for (KeypadComponent component : buttonList) {
224                 componentChannelMap.put(component.id(), component.channel());
225             }
226             for (KeypadComponent component : ledList) {
227                 componentChannelMap.put(component.id(), component.channel());
228             }
229             for (KeypadComponent component : cciList) {
230                 componentChannelMap.put(component.id(), component.channel());
231             }
232
233             configureChannels();
234
235             initDeviceState();
236
237             logger.debug("Async init thread finishing for keypad handler {}", integrationId);
238         }
239     }
240
241     @Override
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);
252                 } else {
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);
259                         }
260                     }
261                 }
262             } else {
263                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
264             }
265         }
266     }
267
268     @Override
269     public void handleCommand(final ChannelUID channelUID, Command command) {
270         logger.debug("Handling command {} for channel {}", command, channelUID);
271
272         Channel channel = getThing().getChannel(channelUID.getId());
273         if (channel == null) {
274             logger.warn("Command received on invalid channel {} for device {}", channelUID, getThing().getUID());
275             return;
276         }
277
278         Integer componentID = componentFromChannel(channelUID);
279         if (componentID == null) {
280             logger.warn("Command received on invalid channel {} for device {}", channelUID, getThing().getUID());
281             return;
282         }
283
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);
293                 }
294             } else {
295                 logger.warn("Invalid command {} received for channel {} device {}", command, channelUID,
296                         getThing().getUID());
297             }
298             return;
299         }
300
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);
308                     if (autoRelease) {
309                         device(commandTargetType, componentID, leapComponent, DeviceCommand.ACTION_RELEASE, null);
310                     }
311                 } else if (command == OnOffType.OFF) {
312                     device(commandTargetType, componentID, leapComponent, DeviceCommand.ACTION_RELEASE, null);
313                 }
314             } else {
315                 logger.warn("Invalid command type {} received for channel {} device {}", command, channelUID,
316                         getThing().getUID());
317             }
318             return;
319         }
320
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());
325             return;
326         }
327     }
328
329     @Override
330     public void channelLinked(ChannelUID channelUID) {
331         logger.debug("Linking keypad {} channel {}", integrationId, channelUID.getId());
332
333         Integer id = componentFromChannel(channelUID);
334         if (id == null) {
335             logger.warn("Unrecognized channel ID {} linked", channelUID.getId());
336             return;
337         }
338
339         // if this channel is for an LED, query the Lutron controller for the current state
340         if (isLed(id)) {
341             queryDevice(commandTargetType, id, DeviceCommand.ACTION_LED_STATE);
342         }
343         // Button and CCI state can't be queried, only monitored for updates.
344         // Init button state to OFF on channel init.
345         if (isButton(id)) {
346             updateState(channelUID, OnOffType.OFF);
347         }
348         // Leave CCI channel state undefined on channel init.
349     }
350
351     @Override
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) {
355             int component;
356
357             try {
358                 component = Integer.parseInt(parameters[0]);
359             } catch (NumberFormatException e) {
360                 logger.error("Invalid component {} in keypad update event message", parameters[0]);
361                 return;
362             }
363
364             ChannelUID channelUID = channelFromComponent(component);
365
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
370                     }
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);
375                     }
376                 } else if (DeviceCommand.ACTION_PRESS.toString().equals(parameters[1])) {
377                     if (isButton(component)) {
378                         updateState(channelUID, OnOffType.ON);
379                         if (autoRelease) {
380                             updateState(channelUID, OnOffType.OFF);
381                         }
382                     } else { // component is CCI
383                         updateState(channelUID, OpenClosedType.CLOSED);
384                     }
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);
390                     }
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.
394                 }
395             } else {
396                 logger.warn("Unable to determine channel for component {} in keypad update event message",
397                         parameters[0]);
398             }
399         }
400     }
401 }