]> git.basschouten.com Git - openhab-addons.git/blob
f4644edb90099bccb73e0fba232bf87f2375b0d4
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2024 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.Set;
18 import java.util.concurrent.TimeUnit;
19
20 import org.eclipse.jdt.annotation.NonNullByDefault;
21 import org.eclipse.jdt.annotation.Nullable;
22 import org.openhab.binding.openwebnet.internal.OpenWebNetBindingConstants;
23 import org.openhab.core.library.types.IncreaseDecreaseType;
24 import org.openhab.core.library.types.OnOffType;
25 import org.openhab.core.library.types.PercentType;
26 import org.openhab.core.thing.ChannelUID;
27 import org.openhab.core.thing.Thing;
28 import org.openhab.core.thing.ThingStatus;
29 import org.openhab.core.thing.ThingStatusDetail;
30 import org.openhab.core.thing.ThingTypeUID;
31 import org.openhab.core.types.Command;
32 import org.openwebnet4j.communication.OWNException;
33 import org.openwebnet4j.message.BaseOpenMessage;
34 import org.openwebnet4j.message.FrameException;
35 import org.openwebnet4j.message.Lighting;
36 import org.openwebnet4j.message.What;
37 import org.openwebnet4j.message.Where;
38 import org.openwebnet4j.message.WhereLightAutom;
39 import org.openwebnet4j.message.WhereZigBee;
40 import org.openwebnet4j.message.Who;
41 import org.slf4j.Logger;
42 import org.slf4j.LoggerFactory;
43
44 /**
45  * The {@link OpenWebNetLightingHandler} is responsible for handling
46  * commands/messages for a Lighting OpenWebNet device.
47  * It extends the abstract {@link OpenWebNetThingHandler}.
48  *
49  * @author Massimo Valla - Initial contribution
50  */
51 @NonNullByDefault
52 public class OpenWebNetLightingHandler extends OpenWebNetThingHandler {
53
54     private final Logger logger = LoggerFactory.getLogger(OpenWebNetLightingHandler.class);
55
56     public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES = OpenWebNetBindingConstants.LIGHTING_SUPPORTED_THING_TYPES;
57
58     // interval to interpret ON as response to requestStatus
59     private static final int BRIGHTNESS_STATUS_REQUEST_INTERVAL_MSEC = 250;
60
61     // time to wait before sending a statusRequest, to avoid repeated requests and
62     // ensure dimmer has reached its final level
63     private static final int BRIGHTNESS_STATUS_REQUEST_DELAY_MSEC = 900;
64
65     private static final int UNKNOWN_STATE = 1000;
66
67     private long lastBrightnessChangeSentTS = 0; // timestamp when last brightness change was sent to the device
68
69     private long lastStatusRequestSentTS = 0; // timestamp when last status request was sent to the device
70
71     private static long lastAllDevicesRefreshTS = 0; // ts when last all device refresh was sent for this handler
72
73     private int brightness = UNKNOWN_STATE; // current brightness percent value for this device
74
75     private int brightnessBeforeOff = UNKNOWN_STATE; // latest brightness before device was set to off
76
77     public OpenWebNetLightingHandler(Thing thing) {
78         super(thing);
79     }
80
81     @Override
82     protected void requestChannelState(ChannelUID channel) {
83         super.requestChannelState(channel);
84         if (deviceWhere != null) {
85             try {
86                 lastStatusRequestSentTS = System.currentTimeMillis();
87                 send(Lighting.requestStatus(toWhere(channel.getId())));
88             } catch (OWNException e) {
89                 logger.debug("Exception while requesting state for channel {}: {} ", channel, e.getMessage());
90                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
91             }
92         }
93     }
94
95     @Override
96     protected long getRefreshAllLastTS() {
97         return lastAllDevicesRefreshTS;
98     };
99
100     @Override
101     protected void refreshDevice(boolean refreshAll) {
102         if (refreshAll) {
103             logger.debug("--- refreshDevice() : refreshing GENERAL... ({})", thing.getUID());
104             try {
105                 send(Lighting.requestStatus(WhereLightAutom.GENERAL.value()));
106                 lastAllDevicesRefreshTS = System.currentTimeMillis();
107             } catch (OWNException e) {
108                 logger.warn("Excpetion while requesting all devices refresh: {}", e.getMessage());
109             }
110         } else {
111             logger.debug("--- refreshDevice() : refreshing SINGLE... ({})", thing.getUID());
112             ThingTypeUID thingType = thing.getThingTypeUID();
113             if (THING_TYPE_ZB_ON_OFF_SWITCH_2UNITS.equals(thingType)) {
114                 /*
115                  * Unfortunately using USB Gateway OpenWebNet both switch endpoints cannot be
116                  * requested at the same time using UNIT 00 because USB stick returns NACK,
117                  * so we need to send a request status for both endpoints
118                  */
119                 requestChannelState(new ChannelUID(thing.getUID(), CHANNEL_SWITCH_02));
120             }
121             requestChannelState(new ChannelUID(thing.getUID(), CHANNEL_SWITCH_01));
122         }
123     }
124
125     @Override
126     protected void handleChannelCommand(ChannelUID channel, Command command) {
127         switch (channel.getId()) {
128             case CHANNEL_BRIGHTNESS:
129                 handleBrightnessCommand(command);
130                 break;
131             case CHANNEL_SWITCH:
132             case CHANNEL_SWITCH_01:
133             case CHANNEL_SWITCH_02:
134                 handleSwitchCommand(channel, command);
135                 break;
136             default: {
137                 logger.warn("Unsupported ChannelUID {}", channel);
138             }
139         }
140     }
141
142     /**
143      * Handles Lighting switch command for a channel
144      *
145      * @param channel the channel
146      * @param command the command
147      */
148     private void handleSwitchCommand(ChannelUID channel, Command command) {
149         logger.debug("handleSwitchCommand() (command={} - channel={})", command, channel);
150         if (command instanceof OnOffType) {
151             try {
152                 if (OnOffType.ON.equals(command)) {
153                     send(Lighting.requestTurnOn(toWhere(channel.getId())));
154                 } else if (OnOffType.OFF.equals(command)) {
155                     send(Lighting.requestTurnOff(toWhere(channel.getId())));
156                 }
157             } catch (OWNException e) {
158                 logger.warn("Exception while processing command {}: {}", command, e.getMessage());
159             }
160         } else {
161             logger.warn("Unsupported command: {}", command);
162         }
163     }
164
165     /**
166      * Handles Lighting brightness command (xx%, INCREASE, DECREASE, ON, OFF)
167      *
168      * @param command the command
169      */
170     private void handleBrightnessCommand(Command command) {
171         logger.debug("handleBrightnessCommand() command={}", command);
172         if (command instanceof PercentType percentCommand) {
173             dimLightTo(percentCommand.intValue(), command);
174         } else if (command instanceof IncreaseDecreaseType) {
175             if (IncreaseDecreaseType.INCREASE.equals(command)) {
176                 dimLightTo(brightness + 10, command);
177             } else { // DECREASE
178                 dimLightTo(brightness - 10, command);
179             }
180         } else if (command instanceof OnOffType) {
181             if (OnOffType.ON.equals(command)) {
182                 dimLightTo(brightnessBeforeOff, command);
183             } else { // OFF
184                 dimLightTo(0, command);
185             }
186         } else {
187             logger.warn("Cannot handle command {} for thing {}", command, getThing().getUID());
188         }
189     }
190
191     /**
192      * Helper method to dim light to given percent
193      */
194     private void dimLightTo(int percent, Command command) {
195         logger.debug("   DIM dimLightTo({}) bri={} briBeforeOff={}", percent, brightness, brightnessBeforeOff);
196         int newBrightness = percent;
197         if (newBrightness == UNKNOWN_STATE) { // we do not know last brightness -> set dimmer to 100%
198             newBrightness = 100;
199         } else if (newBrightness <= 0) {
200             if (brightness == 0) {
201                 logger.debug("   DIM bri already 0: no need to save it");
202             } else {
203                 brightnessBeforeOff = brightness;
204                 logger.debug("   DIM saved briBeforeOff={} before sending bri=0 command to device",
205                         brightnessBeforeOff);
206             }
207         } else if (newBrightness > 100) {
208             newBrightness = 100;
209         }
210         What newBrightnessWhat = Lighting.percentToWhat(newBrightness);
211         logger.debug("   DIM newBrightness={} newBrightnessWhat={}", newBrightness, newBrightnessWhat);
212         @Nullable
213         What brightnessWhat = null;
214         if (brightness != UNKNOWN_STATE) {
215             brightnessWhat = Lighting.percentToWhat(brightness);
216         }
217         if (brightnessWhat == null || !newBrightnessWhat.value().equals(brightnessWhat.value())) {
218             logger.debug("   DIM brightnessWhat {} --> {}  WHAT level change needed", brightnessWhat,
219                     newBrightnessWhat);
220             Where w = deviceWhere;
221             if (w != null) {
222                 try {
223                     lastBrightnessChangeSentTS = System.currentTimeMillis();
224                     send(Lighting.requestDimTo(w.value(), newBrightnessWhat));
225                 } catch (OWNException e) {
226                     logger.warn("Exception while sending dimTo request for command {}: {}", command, e.getMessage());
227                 }
228             }
229         } else {
230             logger.debug("   DIM brightnessWhat {} --> {}  NO WHAT level change needed", brightnessWhat,
231                     newBrightnessWhat);
232         }
233         brightness = newBrightness;
234         updateState(CHANNEL_BRIGHTNESS, new PercentType(brightness));
235         logger.debug("   DIM---END bri={} briBeforeOff={}", brightness, brightnessBeforeOff);
236     }
237
238     @Override
239     protected String ownIdPrefix() {
240         return Who.LIGHTING.value().toString();
241     }
242
243     @Override
244     protected void handleMessage(BaseOpenMessage msg) {
245         logger.debug("handleMessage({}) for thing: {}", msg, thing.getUID());
246         super.handleMessage(msg);
247         ThingTypeUID thingType = thing.getThingTypeUID();
248         if (THING_TYPE_ZB_DIMMER.equals(thingType) || THING_TYPE_BUS_DIMMER.equals(thingType)) {
249             updateBrightness((Lighting) msg);
250         } else {
251             updateOnOffState((Lighting) msg);
252         }
253     }
254
255     /**
256      * Updates brightness based on OWN Lighting message received
257      *
258      * @param msg the Lighting message received
259      */
260     private synchronized void updateBrightness(Lighting msg) {
261         logger.debug("  $BRI updateBrightness({})       || bri={} briBeforeOff={}", msg, brightness,
262                 brightnessBeforeOff);
263         long now = System.currentTimeMillis();
264         long delta = now - lastBrightnessChangeSentTS;
265         boolean belowThresh = delta < BRIGHTNESS_STATUS_REQUEST_DELAY_MSEC;
266         logger.debug("  $BRI delta={}ms {}", delta, (belowThresh ? "< DELAY" : ""));
267         if (belowThresh) {
268             // we just sent a command from OH, so we can ignore this message from network
269             logger.debug("  $BRI a command was sent {} < {} ms --> no action needed", delta,
270                     BRIGHTNESS_STATUS_REQUEST_DELAY_MSEC);
271         } else {
272             if (msg.isOn()) {
273                 // if we have not just sent a requestStatus, on ON event we send requestStatus
274                 // to know current level
275                 long deltaStatusReq = now - lastStatusRequestSentTS;
276                 if (deltaStatusReq > BRIGHTNESS_STATUS_REQUEST_INTERVAL_MSEC) {
277                     logger.debug("  $BRI 'ON' is new notification from network, scheduling requestStatus...");
278                     // we must wait BRIGHTNESS_STATUS_REQUEST_DELAY_MSEC to be sure dimmer has
279                     // reached its final level
280                     scheduler.schedule(() -> {
281                         requestChannelState(new ChannelUID(thing.getUID(), CHANNEL_BRIGHTNESS));
282                     }, BRIGHTNESS_STATUS_REQUEST_DELAY_MSEC, TimeUnit.MILLISECONDS);
283                     return;
284                 } else {
285                     // otherwise we interpret this ON event as the requestStatus response event with
286                     // level=1 so we proceed to call updateBrightnessState()
287                     logger.debug("  $BRI 'ON' is the requestStatus response level");
288                 }
289             }
290             logger.debug("  $BRI update from network");
291             if (msg.getWhat() != null) {
292                 updateBrightnessState(msg);
293             } else { // dimension notification
294                 if (msg.getDim() == Lighting.DimLighting.DIMMER_LEVEL_100) {
295                     int newBrightness;
296                     try {
297                         newBrightness = msg.parseDimmerLevel100();
298                     } catch (FrameException fe) {
299                         logger.warn("updateBrightness() Wrong value for dimmerLevel100 in message: {}", msg);
300                         return;
301                     }
302                     logger.debug("  $BRI DIMMER_LEVEL_100 newBrightness={}", newBrightness);
303                     updateState(CHANNEL_BRIGHTNESS, new PercentType(newBrightness));
304                     if (newBrightness == 0) {
305                         brightnessBeforeOff = brightness;
306                     }
307                     brightness = newBrightness;
308                 } else {
309                     logger.warn("updateBrightness() Cannot handle message {} for thing {}", msg, getThing().getUID());
310                     return;
311                 }
312             }
313         }
314         logger.debug("  $BRI---END updateBrightness({}) || bri={} briBeforeOff={}", msg, brightness,
315                 brightnessBeforeOff);
316     }
317
318     /**
319      * Updates light brightness state based on an OWN Lighting message
320      *
321      * @param msg the Lighting message received
322      */
323     private void updateBrightnessState(Lighting msg) {
324         What w = msg.getWhat();
325         if (w != null) {
326             if (Lighting.WhatLighting.ON.equals(w)) {
327                 w = Lighting.WhatLighting.DIMMER_LEVEL_2; // levels start at 2
328             }
329             int newBrightnessWhat = w.value();
330             int brightnessWhat = UNKNOWN_STATE;
331             if (brightness != UNKNOWN_STATE) {
332                 brightnessWhat = Lighting.percentToWhat(brightness).value();
333             }
334             logger.debug("  $BRI brightnessWhat {} --> {}", brightnessWhat, newBrightnessWhat);
335             if (brightnessWhat != newBrightnessWhat) {
336                 int newBrightness = Lighting.levelToPercent(newBrightnessWhat);
337                 updateState(CHANNEL_BRIGHTNESS, new PercentType(newBrightness));
338                 if (msg.isOff()) {
339                     brightnessBeforeOff = brightness;
340                 }
341                 brightness = newBrightness;
342                 logger.debug("  $BRI brightness CHANGED to {}", brightness);
343             } else {
344                 logger.debug("  $BRI no change");
345             }
346         }
347     }
348
349     /**
350      * Updates light on/off state based on an OWN Lighting event message received
351      *
352      * @param msg the Lighting message received
353      */
354     private void updateOnOffState(Lighting msg) {
355         OpenWebNetBridgeHandler brH = bridgeHandler;
356         if (brH != null) {
357             if (msg.isOn() || msg.isOff()) {
358                 String channelId;
359                 if (brH.isBusGateway()) {
360                     channelId = CHANNEL_SWITCH;
361                 } else {
362                     WhereZigBee w = (WhereZigBee) (msg.getWhere());
363                     if (WhereZigBee.UNIT_02.equals(w.getUnit())) {
364                         channelId = CHANNEL_SWITCH_02;
365                     } else {
366                         channelId = CHANNEL_SWITCH_01;
367                     }
368                 }
369                 updateState(channelId, OnOffType.from(msg.isOn()));
370             } else {
371                 logger.debug("updateOnOffState() Ignoring unsupported WHAT for thing {}. Frame={}", getThing().getUID(),
372                         msg.getFrameValue());
373                 return;
374             }
375         }
376     }
377
378     @Override
379     protected Where buildBusWhere(String wStr) throws IllegalArgumentException {
380         return new WhereLightAutom(wStr);
381     }
382
383     /**
384      * Returns a WHERE address string based on channelId string
385      *
386      * @param channelId the channelId string
387      **/
388     @Nullable
389     private String toWhere(String channelId) {
390         Where w = deviceWhere;
391         if (w != null) {
392             OpenWebNetBridgeHandler brH = bridgeHandler;
393             if (brH != null) {
394                 if (brH.isBusGateway()) {
395                     return w.value();
396                 } else if (channelId.equals(CHANNEL_SWITCH_02)) {
397                     return ((WhereZigBee) w).valueWithUnit(WhereZigBee.UNIT_02);
398                 } else { // CHANNEL_SWITCH_01 or other channels
399                     return ((WhereZigBee) w).valueWithUnit(WhereZigBee.UNIT_01);
400                 }
401             }
402         }
403         return null;
404     }
405 }