]> git.basschouten.com Git - openhab-addons.git/blob
8f89747285f8dbaf2a40f30888313652246d1179
[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.openwebnet.internal.handler;
14
15 import static org.openhab.binding.openwebnet.internal.OpenWebNetBindingConstants.*;
16
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;
22 import java.util.Set;
23 import java.util.TreeSet;
24
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.ThingStatus;
34 import org.openhab.core.thing.ThingStatusDetail;
35 import org.openhab.core.thing.ThingTypeUID;
36 import org.openhab.core.thing.binding.ThingHandlerService;
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.ChannelKind;
40 import org.openhab.core.thing.type.ChannelTypeUID;
41 import org.openhab.core.types.Command;
42 import org.openwebnet4j.communication.OWNException;
43 import org.openwebnet4j.message.BaseOpenMessage;
44 import org.openwebnet4j.message.CEN;
45 import org.openwebnet4j.message.CEN.Pressure;
46 import org.openwebnet4j.message.CENPlusScenario;
47 import org.openwebnet4j.message.CENPlusScenario.CENPlusPressure;
48 import org.openwebnet4j.message.CENScenario;
49 import org.openwebnet4j.message.CENScenario.CENPressure;
50 import org.openwebnet4j.message.FrameException;
51 import org.openwebnet4j.message.Where;
52 import org.openwebnet4j.message.WhereCEN;
53 import org.openwebnet4j.message.Who;
54 import org.slf4j.Logger;
55 import org.slf4j.LoggerFactory;
56
57 /**
58  * The {@link OpenWebNetScenarioHandler} is responsible for handling CEN/CEN+ Scenarios messages and Dry Contact / IR
59  * Interfaces messages.
60  * It extends the abstract {@link OpenWebNetThingHandler}.
61  *
62  * @author Massimo Valla - Initial contribution
63  */
64 @NonNullByDefault
65 public class OpenWebNetScenarioHandler extends OpenWebNetThingHandler {
66
67     private final Logger logger = LoggerFactory.getLogger(OpenWebNetScenarioHandler.class);
68
69     private interface PressEvent {
70         @Override
71         public String toString();
72     }
73
74     private enum CENPressEvent implements PressEvent {
75         CEN_EVENT_START_PRESS("START_PRESS"),
76         CEN_EVENT_SHORT_PRESS("SHORT_PRESS"),
77         CEN_EVENT_EXTENDED_PRESS("EXTENDED_PRESS"),
78         CEN_EVENT_RELEASE_EXTENDED_PRESS("RELEASE_EXTENDED_PRESS");
79
80         private final String press;
81
82         CENPressEvent(final String pr) {
83             this.press = pr;
84         }
85
86         public static @Nullable CENPressEvent fromValue(String s) {
87             Optional<CENPressEvent> event = Arrays.stream(values()).filter(val -> s.equals(val.press)).findFirst();
88             return event.orElse(null);
89         }
90
91         @Override
92         public String toString() {
93             return press;
94         }
95     }
96
97     private enum CENPlusPressEvent implements PressEvent {
98         CENPLUS_EVENT_SHORT_PRESS("SHORT_PRESS"),
99         CENPLUS_EVENT_START_EXTENDED_PRESS("START_EXTENDED_PRESS"),
100         CENPLUS_EVENT_EXTENDED_PRESS("EXTENDED_PRESS"),
101         CENPLUS_EVENT_RELEASE_EXTENDED_PRESS("RELEASE_EXTENDED_PRESS");
102
103         private final String press;
104
105         CENPlusPressEvent(final String pr) {
106             this.press = pr;
107         }
108
109         public static @Nullable CENPlusPressEvent fromValue(String s) {
110             Optional<CENPlusPressEvent> event = Arrays.stream(values()).filter(val -> s.equals(val.press)).findFirst();
111             return event.orElse(null);
112         }
113
114         @Override
115         public String toString() {
116             return press;
117         }
118     }
119
120     private boolean isDryContactIR = false;
121     private boolean isCENPlus = false;
122
123     private static long lastAllDevicesRefreshTS = 0; // ts when last all device refresh was sent for this handler
124
125     public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES = OpenWebNetBindingConstants.SCENARIO_SUPPORTED_THING_TYPES;
126
127     public OpenWebNetScenarioHandler(Thing thing) {
128         super(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())) {
133             isCENPlus = true;
134             logger.debug("created CEN+ device for thing: {}", getThing().getUID());
135         } else {
136             logger.debug("created CEN device for thing: {}", getThing().getUID());
137         }
138     }
139
140     @Override
141     public void initialize() {
142         super.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();
148                 Channel ch;
149                 for (Integer i : buttons) {
150                     ch = thing.getChannel(CHANNEL_SCENARIO_BUTTON + i);
151                     if (ch == null) {
152                         thingBuilder.withChannel(buttonToChannel(i));
153                         logger.debug("added channel {} to thing: {}", i, getThing().getUID());
154                     }
155                 }
156                 updateThing(thingBuilder.build());
157             } else {
158                 logger.warn("invalid config parameter buttons='{}' for thing {}", buttonsConfig, thing.getUID());
159             }
160         }
161     }
162
163     @Override
164     public Collection<Class<? extends ThingHandlerService>> getServices() {
165         return Collections.singleton(OpenWebNetCENActions.class);
166     }
167
168     @Override
169     protected String ownIdPrefix() {
170         if (isCENPlus || isDryContactIR) {
171             return Who.CEN_PLUS_SCENARIO_SCHEDULER.value().toString();
172         } else {
173             return Who.CEN_SCENARIO_SCHEDULER.value().toString();
174         }
175     }
176
177     @Override
178     protected void handleMessage(BaseOpenMessage msg) {
179         super.handleMessage(msg);
180         if (msg.isCommand()) {
181             if (isDryContactIR) {
182                 updateDryContactIRState((CENPlusScenario) msg);
183             } else {
184                 triggerButtonChannel((CEN) msg);
185             }
186         } else {
187             logger.debug("handleMessage() Ignoring unsupported DIM for thing {}. Frame={}", getThing().getUID(), msg);
188         }
189     }
190
191     private void updateDryContactIRState(CENPlusScenario msg) {
192         logger.debug("updateDryContactIRState() for thing: {}", thing.getUID());
193         try {
194             if (msg.isOn()) {
195                 updateState(CHANNEL_DRY_CONTACT_IR, OnOffType.ON);
196             } else if (msg.isOff()) {
197                 updateState(CHANNEL_DRY_CONTACT_IR, OnOffType.OFF);
198             } else {
199                 logger.debug("updateDryContactIRState() Ignoring unsupported WHAT for thing {}. Frame={}",
200                         getThing().getUID(), msg);
201             }
202         } catch (FrameException fe) {
203             logger.warn("updateDryContactIRState() Ignoring invalid frame {}", msg);
204         }
205     }
206
207     private void triggerButtonChannel(CEN cenMsg) {
208         Integer buttonNumber;
209         try {
210             buttonNumber = cenMsg.getButtonNumber();
211         } catch (FrameException e) {
212             logger.warn("cannot read CEN/CEN+ button. Ignoring message {}", cenMsg);
213             return;
214         }
215         if (buttonNumber == null || buttonNumber < 0 || buttonNumber > 31) {
216             logger.warn("invalid CEN/CEN+ button number: {}. Ignoring message {}", buttonNumber, cenMsg);
217             return;
218         }
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());
226         }
227         final Channel channel = ch;
228         PressEvent pressEv = null;
229         Pressure press = null;
230         try {
231             press = cenMsg.getButtonPressure();
232         } catch (FrameException e) {
233             logger.warn("invalid CEN/CEN+ Press. Ignoring message {}", cenMsg);
234             return;
235         }
236         if (press == null) {
237             logger.warn("invalid CEN/CEN+ Press. Ignoring message {}", cenMsg);
238             return;
239         }
240
241         if (cenMsg instanceof CENScenario) {
242             switch ((CENPressure) press) {
243                 case START_PRESSURE:
244                     pressEv = CENPressEvent.CEN_EVENT_START_PRESS;
245                     break;
246                 case RELEASE_SHORT_PRESSURE:
247                     pressEv = CENPressEvent.CEN_EVENT_SHORT_PRESS;
248                     break;
249                 case EXTENDED_PRESSURE:
250                     pressEv = CENPressEvent.CEN_EVENT_EXTENDED_PRESS;
251                     break;
252                 case RELEASE_EXTENDED_PRESSURE:
253                     pressEv = CENPressEvent.CEN_EVENT_RELEASE_EXTENDED_PRESS;
254                     break;
255                 default:
256                     logger.warn("unsupported CENPress. Ignoring message {}", cenMsg);
257                     return;
258             }
259         } else {
260             switch ((CENPlusPressure) press) {
261                 case SHORT_PRESSURE:
262                     pressEv = CENPlusPressEvent.CENPLUS_EVENT_SHORT_PRESS;
263                     break;
264                 case START_EXTENDED_PRESSURE:
265                     pressEv = CENPlusPressEvent.CENPLUS_EVENT_START_EXTENDED_PRESS;
266                     break;
267                 case EXTENDED_PRESSURE:
268                     pressEv = CENPlusPressEvent.CENPLUS_EVENT_EXTENDED_PRESS;
269                     break;
270                 case RELEASE_EXTENDED_PRESSURE:
271                     pressEv = CENPlusPressEvent.CENPLUS_EVENT_RELEASE_EXTENDED_PRESS;
272                     break;
273                 default:
274                     logger.warn("unsupported CENPlusPress. Ignoring message {}", cenMsg);
275                     return;
276             }
277         }
278         triggerChannel(channel.getUID(), pressEv.toString());
279     }
280
281     private Channel buttonToChannel(int buttonNumber) {
282         ChannelTypeUID channelTypeUID;
283         if (isCENPlus) {
284             channelTypeUID = new ChannelTypeUID(BINDING_ID, CHANNEL_TYPE_CEN_PLUS_BUTTON_EVENT);
285         } else {
286             channelTypeUID = new ChannelTypeUID(BINDING_ID, CHANNEL_TYPE_CEN_BUTTON_EVENT);
287         }
288         return ChannelBuilder
289                 .create(new ChannelUID(getThing().getUID(), CHANNEL_SCENARIO_BUTTON + buttonNumber), "String")
290                 .withType(channelTypeUID).withKind(ChannelKind.TRIGGER).withLabel("Button " + buttonNumber).build();
291     }
292
293     /**
294      * Construct a CEN/CEN+ virtual press message for this device given a pressString and button number
295      *
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
300      */
301     public CEN pressStrToMessage(String pressString, int button) throws IllegalArgumentException {
302         Where w = deviceWhere;
303         if (w == null) {
304             throw new IllegalArgumentException("pressStrToMessage: deviceWhere is null");
305         }
306         if (isCENPlus) {
307             CENPlusPressEvent prEvent = CENPlusPressEvent.fromValue(pressString);
308             if (prEvent != null) {
309                 switch (prEvent) {
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);
318                     default:
319                         throw new IllegalArgumentException("unsupported press type: " + pressString);
320                 }
321             } else {
322                 throw new IllegalArgumentException("unsupported press type: " + pressString);
323             }
324         } else {
325             CENPressEvent prEvent = CENPressEvent.fromValue(pressString);
326             if (prEvent != null) {
327                 switch (prEvent) {
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);
336                     default:
337                         throw new IllegalArgumentException("unsupported press type: " + pressString);
338                 }
339             } else {
340                 throw new IllegalArgumentException("unsupported press type: " + pressString);
341             }
342         }
343     }
344
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());
352         }
353         sc.close();
354         return intSet;
355     }
356
357     @Override
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,
360                 channel);
361     }
362
363     @Override
364     protected void requestChannelState(ChannelUID channel) {
365         if (isDryContactIR) {
366             super.requestChannelState(channel);
367             Where w = deviceWhere;
368             if (w != null) {
369                 try {
370                     send(CENPlusScenario.requestStatus(w.value()));
371                 } catch (OWNException e) {
372                     logger.debug("Exception while requesting state for channel {}: {} ", channel, e.getMessage());
373                     updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
374                 }
375             }
376         } else {
377             logger.debug("requestChannelState() CEN/CEN+ channels are trigger channels and do not have state.");
378         }
379     }
380
381     @Override
382     protected long getRefreshAllLastTS() {
383         return lastAllDevicesRefreshTS;
384     };
385
386     @Override
387     protected void refreshDevice(boolean refreshAll) {
388         if (isDryContactIR) {
389             if (refreshAll) {
390                 logger.debug("--- refreshDevice() : refreshing GENERAL... ({})", thing.getUID());
391                 try {
392                     send(CENPlusScenario.requestStatus("30"));
393                     lastAllDevicesRefreshTS = System.currentTimeMillis();
394                 } catch (OWNException e) {
395                     logger.warn("Excpetion while requesting all devices refresh: {}", e.getMessage());
396                 }
397             } else {
398                 logger.debug("--- refreshDevice() : refreshing SINGLE... ({})", thing.getUID());
399                 requestChannelState(new ChannelUID(thing.getUID(), CHANNEL_DRY_CONTACT_IR));
400             }
401         } else {
402             logger.debug("CEN/CEN+ channels are trigger channels and do not have state. Setting it ONLINE");
403             // put CEN/CEN+ scenario things to ONLINE automatically as they do not have state
404             ThingStatus ts = getThing().getStatus();
405             if (ThingStatus.ONLINE != ts && ThingStatus.REMOVING != ts && ThingStatus.REMOVED != ts) {
406                 updateStatus(ThingStatus.ONLINE);
407             }
408         }
409     }
410
411     @Override
412     protected Where buildBusWhere(String wStr) throws IllegalArgumentException {
413         return new WhereCEN(wStr);
414     }
415 }