]> git.basschouten.com Git - openhab-addons.git/blob
f85ab646ead075f483cdfa7d5c3e293c4878ac23
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2020 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.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;
42
43 /**
44  * Abstract class providing common definitions and methods for derived keypad classes
45  *
46  * @author Bob Adair - Initial contribution, based partly on Allan Tong's KeypadHandler class
47  */
48 public abstract class BaseKeypadHandler extends LutronHandler {
49
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;
54
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
59
60     private final Logger logger = LoggerFactory.getLogger(BaseKeypadHandler.class);
61
62     protected List<KeypadComponent> buttonList = new ArrayList<>();
63     protected List<KeypadComponent> ledList = new ArrayList<>();
64     protected List<KeypadComponent> cciList = new ArrayList<>();
65
66     protected int integrationId;
67     protected String model;
68     protected Boolean autoRelease;
69     protected Boolean advancedChannels = false;
70
71     protected Map<Integer, String> componentChannelMap = new HashMap<>(50);
72
73     protected abstract void configureComponents(String model);
74
75     private final Object asyncInitLock = new Object();
76
77     protected KeypadConfig kp;
78
79     public BaseKeypadHandler(Thing thing) {
80         super(thing);
81     }
82
83     /**
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.
86      *
87      * @param id The component id.
88      * @return True if the component is a LED.
89      */
90     protected boolean isLed(int id) {
91         return kp.isLed(id);
92     }
93
94     /**
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.
97      *
98      * @param id The component id.
99      * @return True if the component is a button.
100      */
101     protected boolean isButton(int id) {
102         return kp.isButton(id);
103     }
104
105     /**
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.
108      *
109      * @param id The component id.
110      * @return True if the component is a CCI.
111      */
112     protected boolean isCCI(int id) {
113         return kp.isCCI(id);
114     }
115
116     protected void configureChannels() {
117         Channel channel;
118         ChannelTypeUID channelTypeUID;
119         ChannelUID channelUID;
120
121         logger.debug("Configuring channels for keypad {}", integrationId);
122
123         List<Channel> channelList = new ArrayList<>();
124         List<Channel> existingChannels = getThing().getChannels();
125
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());
132         }
133
134         ThingBuilder thingBuilder = editThing();
135
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);
143         }
144
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);
152         }
153
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);
161         }
162
163         thingBuilder.withChannels(channelList);
164         updateThing(thingBuilder.build());
165         logger.debug("Done configuring channels for keypad {}", integrationId);
166     }
167
168     protected ChannelUID channelFromComponent(int component) {
169         String channel = null;
170
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);
175         }
176         return channel == null ? null : new ChannelUID(getThing().getUID(), channel);
177     }
178
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);
182     }
183
184     @Override
185     public int getIntegrationId() {
186         return integrationId;
187     }
188
189     @Override
190     public void initialize() {
191         Number id = (Number) getThing().getConfiguration().get("integrationId");
192         if (id == null) {
193             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "No integrationId");
194             return;
195         }
196         integrationId = id.intValue();
197
198         logger.debug("Initializing Keypad Handler for integration ID {}", id);
199
200         model = (String) getThing().getConfiguration().get("model");
201         if (model != null) {
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];
207             }
208         }
209
210         Boolean arParam = (Boolean) getThing().getConfiguration().get("autorelease");
211         autoRelease = arParam == null ? true : arParam;
212
213         // schedule a thread to finish initialization asynchronously since it can take several seconds
214         scheduler.schedule(this::asyncInitialize, 0, TimeUnit.SECONDS);
215     }
216
217     private void asyncInitialize() {
218         synchronized (asyncInitLock) {
219             logger.debug("Async init thread staring for keypad handler {}", integrationId);
220
221             buttonList.clear(); // in case we are re-initializing
222             ledList.clear();
223             cciList.clear();
224             componentChannelMap.clear();
225
226             configureComponents(model);
227
228             // load the channel-id map
229             for (KeypadComponent component : buttonList) {
230                 componentChannelMap.put(component.id(), component.channel());
231             }
232             for (KeypadComponent component : ledList) {
233                 componentChannelMap.put(component.id(), component.channel());
234             }
235             for (KeypadComponent component : cciList) {
236                 componentChannelMap.put(component.id(), component.channel());
237             }
238
239             configureChannels();
240
241             initDeviceState();
242
243             logger.debug("Async init thread finishing for keypad handler {}", integrationId);
244         }
245     }
246
247     @Override
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);
258                 } else {
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);
265                         }
266                     }
267                 }
268             } else {
269                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
270             }
271         }
272     }
273
274     @Override
275     public void handleCommand(final ChannelUID channelUID, Command command) {
276         logger.debug("Handling command {} for channel {}", command, channelUID);
277
278         Channel channel = getThing().getChannel(channelUID.getId());
279         if (channel == null) {
280             logger.warn("Command received on invalid channel {} for device {}", channelUID, getThing().getUID());
281             return;
282         }
283
284         Integer componentID = componentFromChannel(channelUID);
285         if (componentID == null) {
286             logger.warn("Command received on invalid channel {} for device {}", channelUID, getThing().getUID());
287             return;
288         }
289
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);
299                 }
300             } else {
301                 logger.warn("Invalid command {} received for channel {} device {}", command, channelUID,
302                         getThing().getUID());
303             }
304             return;
305         }
306
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);
312                     if (autoRelease) {
313                         device(componentID, ACTION_RELEASE);
314                     }
315                 } else if (command == OnOffType.OFF) {
316                     device(componentID, ACTION_RELEASE);
317                 }
318             } else {
319                 logger.warn("Invalid command type {} received for channel {} device {}", command, channelUID,
320                         getThing().getUID());
321             }
322             return;
323         }
324
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());
329             return;
330         }
331     }
332
333     @Override
334     public void channelLinked(ChannelUID channelUID) {
335         logger.debug("Linking keypad {} channel {}", integrationId, channelUID.getId());
336
337         Integer id = componentFromChannel(channelUID);
338         if (id == null) {
339             logger.warn("Unrecognized channel ID {} linked", channelUID.getId());
340             return;
341         }
342
343         // if this channel is for an LED, query the Lutron controller for the current state
344         if (isLed(id)) {
345             queryDevice(id, ACTION_LED_STATE);
346         }
347         // Button and CCI state can't be queried, only monitored for updates.
348         // Init button state to OFF on channel init.
349         if (isButton(id)) {
350             updateState(channelUID, OnOffType.OFF);
351         }
352         // Leave CCI channel state undefined on channel init.
353     }
354
355     @Override
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) {
359             int component;
360
361             try {
362                 component = Integer.parseInt(parameters[0]);
363             } catch (NumberFormatException e) {
364                 logger.error("Invalid component {} in keypad update event message", parameters[0]);
365                 return;
366             }
367
368             ChannelUID channelUID = channelFromComponent(component);
369
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
374                     }
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);
379                     }
380                 } else if (ACTION_PRESS.toString().equals(parameters[1])) {
381                     if (isButton(component)) {
382                         updateState(channelUID, OnOffType.ON);
383                         if (autoRelease) {
384                             updateState(channelUID, OnOffType.OFF);
385                         }
386                     } else { // component is CCI
387                         updateState(channelUID, OpenClosedType.CLOSED);
388                     }
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);
394                     }
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.
398                 }
399             } else {
400                 logger.warn("Unable to determine channel for component {} in keypad update event message",
401                         parameters[0]);
402             }
403         }
404     }
405 }