2 * Copyright (c) 2010-2023 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.*;
18 import java.util.concurrent.TimeUnit;
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;
45 * The {@link OpenWebNetLightingHandler} is responsible for handling
46 * commands/messages for a Lighting OpenWebNet device.
47 * It extends the abstract {@link OpenWebNetThingHandler}.
49 * @author Massimo Valla - Initial contribution
52 public class OpenWebNetLightingHandler extends OpenWebNetThingHandler {
54 private final Logger logger = LoggerFactory.getLogger(OpenWebNetLightingHandler.class);
56 public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES = OpenWebNetBindingConstants.LIGHTING_SUPPORTED_THING_TYPES;
58 // interval to interpret ON as response to requestStatus
59 private static final int BRIGHTNESS_STATUS_REQUEST_INTERVAL_MSEC = 250;
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;
65 private static final int UNKNOWN_STATE = 1000;
67 private long lastBrightnessChangeSentTS = 0; // timestamp when last brightness change was sent to the device
69 private long lastStatusRequestSentTS = 0; // timestamp when last status request was sent to the device
71 private static long lastAllDevicesRefreshTS = 0; // ts when last all device refresh was sent for this handler
73 private int brightness = UNKNOWN_STATE; // current brightness percent value for this device
75 private int brightnessBeforeOff = UNKNOWN_STATE; // latest brightness before device was set to off
77 public OpenWebNetLightingHandler(Thing thing) {
82 protected void requestChannelState(ChannelUID channel) {
83 super.requestChannelState(channel);
84 if (deviceWhere != null) {
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());
96 protected long getRefreshAllLastTS() {
97 return lastAllDevicesRefreshTS;
101 protected void refreshDevice(boolean refreshAll) {
103 logger.debug("--- refreshDevice() : refreshing GENERAL... ({})", thing.getUID());
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());
111 logger.debug("--- refreshDevice() : refreshing SINGLE... ({})", thing.getUID());
112 ThingTypeUID thingType = thing.getThingTypeUID();
113 if (THING_TYPE_ZB_ON_OFF_SWITCH_2UNITS.equals(thingType)) {
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
119 requestChannelState(new ChannelUID(thing.getUID(), CHANNEL_SWITCH_02));
121 requestChannelState(new ChannelUID(thing.getUID(), CHANNEL_SWITCH_01));
126 protected void handleChannelCommand(ChannelUID channel, Command command) {
127 switch (channel.getId()) {
128 case CHANNEL_BRIGHTNESS:
129 handleBrightnessCommand(command);
132 case CHANNEL_SWITCH_01:
133 case CHANNEL_SWITCH_02:
134 handleSwitchCommand(channel, command);
137 logger.warn("Unsupported ChannelUID {}", channel);
143 * Handles Lighting switch command for a channel
145 * @param channel the channel
146 * @param command the command
148 private void handleSwitchCommand(ChannelUID channel, Command command) {
149 logger.debug("handleSwitchCommand() (command={} - channel={})", command, channel);
150 if (command instanceof OnOffType) {
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())));
157 } catch (OWNException e) {
158 logger.warn("Exception while processing command {}: {}", command, e.getMessage());
161 logger.warn("Unsupported command: {}", command);
166 * Handles Lighting brightness command (xx%, INCREASE, DECREASE, ON, OFF)
168 * @param command the command
170 private void handleBrightnessCommand(Command command) {
171 logger.debug("handleBrightnessCommand() command={}", command);
172 if (command instanceof PercentType) {
173 dimLightTo(((PercentType) command).intValue(), command);
174 } else if (command instanceof IncreaseDecreaseType) {
175 if (IncreaseDecreaseType.INCREASE.equals(command)) {
176 dimLightTo(brightness + 10, command);
178 dimLightTo(brightness - 10, command);
180 } else if (command instanceof OnOffType) {
181 if (OnOffType.ON.equals(command)) {
182 dimLightTo(brightnessBeforeOff, command);
184 dimLightTo(0, command);
187 logger.warn("Cannot handle command {} for thing {}", command, getThing().getUID());
192 * Helper method to dim light to given percent
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%
199 } else if (newBrightness <= 0) {
200 if (brightness == 0) {
201 logger.debug(" DIM bri already 0: no need to save it");
203 brightnessBeforeOff = brightness;
204 logger.debug(" DIM saved briBeforeOff={} before sending bri=0 command to device",
205 brightnessBeforeOff);
207 } else if (newBrightness > 100) {
210 What newBrightnessWhat = Lighting.percentToWhat(newBrightness);
211 logger.debug(" DIM newBrightness={} newBrightnessWhat={}", newBrightness, newBrightnessWhat);
213 What brightnessWhat = null;
214 if (brightness != UNKNOWN_STATE) {
215 brightnessWhat = Lighting.percentToWhat(brightness);
217 if (brightnessWhat == null || !newBrightnessWhat.value().equals(brightnessWhat.value())) {
218 logger.debug(" DIM brightnessWhat {} --> {} WHAT level change needed", brightnessWhat,
220 Where w = deviceWhere;
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());
230 logger.debug(" DIM brightnessWhat {} --> {} NO WHAT level change needed", brightnessWhat,
233 brightness = newBrightness;
234 updateState(CHANNEL_BRIGHTNESS, new PercentType(brightness));
235 logger.debug(" DIM---END bri={} briBeforeOff={}", brightness, brightnessBeforeOff);
239 protected String ownIdPrefix() {
240 return Who.LIGHTING.value().toString();
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);
251 updateOnOffState((Lighting) msg);
256 * Updates brightness based on OWN Lighting message received
258 * @param msg the Lighting message received
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" : ""));
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);
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);
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");
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) {
297 newBrightness = msg.parseDimmerLevel100();
298 } catch (FrameException fe) {
299 logger.warn("updateBrightness() Wrong value for dimmerLevel100 in message: {}", msg);
302 logger.debug(" $BRI DIMMER_LEVEL_100 newBrightness={}", newBrightness);
303 updateState(CHANNEL_BRIGHTNESS, new PercentType(newBrightness));
304 if (newBrightness == 0) {
305 brightnessBeforeOff = brightness;
307 brightness = newBrightness;
309 logger.warn("updateBrightness() Cannot handle message {} for thing {}", msg, getThing().getUID());
314 logger.debug(" $BRI---END updateBrightness({}) || bri={} briBeforeOff={}", msg, brightness,
315 brightnessBeforeOff);
319 * Updates light brightness state based on an OWN Lighting message
321 * @param msg the Lighting message received
323 private void updateBrightnessState(Lighting msg) {
324 What w = msg.getWhat();
326 if (Lighting.WhatLighting.ON.equals(w)) {
327 w = Lighting.WhatLighting.DIMMER_LEVEL_2; // levels start at 2
329 int newBrightnessWhat = w.value();
330 int brightnessWhat = UNKNOWN_STATE;
331 if (brightness != UNKNOWN_STATE) {
332 brightnessWhat = Lighting.percentToWhat(brightness).value();
334 logger.debug(" $BRI brightnessWhat {} --> {}", brightnessWhat, newBrightnessWhat);
335 if (brightnessWhat != newBrightnessWhat) {
336 int newBrightness = Lighting.levelToPercent(newBrightnessWhat);
337 updateState(CHANNEL_BRIGHTNESS, new PercentType(newBrightness));
339 brightnessBeforeOff = brightness;
341 brightness = newBrightness;
342 logger.debug(" $BRI brightness CHANGED to {}", brightness);
344 logger.debug(" $BRI no change");
350 * Updates light on/off state based on an OWN Lighting event message received
352 * @param msg the Lighting message received
354 private void updateOnOffState(Lighting msg) {
355 OpenWebNetBridgeHandler brH = bridgeHandler;
357 if (msg.isOn() || msg.isOff()) {
359 if (brH.isBusGateway()) {
360 channelId = CHANNEL_SWITCH;
362 WhereZigBee w = (WhereZigBee) (msg.getWhere());
363 if (WhereZigBee.UNIT_02.equals(w.getUnit())) {
364 channelId = CHANNEL_SWITCH_02;
366 channelId = CHANNEL_SWITCH_01;
369 updateState(channelId, OnOffType.from(msg.isOn()));
371 logger.debug("updateOnOffState() Ignoring unsupported WHAT for thing {}. Frame={}", getThing().getUID(),
372 msg.getFrameValue());
379 protected Where buildBusWhere(String wStr) throws IllegalArgumentException {
380 return new WhereLightAutom(wStr);
384 * Returns a WHERE address string based on channelId string
386 * @param channelId the channelId string
389 private String toWhere(String channelId) {
390 Where w = deviceWhere;
392 OpenWebNetBridgeHandler brH = bridgeHandler;
394 if (brH.isBusGateway()) {
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);