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.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.Optional;
20 import java.util.Scanner;
22 import java.util.TreeSet;
24 import org.eclipse.jdt.annotation.NonNullByDefault;
25 import org.eclipse.jdt.annotation.Nullable;
26 import org.openhab.binding.openwebnet.internal.OpenWebNetBindingConstants;
27 import org.openhab.binding.openwebnet.internal.actions.OpenWebNetCENActions;
28 import org.openhab.core.library.types.OnOffType;
29 import org.openhab.core.thing.Channel;
30 import org.openhab.core.thing.ChannelUID;
31 import org.openhab.core.thing.Thing;
32 import org.openhab.core.thing.ThingStatus;
33 import org.openhab.core.thing.ThingStatusDetail;
34 import org.openhab.core.thing.ThingTypeUID;
35 import org.openhab.core.thing.binding.ThingHandlerService;
36 import org.openhab.core.thing.binding.builder.ChannelBuilder;
37 import org.openhab.core.thing.binding.builder.ThingBuilder;
38 import org.openhab.core.thing.type.ChannelKind;
39 import org.openhab.core.thing.type.ChannelTypeUID;
40 import org.openhab.core.types.Command;
41 import org.openwebnet4j.communication.OWNException;
42 import org.openwebnet4j.message.BaseOpenMessage;
43 import org.openwebnet4j.message.CEN;
44 import org.openwebnet4j.message.CEN.Pressure;
45 import org.openwebnet4j.message.CENPlusScenario;
46 import org.openwebnet4j.message.CENPlusScenario.CENPlusPressure;
47 import org.openwebnet4j.message.CENScenario;
48 import org.openwebnet4j.message.CENScenario.CENPressure;
49 import org.openwebnet4j.message.FrameException;
50 import org.openwebnet4j.message.Where;
51 import org.openwebnet4j.message.WhereCEN;
52 import org.openwebnet4j.message.Who;
53 import org.slf4j.Logger;
54 import org.slf4j.LoggerFactory;
57 * The {@link OpenWebNetScenarioHandler} is responsible for handling CEN/CEN+ Scenarios messages and Dry Contact / IR
58 * Interfaces messages.
59 * It extends the abstract {@link OpenWebNetThingHandler}.
61 * @author Massimo Valla - Initial contribution
64 public class OpenWebNetScenarioHandler extends OpenWebNetThingHandler {
66 private final Logger logger = LoggerFactory.getLogger(OpenWebNetScenarioHandler.class);
68 private interface PressEvent {
70 public String toString();
73 private enum CENPressEvent implements PressEvent {
74 CEN_EVENT_START_PRESS("START_PRESS"),
75 CEN_EVENT_SHORT_PRESS("SHORT_PRESS"),
76 CEN_EVENT_EXTENDED_PRESS("EXTENDED_PRESS"),
77 CEN_EVENT_RELEASE_EXTENDED_PRESS("RELEASE_EXTENDED_PRESS");
79 private final String press;
81 CENPressEvent(final String pr) {
85 public static @Nullable CENPressEvent fromValue(String s) {
86 Optional<CENPressEvent> event = Arrays.stream(values()).filter(val -> s.equals(val.press)).findFirst();
87 return event.orElse(null);
91 public String toString() {
96 private enum CENPlusPressEvent implements PressEvent {
97 CENPLUS_EVENT_SHORT_PRESS("SHORT_PRESS"),
98 CENPLUS_EVENT_START_EXTENDED_PRESS("START_EXTENDED_PRESS"),
99 CENPLUS_EVENT_EXTENDED_PRESS("EXTENDED_PRESS"),
100 CENPLUS_EVENT_RELEASE_EXTENDED_PRESS("RELEASE_EXTENDED_PRESS");
102 private final String press;
104 CENPlusPressEvent(final String pr) {
108 public static @Nullable CENPlusPressEvent fromValue(String s) {
109 Optional<CENPlusPressEvent> event = Arrays.stream(values()).filter(val -> s.equals(val.press)).findFirst();
110 return event.orElse(null);
114 public String toString() {
119 private boolean isDryContactIR = false;
120 private boolean isCENPlus = false;
122 private static long lastAllDevicesRefreshTS = 0; // ts when last all device refresh was sent for this handler
124 public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES = OpenWebNetBindingConstants.SCENARIO_SUPPORTED_THING_TYPES;
126 public OpenWebNetScenarioHandler(Thing thing) {
128 if (OpenWebNetBindingConstants.THING_TYPE_BUS_DRY_CONTACT_IR.equals(thing.getThingTypeUID())) {
129 isDryContactIR = true;
130 logger.debug("created DryContact/IR device for thing: {}", getThing().getUID());
131 } else if (OpenWebNetBindingConstants.THING_TYPE_BUS_CENPLUS_SCENARIO_CONTROL.equals(thing.getThingTypeUID())) {
133 logger.debug("created CEN+ device for thing: {}", getThing().getUID());
135 logger.debug("created CEN device for thing: {}", getThing().getUID());
140 public void initialize() {
142 Object buttonsConfig = getConfig().get(CONFIG_PROPERTY_SCENARIO_BUTTONS);
143 if (buttonsConfig != null) {
144 Set<Integer> buttons = csvStringToSetInt((String) buttonsConfig);
145 if (!buttons.isEmpty()) {
146 ThingBuilder thingBuilder = editThing();
148 for (Integer i : buttons) {
149 ch = thing.getChannel(CHANNEL_SCENARIO_BUTTON + i);
151 thingBuilder.withChannel(buttonToChannel(i));
152 logger.debug("added channel {} to thing: {}", i, getThing().getUID());
155 updateThing(thingBuilder.build());
157 logger.warn("invalid config parameter buttons='{}' for thing {}", buttonsConfig, thing.getUID());
163 public Collection<Class<? extends ThingHandlerService>> getServices() {
164 return Set.of(OpenWebNetCENActions.class);
168 protected String ownIdPrefix() {
169 if (isCENPlus || isDryContactIR) {
170 return Who.CEN_PLUS_SCENARIO_SCHEDULER.value().toString();
172 return Who.CEN_SCENARIO_SCHEDULER.value().toString();
177 protected void handleMessage(BaseOpenMessage msg) {
178 super.handleMessage(msg);
179 if (msg.isCommand()) {
180 if (isDryContactIR) {
181 updateDryContactIRState((CENPlusScenario) msg);
183 triggerButtonChannel((CEN) msg);
186 logger.debug("handleMessage() Ignoring unsupported DIM for thing {}. Frame={}", getThing().getUID(), msg);
190 private void updateDryContactIRState(CENPlusScenario msg) {
191 logger.debug("updateDryContactIRState() for thing: {}", thing.getUID());
194 updateState(CHANNEL_DRY_CONTACT_IR, OnOffType.ON);
195 } else if (msg.isOff()) {
196 updateState(CHANNEL_DRY_CONTACT_IR, OnOffType.OFF);
198 logger.debug("updateDryContactIRState() Ignoring unsupported WHAT for thing {}. Frame={}",
199 getThing().getUID(), msg);
201 } catch (FrameException fe) {
202 logger.warn("updateDryContactIRState() Ignoring invalid frame {}", msg);
206 private void triggerButtonChannel(CEN cenMsg) {
207 Integer buttonNumber;
209 buttonNumber = cenMsg.getButtonNumber();
210 } catch (FrameException e) {
211 logger.warn("cannot read CEN/CEN+ button. Ignoring message {}", cenMsg);
214 if (buttonNumber == null || buttonNumber < 0 || buttonNumber > 31) {
215 logger.warn("invalid CEN/CEN+ button number: {}. Ignoring message {}", buttonNumber, cenMsg);
218 Channel ch = thing.getChannel(CHANNEL_SCENARIO_BUTTON + buttonNumber);
219 if (ch == null) { // we have found a new button for this device, let's add a new channel for the button
220 ThingBuilder thingBuilder = editThing();
221 ch = buttonToChannel(buttonNumber);
222 thingBuilder.withChannel(ch);
223 updateThing(thingBuilder.build());
224 logger.info("added new channel {} to thing {}", ch.getUID(), getThing().getUID());
226 final Channel channel = ch;
227 PressEvent pressEv = null;
228 Pressure press = null;
230 press = cenMsg.getButtonPressure();
231 } catch (FrameException e) {
232 logger.warn("invalid CEN/CEN+ Press. Ignoring message {}", cenMsg);
236 logger.warn("invalid CEN/CEN+ Press. Ignoring message {}", cenMsg);
240 if (cenMsg instanceof CENScenario) {
241 switch ((CENPressure) press) {
243 pressEv = CENPressEvent.CEN_EVENT_START_PRESS;
245 case RELEASE_SHORT_PRESSURE:
246 pressEv = CENPressEvent.CEN_EVENT_SHORT_PRESS;
248 case EXTENDED_PRESSURE:
249 pressEv = CENPressEvent.CEN_EVENT_EXTENDED_PRESS;
251 case RELEASE_EXTENDED_PRESSURE:
252 pressEv = CENPressEvent.CEN_EVENT_RELEASE_EXTENDED_PRESS;
255 logger.warn("unsupported CENPress. Ignoring message {}", cenMsg);
259 switch ((CENPlusPressure) press) {
261 pressEv = CENPlusPressEvent.CENPLUS_EVENT_SHORT_PRESS;
263 case START_EXTENDED_PRESSURE:
264 pressEv = CENPlusPressEvent.CENPLUS_EVENT_START_EXTENDED_PRESS;
266 case EXTENDED_PRESSURE:
267 pressEv = CENPlusPressEvent.CENPLUS_EVENT_EXTENDED_PRESS;
269 case RELEASE_EXTENDED_PRESSURE:
270 pressEv = CENPlusPressEvent.CENPLUS_EVENT_RELEASE_EXTENDED_PRESS;
273 logger.warn("unsupported CENPlusPress. Ignoring message {}", cenMsg);
277 triggerChannel(channel.getUID(), pressEv.toString());
280 private Channel buttonToChannel(int buttonNumber) {
281 ChannelTypeUID channelTypeUID;
283 channelTypeUID = new ChannelTypeUID(BINDING_ID, CHANNEL_TYPE_CEN_PLUS_BUTTON_EVENT);
285 channelTypeUID = new ChannelTypeUID(BINDING_ID, CHANNEL_TYPE_CEN_BUTTON_EVENT);
287 return ChannelBuilder
288 .create(new ChannelUID(getThing().getUID(), CHANNEL_SCENARIO_BUTTON + buttonNumber), "String")
289 .withType(channelTypeUID).withKind(ChannelKind.TRIGGER).withLabel("Button " + buttonNumber).build();
293 * Construct a CEN/CEN+ virtual press message for this device given a pressString and button number
295 * @param pressString one START_PRESS, SHORT_PRESS etc.
296 * @param button number [0-31]
297 * @return CEN message
298 * @throws IllegalArgumentException if button number or pressString are invalid
300 public CEN pressStrToMessage(String pressString, int button) throws IllegalArgumentException {
301 Where w = deviceWhere;
303 throw new IllegalArgumentException("pressStrToMessage: deviceWhere is null");
306 CENPlusPressEvent prEvent = CENPlusPressEvent.fromValue(pressString);
307 if (prEvent != null) {
309 case CENPLUS_EVENT_SHORT_PRESS:
310 return CENPlusScenario.virtualShortPressure(w.value(), button);
311 case CENPLUS_EVENT_START_EXTENDED_PRESS:
312 return CENPlusScenario.virtualStartExtendedPressure(w.value(), button);
313 case CENPLUS_EVENT_EXTENDED_PRESS:
314 return CENPlusScenario.virtualExtendedPressure(w.value(), button);
315 case CENPLUS_EVENT_RELEASE_EXTENDED_PRESS:
316 return CENPlusScenario.virtualReleaseExtendedPressure(w.value(), button);
318 throw new IllegalArgumentException("unsupported press type: " + pressString);
321 throw new IllegalArgumentException("unsupported press type: " + pressString);
324 CENPressEvent prEvent = CENPressEvent.fromValue(pressString);
325 if (prEvent != null) {
327 case CEN_EVENT_START_PRESS:
328 return CENScenario.virtualStartPressure(w.value(), button);
329 case CEN_EVENT_SHORT_PRESS:
330 return CENScenario.virtualReleaseShortPressure(w.value(), button);
331 case CEN_EVENT_EXTENDED_PRESS:
332 return CENScenario.virtualExtendedPressure(w.value(), button);
333 case CEN_EVENT_RELEASE_EXTENDED_PRESS:
334 return CENScenario.virtualReleaseExtendedPressure(w.value(), button);
336 throw new IllegalArgumentException("unsupported press type: " + pressString);
339 throw new IllegalArgumentException("unsupported press type: " + pressString);
344 private static Set<Integer> csvStringToSetInt(String s) {
345 TreeSet<Integer> intSet = new TreeSet<>();
346 String sNorm = s.replaceAll("\\s", "");
347 Scanner sc = new Scanner(sNorm);
348 sc.useDelimiter(",");
349 while (sc.hasNextInt()) {
350 intSet.add(sc.nextInt());
357 protected void handleChannelCommand(ChannelUID channel, Command command) {
358 logger.warn("CEN/CEN+ and DryContact/IR have read-only channels. Ignoring command {} for channel {}", command,
363 protected void requestChannelState(ChannelUID channel) {
364 if (isDryContactIR) {
365 super.requestChannelState(channel);
366 Where w = deviceWhere;
369 send(CENPlusScenario.requestStatus(w.value()));
370 } catch (OWNException e) {
371 logger.debug("Exception while requesting state for channel {}: {} ", channel, e.getMessage());
372 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
376 logger.debug("requestChannelState() CEN/CEN+ channels are trigger channels and do not have state.");
381 protected long getRefreshAllLastTS() {
382 return lastAllDevicesRefreshTS;
386 protected void refreshDevice(boolean refreshAll) {
387 if (isDryContactIR) {
389 logger.debug("--- refreshDevice() : refreshing GENERAL... ({})", thing.getUID());
391 send(CENPlusScenario.requestStatus("30"));
392 lastAllDevicesRefreshTS = System.currentTimeMillis();
393 } catch (OWNException e) {
394 logger.warn("Excpetion while requesting all devices refresh: {}", e.getMessage());
397 logger.debug("--- refreshDevice() : refreshing SINGLE... ({})", thing.getUID());
398 requestChannelState(new ChannelUID(thing.getUID(), CHANNEL_DRY_CONTACT_IR));
401 logger.debug("CEN/CEN+ channels are trigger channels and do not have state. Setting it ONLINE");
402 // put CEN/CEN+ scenario things to ONLINE automatically as they do not have state
403 ThingStatus ts = getThing().getStatus();
404 if (ThingStatus.ONLINE != ts && ThingStatus.REMOVING != ts && ThingStatus.REMOVED != ts) {
405 updateStatus(ThingStatus.ONLINE);
411 protected Where buildBusWhere(String wStr) throws IllegalArgumentException {
412 return new WhereCEN(wStr);