]> git.basschouten.com Git - openhab-addons.git/blob
e62342936871ec48988a8a30629d51e98ad5650b
[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.Optional;
20 import java.util.Scanner;
21 import java.util.Set;
22 import java.util.TreeSet;
23
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;
55
56 /**
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}.
60  *
61  * @author Massimo Valla - Initial contribution
62  */
63 @NonNullByDefault
64 public class OpenWebNetScenarioHandler extends OpenWebNetThingHandler {
65
66     private final Logger logger = LoggerFactory.getLogger(OpenWebNetScenarioHandler.class);
67
68     private interface PressEvent {
69         @Override
70         public String toString();
71     }
72
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");
78
79         private final String press;
80
81         CENPressEvent(final String pr) {
82             this.press = pr;
83         }
84
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);
88         }
89
90         @Override
91         public String toString() {
92             return press;
93         }
94     }
95
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");
101
102         private final String press;
103
104         CENPlusPressEvent(final String pr) {
105             this.press = pr;
106         }
107
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);
111         }
112
113         @Override
114         public String toString() {
115             return press;
116         }
117     }
118
119     private boolean isDryContactIR = false;
120     private boolean isCENPlus = false;
121
122     private static long lastAllDevicesRefreshTS = 0; // ts when last all device refresh was sent for this handler
123
124     public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES = OpenWebNetBindingConstants.SCENARIO_SUPPORTED_THING_TYPES;
125
126     public OpenWebNetScenarioHandler(Thing thing) {
127         super(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())) {
132             isCENPlus = true;
133             logger.debug("created CEN+ device for thing: {}", getThing().getUID());
134         } else {
135             logger.debug("created CEN device for thing: {}", getThing().getUID());
136         }
137     }
138
139     @Override
140     public void initialize() {
141         super.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();
147                 Channel ch;
148                 for (Integer i : buttons) {
149                     ch = thing.getChannel(CHANNEL_SCENARIO_BUTTON + i);
150                     if (ch == null) {
151                         thingBuilder.withChannel(buttonToChannel(i));
152                         logger.debug("added channel {} to thing: {}", i, getThing().getUID());
153                     }
154                 }
155                 updateThing(thingBuilder.build());
156             } else {
157                 logger.warn("invalid config parameter buttons='{}' for thing {}", buttonsConfig, thing.getUID());
158             }
159         }
160     }
161
162     @Override
163     public Collection<Class<? extends ThingHandlerService>> getServices() {
164         return Set.of(OpenWebNetCENActions.class);
165     }
166
167     @Override
168     protected String ownIdPrefix() {
169         if (isCENPlus || isDryContactIR) {
170             return Who.CEN_PLUS_SCENARIO_SCHEDULER.value().toString();
171         } else {
172             return Who.CEN_SCENARIO_SCHEDULER.value().toString();
173         }
174     }
175
176     @Override
177     protected void handleMessage(BaseOpenMessage msg) {
178         super.handleMessage(msg);
179         if (msg.isCommand()) {
180             if (isDryContactIR) {
181                 updateDryContactIRState((CENPlusScenario) msg);
182             } else {
183                 triggerButtonChannel((CEN) msg);
184             }
185         } else {
186             logger.debug("handleMessage() Ignoring unsupported DIM for thing {}. Frame={}", getThing().getUID(), msg);
187         }
188     }
189
190     private void updateDryContactIRState(CENPlusScenario msg) {
191         logger.debug("updateDryContactIRState() for thing: {}", thing.getUID());
192         try {
193             if (msg.isOn()) {
194                 updateState(CHANNEL_DRY_CONTACT_IR, OnOffType.ON);
195             } else if (msg.isOff()) {
196                 updateState(CHANNEL_DRY_CONTACT_IR, OnOffType.OFF);
197             } else {
198                 logger.debug("updateDryContactIRState() Ignoring unsupported WHAT for thing {}. Frame={}",
199                         getThing().getUID(), msg);
200             }
201         } catch (FrameException fe) {
202             logger.warn("updateDryContactIRState() Ignoring invalid frame {}", msg);
203         }
204     }
205
206     private void triggerButtonChannel(CEN cenMsg) {
207         Integer buttonNumber;
208         try {
209             buttonNumber = cenMsg.getButtonNumber();
210         } catch (FrameException e) {
211             logger.warn("cannot read CEN/CEN+ button. Ignoring message {}", cenMsg);
212             return;
213         }
214         if (buttonNumber == null || buttonNumber < 0 || buttonNumber > 31) {
215             logger.warn("invalid CEN/CEN+ button number: {}. Ignoring message {}", buttonNumber, cenMsg);
216             return;
217         }
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());
225         }
226         final Channel channel = ch;
227         PressEvent pressEv = null;
228         Pressure press = null;
229         try {
230             press = cenMsg.getButtonPressure();
231         } catch (FrameException e) {
232             logger.warn("invalid CEN/CEN+ Press. Ignoring message {}", cenMsg);
233             return;
234         }
235         if (press == null) {
236             logger.warn("invalid CEN/CEN+ Press. Ignoring message {}", cenMsg);
237             return;
238         }
239
240         if (cenMsg instanceof CENScenario) {
241             switch ((CENPressure) press) {
242                 case START_PRESSURE:
243                     pressEv = CENPressEvent.CEN_EVENT_START_PRESS;
244                     break;
245                 case RELEASE_SHORT_PRESSURE:
246                     pressEv = CENPressEvent.CEN_EVENT_SHORT_PRESS;
247                     break;
248                 case EXTENDED_PRESSURE:
249                     pressEv = CENPressEvent.CEN_EVENT_EXTENDED_PRESS;
250                     break;
251                 case RELEASE_EXTENDED_PRESSURE:
252                     pressEv = CENPressEvent.CEN_EVENT_RELEASE_EXTENDED_PRESS;
253                     break;
254                 default:
255                     logger.warn("unsupported CENPress. Ignoring message {}", cenMsg);
256                     return;
257             }
258         } else {
259             switch ((CENPlusPressure) press) {
260                 case SHORT_PRESSURE:
261                     pressEv = CENPlusPressEvent.CENPLUS_EVENT_SHORT_PRESS;
262                     break;
263                 case START_EXTENDED_PRESSURE:
264                     pressEv = CENPlusPressEvent.CENPLUS_EVENT_START_EXTENDED_PRESS;
265                     break;
266                 case EXTENDED_PRESSURE:
267                     pressEv = CENPlusPressEvent.CENPLUS_EVENT_EXTENDED_PRESS;
268                     break;
269                 case RELEASE_EXTENDED_PRESSURE:
270                     pressEv = CENPlusPressEvent.CENPLUS_EVENT_RELEASE_EXTENDED_PRESS;
271                     break;
272                 default:
273                     logger.warn("unsupported CENPlusPress. Ignoring message {}", cenMsg);
274                     return;
275             }
276         }
277         triggerChannel(channel.getUID(), pressEv.toString());
278     }
279
280     private Channel buttonToChannel(int buttonNumber) {
281         ChannelTypeUID channelTypeUID;
282         if (isCENPlus) {
283             channelTypeUID = new ChannelTypeUID(BINDING_ID, CHANNEL_TYPE_CEN_PLUS_BUTTON_EVENT);
284         } else {
285             channelTypeUID = new ChannelTypeUID(BINDING_ID, CHANNEL_TYPE_CEN_BUTTON_EVENT);
286         }
287         return ChannelBuilder
288                 .create(new ChannelUID(getThing().getUID(), CHANNEL_SCENARIO_BUTTON + buttonNumber), "String")
289                 .withType(channelTypeUID).withKind(ChannelKind.TRIGGER).withLabel("Button " + buttonNumber).build();
290     }
291
292     /**
293      * Construct a CEN/CEN+ virtual press message for this device given a pressString and button number
294      *
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
299      */
300     public CEN pressStrToMessage(String pressString, int button) throws IllegalArgumentException {
301         Where w = deviceWhere;
302         if (w == null) {
303             throw new IllegalArgumentException("pressStrToMessage: deviceWhere is null");
304         }
305         if (isCENPlus) {
306             CENPlusPressEvent prEvent = CENPlusPressEvent.fromValue(pressString);
307             if (prEvent != null) {
308                 switch (prEvent) {
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);
317                     default:
318                         throw new IllegalArgumentException("unsupported press type: " + pressString);
319                 }
320             } else {
321                 throw new IllegalArgumentException("unsupported press type: " + pressString);
322             }
323         } else {
324             CENPressEvent prEvent = CENPressEvent.fromValue(pressString);
325             if (prEvent != null) {
326                 switch (prEvent) {
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);
335                     default:
336                         throw new IllegalArgumentException("unsupported press type: " + pressString);
337                 }
338             } else {
339                 throw new IllegalArgumentException("unsupported press type: " + pressString);
340             }
341         }
342     }
343
344     private static Set<Integer> csvStringToSetInt(String s) {
345         TreeSet<Integer> intSet = new TreeSet<Integer>();
346         String sNorm = s.replaceAll("\\s", "");
347         Scanner sc = new Scanner(sNorm);
348         sc.useDelimiter(",");
349         while (sc.hasNextInt()) {
350             intSet.add(sc.nextInt());
351         }
352         sc.close();
353         return intSet;
354     }
355
356     @Override
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,
359                 channel);
360     }
361
362     @Override
363     protected void requestChannelState(ChannelUID channel) {
364         if (isDryContactIR) {
365             super.requestChannelState(channel);
366             Where w = deviceWhere;
367             if (w != null) {
368                 try {
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());
373                 }
374             }
375         } else {
376             logger.debug("requestChannelState() CEN/CEN+ channels are trigger channels and do not have state.");
377         }
378     }
379
380     @Override
381     protected long getRefreshAllLastTS() {
382         return lastAllDevicesRefreshTS;
383     };
384
385     @Override
386     protected void refreshDevice(boolean refreshAll) {
387         if (isDryContactIR) {
388             if (refreshAll) {
389                 logger.debug("--- refreshDevice() : refreshing GENERAL... ({})", thing.getUID());
390                 try {
391                     send(CENPlusScenario.requestStatus("30"));
392                     lastAllDevicesRefreshTS = System.currentTimeMillis();
393                 } catch (OWNException e) {
394                     logger.warn("Excpetion while requesting all devices refresh: {}", e.getMessage());
395                 }
396             } else {
397                 logger.debug("--- refreshDevice() : refreshing SINGLE... ({})", thing.getUID());
398                 requestChannelState(new ChannelUID(thing.getUID(), CHANNEL_DRY_CONTACT_IR));
399             }
400         } else {
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);
406             }
407         }
408     }
409
410     @Override
411     protected Where buildBusWhere(String wStr) throws IllegalArgumentException {
412         return new WhereCEN(wStr);
413     }
414 }