2 * Copyright (c) 2010-2021 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.openwebnet.internal.handler;
15 import static org.openhab.binding.openwebnet.internal.OpenWebNetBindingConstants.*;
17 import java.util.Arrays;
18 import java.util.Collection;
19 import java.util.Collections;
20 import java.util.Optional;
21 import java.util.Scanner;
23 import java.util.TreeSet;
25 import org.eclipse.jdt.annotation.NonNullByDefault;
26 import org.eclipse.jdt.annotation.Nullable;
27 import org.openhab.binding.openwebnet.internal.OpenWebNetBindingConstants;
28 import org.openhab.binding.openwebnet.internal.actions.OpenWebNetCENActions;
29 import org.openhab.core.library.types.OnOffType;
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.ThingTypeUID;
34 import org.openhab.core.thing.binding.ThingHandlerService;
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.ChannelKind;
38 import org.openhab.core.thing.type.ChannelTypeUID;
39 import org.openhab.core.types.Command;
40 import org.openwebnet4j.communication.OWNException;
41 import org.openwebnet4j.message.BaseOpenMessage;
42 import org.openwebnet4j.message.CEN;
43 import org.openwebnet4j.message.CEN.Pressure;
44 import org.openwebnet4j.message.CENPlusScenario;
45 import org.openwebnet4j.message.CENPlusScenario.CENPlusPressure;
46 import org.openwebnet4j.message.CENScenario;
47 import org.openwebnet4j.message.CENScenario.CENPressure;
48 import org.openwebnet4j.message.FrameException;
49 import org.openwebnet4j.message.Where;
50 import org.openwebnet4j.message.WhereCEN;
51 import org.openwebnet4j.message.Who;
52 import org.slf4j.Logger;
53 import org.slf4j.LoggerFactory;
56 * The {@link OpenWebNetScenarioHandler} is responsible for handling CEN/CEN+ Scenarios messages and Dry Contact / IR
57 * Interfaces messages.
58 * It extends the abstract {@link OpenWebNetThingHandler}.
60 * @author Massimo Valla - Initial contribution
63 public class OpenWebNetScenarioHandler extends OpenWebNetThingHandler {
65 private final Logger logger = LoggerFactory.getLogger(OpenWebNetScenarioHandler.class);
67 private interface PressEvent {
69 public String toString();
72 private enum CENPressEvent implements PressEvent {
73 CEN_EVENT_START_PRESS("START_PRESS"),
74 CEN_EVENT_SHORT_PRESS("SHORT_PRESS"),
75 CEN_EVENT_EXTENDED_PRESS("EXTENDED_PRESS"),
76 CEN_EVENT_RELEASE_EXTENDED_PRESS("RELEASE_EXTENDED_PRESS");
78 private final String press;
80 CENPressEvent(final String pr) {
84 public static @Nullable CENPressEvent fromValue(String s) {
85 Optional<CENPressEvent> event = Arrays.stream(values()).filter(val -> s.equals(val.press)).findFirst();
86 return event.orElse(null);
90 public String toString() {
95 private enum CENPlusPressEvent implements PressEvent {
96 CENPLUS_EVENT_SHORT_PRESS("SHORT_PRESS"),
97 CENPLUS_EVENT_START_EXTENDED_PRESS("START_EXTENDED_PRESS"),
98 CENPLUS_EVENT_EXTENDED_PRESS("EXTENDED_PRESS"),
99 CENPLUS_EVENT_RELEASE_EXTENDED_PRESS("RELEASE_EXTENDED_PRESS");
101 private final String press;
103 CENPlusPressEvent(final String pr) {
107 public static @Nullable CENPlusPressEvent fromValue(String s) {
108 Optional<CENPlusPressEvent> event = Arrays.stream(values()).filter(val -> s.equals(val.press)).findFirst();
109 return event.orElse(null);
113 public String toString() {
118 private boolean isDryContactIR = false;
119 private boolean isCENPlus = false;
120 private static long lastAllDevicesRefreshTS = -1; // timestamp when the last request for all device refresh was sent
122 protected static final int ALL_DEVICES_REFRESH_INTERVAL_MSEC = 10000; // interval in msec before sending another all
123 // devices refresh request
125 public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES = OpenWebNetBindingConstants.SCENARIO_SUPPORTED_THING_TYPES;
127 public OpenWebNetScenarioHandler(Thing thing) {
129 if (OpenWebNetBindingConstants.THING_TYPE_BUS_DRY_CONTACT_IR.equals(thing.getThingTypeUID())) {
130 isDryContactIR = true;
131 logger.debug("created DryContact/IR device for thing: {}", getThing().getUID());
132 } else if (OpenWebNetBindingConstants.THING_TYPE_BUS_CENPLUS_SCENARIO_CONTROL.equals(thing.getThingTypeUID())) {
134 logger.debug("created CEN+ device for thing: {}", getThing().getUID());
136 logger.debug("created CEN device for thing: {}", getThing().getUID());
141 public void initialize() {
143 Object buttonsConfig = getConfig().get(CONFIG_PROPERTY_SCENARIO_BUTTONS);
144 if (buttonsConfig != null) {
145 Set<Integer> buttons = csvStringToSetInt((String) buttonsConfig);
146 if (!buttons.isEmpty()) {
147 ThingBuilder thingBuilder = editThing();
149 for (Integer i : buttons) {
150 ch = thing.getChannel(CHANNEL_SCENARIO_BUTTON + i);
152 thingBuilder.withChannel(buttonToChannel(i));
153 logger.debug("added channel {} to thing: {}", i, getThing().getUID());
156 updateThing(thingBuilder.build());
158 logger.warn("invalid config parameter buttons='{}' for thing {}", buttonsConfig, thing.getUID());
164 public Collection<Class<? extends ThingHandlerService>> getServices() {
165 return Collections.singleton(OpenWebNetCENActions.class);
169 protected String ownIdPrefix() {
170 if (isCENPlus || isDryContactIR) {
171 return Who.CEN_PLUS_SCENARIO_SCHEDULER.value().toString();
173 return Who.CEN_SCENARIO_SCHEDULER.value().toString();
178 protected void handleMessage(BaseOpenMessage msg) {
179 super.handleMessage(msg);
180 if (msg.isCommand()) {
181 if (isDryContactIR) {
182 updateDryContactIRState((CENPlusScenario) msg);
184 triggerButtonChannel((CEN) msg);
187 logger.debug("handleMessage() Ignoring unsupported DIM for thing {}. Frame={}", getThing().getUID(), msg);
191 private void updateDryContactIRState(CENPlusScenario msg) {
192 logger.debug("updateDryContactIRState() for thing: {}", thing.getUID());
195 updateState(CHANNEL_DRY_CONTACT_IR, OnOffType.ON);
196 } else if (msg.isOff()) {
197 updateState(CHANNEL_DRY_CONTACT_IR, OnOffType.OFF);
199 logger.debug("updateDryContactIRState() Ignoring unsupported WHAT for thing {}. Frame={}",
200 getThing().getUID(), msg);
202 } catch (FrameException fe) {
203 logger.warn("updateDryContactIRState() Ignoring invalid frame {}", msg);
207 private void triggerButtonChannel(CEN cenMsg) {
208 Integer buttonNumber;
210 buttonNumber = cenMsg.getButtonNumber();
211 } catch (FrameException e) {
212 logger.warn("cannot read CEN/CEN+ button. Ignoring message {}", cenMsg);
215 if (buttonNumber == null || buttonNumber < 0 || buttonNumber > 31) {
216 logger.warn("invalid CEN/CEN+ button number: {}. Ignoring message {}", buttonNumber, cenMsg);
219 Channel ch = thing.getChannel(CHANNEL_SCENARIO_BUTTON + buttonNumber);
220 if (ch == null) { // we have found a new button for this device, let's add a new channel for the button
221 ThingBuilder thingBuilder = editThing();
222 ch = buttonToChannel(buttonNumber);
223 thingBuilder.withChannel(ch);
224 updateThing(thingBuilder.build());
225 logger.info("added new channel {} to thing {}", ch.getUID(), getThing().getUID());
227 final Channel channel = ch;
228 PressEvent pressEv = null;
229 Pressure press = null;
231 press = cenMsg.getButtonPressure();
232 } catch (FrameException e) {
233 logger.warn("invalid CEN/CEN+ Press. Ignoring message {}", cenMsg);
237 logger.warn("invalid CEN/CEN+ Press. Ignoring message {}", cenMsg);
241 if (cenMsg instanceof CENScenario) {
242 switch ((CENPressure) press) {
244 pressEv = CENPressEvent.CEN_EVENT_START_PRESS;
246 case RELEASE_SHORT_PRESSURE:
247 pressEv = CENPressEvent.CEN_EVENT_SHORT_PRESS;
249 case EXTENDED_PRESSURE:
250 pressEv = CENPressEvent.CEN_EVENT_EXTENDED_PRESS;
252 case RELEASE_EXTENDED_PRESSURE:
253 pressEv = CENPressEvent.CEN_EVENT_RELEASE_EXTENDED_PRESS;
256 logger.warn("unsupported CENPress. Ignoring message {}", cenMsg);
260 switch ((CENPlusPressure) press) {
262 pressEv = CENPlusPressEvent.CENPLUS_EVENT_SHORT_PRESS;
264 case START_EXTENDED_PRESSURE:
265 pressEv = CENPlusPressEvent.CENPLUS_EVENT_START_EXTENDED_PRESS;
267 case EXTENDED_PRESSURE:
268 pressEv = CENPlusPressEvent.CENPLUS_EVENT_EXTENDED_PRESS;
270 case RELEASE_EXTENDED_PRESSURE:
271 pressEv = CENPlusPressEvent.CENPLUS_EVENT_RELEASE_EXTENDED_PRESS;
274 logger.warn("unsupported CENPlusPress. Ignoring message {}", cenMsg);
278 triggerChannel(channel.getUID(), pressEv.toString());
281 private Channel buttonToChannel(int buttonNumber) {
282 ChannelTypeUID channelTypeUID;
284 channelTypeUID = new ChannelTypeUID(BINDING_ID, CHANNEL_TYPE_CEN_PLUS_BUTTON_EVENT);
286 channelTypeUID = new ChannelTypeUID(BINDING_ID, CHANNEL_TYPE_CEN_BUTTON_EVENT);
288 return ChannelBuilder
289 .create(new ChannelUID(getThing().getUID(), CHANNEL_SCENARIO_BUTTON + buttonNumber), "String")
290 .withType(channelTypeUID).withKind(ChannelKind.TRIGGER).withLabel("Button " + buttonNumber).build();
294 * Construct a CEN/CEN+ virtual press message for this device given a pressString and button number
296 * @param pressString one START_PRESS, SHORT_PRESS etc.
297 * @param button number [0-31]
298 * @return CEN message
299 * @throws IllegalArgumentException if button number or pressString are invalid
301 public CEN pressStrToMessage(String pressString, int button) throws IllegalArgumentException {
302 Where w = deviceWhere;
304 throw new IllegalArgumentException("pressStrToMessage: deviceWhere is null");
307 CENPlusPressEvent prEvent = CENPlusPressEvent.fromValue(pressString);
308 if (prEvent != null) {
310 case CENPLUS_EVENT_SHORT_PRESS:
311 return CENPlusScenario.virtualShortPressure(w.value(), button);
312 case CENPLUS_EVENT_START_EXTENDED_PRESS:
313 return CENPlusScenario.virtualStartExtendedPressure(w.value(), button);
314 case CENPLUS_EVENT_EXTENDED_PRESS:
315 return CENPlusScenario.virtualExtendedPressure(w.value(), button);
316 case CENPLUS_EVENT_RELEASE_EXTENDED_PRESS:
317 return CENPlusScenario.virtualReleaseExtendedPressure(w.value(), button);
319 throw new IllegalArgumentException("unsupported press type: " + pressString);
322 throw new IllegalArgumentException("unsupported press type: " + pressString);
325 CENPressEvent prEvent = CENPressEvent.fromValue(pressString);
326 if (prEvent != null) {
328 case CEN_EVENT_START_PRESS:
329 return CENScenario.virtualStartPressure(w.value(), button);
330 case CEN_EVENT_SHORT_PRESS:
331 return CENScenario.virtualReleaseShortPressure(w.value(), button);
332 case CEN_EVENT_EXTENDED_PRESS:
333 return CENScenario.virtualExtendedPressure(w.value(), button);
334 case CEN_EVENT_RELEASE_EXTENDED_PRESS:
335 return CENScenario.virtualReleaseExtendedPressure(w.value(), button);
337 throw new IllegalArgumentException("unsupported press type: " + pressString);
340 throw new IllegalArgumentException("unsupported press type: " + pressString);
345 private static Set<Integer> csvStringToSetInt(String s) {
346 TreeSet<Integer> intSet = new TreeSet<Integer>();
347 String sNorm = s.replaceAll("\\s", "");
348 Scanner sc = new Scanner(sNorm);
349 sc.useDelimiter(",");
350 while (sc.hasNextInt()) {
351 intSet.add(sc.nextInt());
358 protected void handleChannelCommand(ChannelUID channel, Command command) {
359 logger.warn("CEN/CEN+ and DryContact/IR have read-only channels. Ignoring command {} for channel {}", command,
364 protected void refreshDevice(boolean refreshAll) {
365 if (isDryContactIR) {
367 long now = System.currentTimeMillis();
368 if (now - lastAllDevicesRefreshTS > ALL_DEVICES_REFRESH_INTERVAL_MSEC) {
370 send(CENPlusScenario.requestStatus("30"));
371 lastAllDevicesRefreshTS = now;
372 } catch (OWNException e) {
373 logger.warn("Excpetion while requesting all DryContact/IR devices refresh: {}", e.getMessage());
376 logger.debug("Refresh all devices just sent...");
382 logger.debug("CEN/CEN+ channels are trigger channels and do not have state");
387 protected void requestChannelState(ChannelUID channel) {
388 if (isDryContactIR) {
391 logger.debug("CEN/CEN+ channels are trigger channels and do not have state");
395 /* helper method to request DryContact/IR device state */
396 private void requestState() {
397 Where w = deviceWhere;
400 send(CENPlusScenario.requestStatus(w.value()));
401 } catch (OWNException e) {
402 logger.warn("requestState() Exception while requesting device state: {} for thing {}", e.getMessage(),
406 logger.warn("Could not requestState(): deviceWhere is null");
411 protected Where buildBusWhere(String wStr) throws IllegalArgumentException {
412 return new WhereCEN(wStr);