2 * Copyright (c) 2010-2021 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.handler;
15 import static org.openhab.binding.openwebnet.OpenWebNetBindingConstants.CHANNEL_BRIGHTNESS;
16 import static org.openhab.binding.openwebnet.OpenWebNetBindingConstants.CHANNEL_SWITCH;
17 import static org.openhab.binding.openwebnet.OpenWebNetBindingConstants.CHANNEL_SWITCH_01;
18 import static org.openhab.binding.openwebnet.OpenWebNetBindingConstants.CHANNEL_SWITCH_02;
19 import static org.openhab.binding.openwebnet.OpenWebNetBindingConstants.THING_TYPE_BUS_DIMMER;
20 import static org.openhab.binding.openwebnet.OpenWebNetBindingConstants.THING_TYPE_ZB_DIMMER;
21 import static org.openhab.binding.openwebnet.OpenWebNetBindingConstants.THING_TYPE_ZB_ON_OFF_SWITCH_2UNITS;
24 import java.util.concurrent.TimeUnit;
26 import org.eclipse.jdt.annotation.NonNullByDefault;
27 import org.eclipse.jdt.annotation.Nullable;
28 import org.openhab.binding.openwebnet.OpenWebNetBindingConstants;
29 import org.openhab.core.library.types.IncreaseDecreaseType;
30 import org.openhab.core.library.types.OnOffType;
31 import org.openhab.core.library.types.PercentType;
32 import org.openhab.core.thing.ChannelUID;
33 import org.openhab.core.thing.Thing;
34 import org.openhab.core.thing.ThingStatus;
35 import org.openhab.core.thing.ThingTypeUID;
36 import org.openhab.core.types.Command;
37 import org.openwebnet4j.communication.OWNException;
38 import org.openwebnet4j.communication.Response;
39 import org.openwebnet4j.message.BaseOpenMessage;
40 import org.openwebnet4j.message.FrameException;
41 import org.openwebnet4j.message.Lighting;
42 import org.openwebnet4j.message.What;
43 import org.openwebnet4j.message.Where;
44 import org.openwebnet4j.message.WhereLightAutom;
45 import org.openwebnet4j.message.WhereZigBee;
46 import org.openwebnet4j.message.Who;
47 import org.slf4j.Logger;
48 import org.slf4j.LoggerFactory;
51 * The {@link OpenWebNetLightingHandler} is responsible for handling commands/messages for a Lighting OpenWebNet device.
52 * It extends the abstract {@link OpenWebNetThingHandler}.
54 * @author Massimo Valla - Initial contribution
57 public class OpenWebNetLightingHandler extends OpenWebNetThingHandler {
59 private final Logger logger = LoggerFactory.getLogger(OpenWebNetLightingHandler.class);
61 public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES = OpenWebNetBindingConstants.LIGHTING_SUPPORTED_THING_TYPES;
63 // interval to interpret ON as response to requestStatus
64 private static final int BRIGHTNESS_STATUS_REQUEST_INTERVAL_MSEC = 250;
66 // time to wait before sending a statusRequest, to avoid repeated requests and ensure dimmer has reached its final
68 private static final int BRIGHTNESS_STATUS_REQUEST_DELAY_MSEC = 900;
70 private static final int UNKNOWN_STATE = 1000;
72 private static long lastAllDevicesRefreshTS = -1; // timestamp when the last request for all device refresh was sent
75 protected static final int ALL_DEVICES_REFRESH_INTERVAL_MSEC = 60000; // interval in msec before sending another all
76 // devices refresh request
78 private long lastBrightnessChangeSentTS = 0; // timestamp when last brightness change was sent to the device
80 private long lastStatusRequestSentTS = 0; // timestamp when last status request was sent to the device
82 private int brightness = UNKNOWN_STATE; // current brightness percent value for this device
84 private int brightnessBeforeOff = UNKNOWN_STATE; // latest brightness before device was set to off
86 private int sw[] = { UNKNOWN_STATE, UNKNOWN_STATE }; // current switch(es) state
88 public OpenWebNetLightingHandler(Thing thing) {
93 protected void requestChannelState(ChannelUID channel) {
94 logger.debug("requestChannelState() thingUID={} channel={}", thing.getUID(), channel.getId());
95 requestStatus(channel.getId());
98 /** helper method to request light status based on channel */
99 private void requestStatus(String channelId) {
100 Where w = deviceWhere;
103 lastStatusRequestSentTS = System.currentTimeMillis();
104 Response res = send(Lighting.requestStatus(toWhere(channelId)));
105 if (res != null && res.isSuccess()) {
106 // set thing online, if not already
107 ThingStatus ts = getThing().getStatus();
108 if (ThingStatus.ONLINE != ts && ThingStatus.REMOVING != ts && ThingStatus.REMOVED != ts) {
109 updateStatus(ThingStatus.ONLINE);
112 } catch (OWNException e) {
113 logger.warn("requestStatus() Exception while requesting light state: {}", e.getMessage());
116 logger.warn("Could not requestStatus(): deviceWhere is null");
121 protected void refreshDevice(boolean refreshAll) {
122 OpenWebNetBridgeHandler brH = bridgeHandler;
124 if (brH.isBusGateway() && refreshAll) {
125 long now = System.currentTimeMillis();
126 if (now - lastAllDevicesRefreshTS > ALL_DEVICES_REFRESH_INTERVAL_MSEC) {
128 send(Lighting.requestStatus(WhereLightAutom.GENERAL.value()));
129 lastAllDevicesRefreshTS = now;
130 } catch (OWNException e) {
131 logger.warn("Excpetion while requesting all devices refresh: {}", e.getMessage());
134 logger.debug("Refresh all devices just sent...");
136 } else { // USB or BUS-single device
137 ThingTypeUID thingType = thing.getThingTypeUID();
138 if (THING_TYPE_ZB_ON_OFF_SWITCH_2UNITS.equals(thingType)) {
139 // Unfortunately using USB Gateway OpenWebNet both switch endpoints cannot be requested at the same
140 // time using UNIT 00 because USB stick returns NACK, so we need to send a request status for both
142 requestStatus(CHANNEL_SWITCH_02);
144 requestStatus(""); // channel here does not make any difference, see {@link #toWhere()}
150 protected void handleChannelCommand(ChannelUID channel, Command command) {
151 switch (channel.getId()) {
152 case CHANNEL_BRIGHTNESS:
153 handleBrightnessCommand(command);
156 case CHANNEL_SWITCH_01:
157 case CHANNEL_SWITCH_02:
158 handleSwitchCommand(channel, command);
161 logger.warn("Unsupported ChannelUID {}", channel);
167 * Handles Lighting switch command for a channel
169 * @param channel the channel
170 * @param command the command
172 private void handleSwitchCommand(ChannelUID channel, Command command) {
173 logger.debug("handleSwitchCommand() (command={} - channel={})", command, channel);
174 if (command instanceof OnOffType) {
176 if (OnOffType.ON.equals(command)) {
177 send(Lighting.requestTurnOn(toWhere(channel.getId())));
178 } else if (OnOffType.OFF.equals(command)) {
179 send(Lighting.requestTurnOff(toWhere(channel.getId())));
181 } catch (OWNException e) {
182 logger.warn("Exception while processing command {}: {}", command, e.getMessage());
185 logger.warn("Unsupported command: {}", command);
190 * Handles Lighting brightness command (xx%, INCREASE, DECREASE, ON, OFF)
192 * @param command the command
194 private void handleBrightnessCommand(Command command) {
195 logger.debug("handleBrightnessCommand() command={}", command);
196 if (command instanceof PercentType) {
197 dimLightTo(((PercentType) command).intValue(), command);
198 } else if (command instanceof IncreaseDecreaseType) {
199 if (IncreaseDecreaseType.INCREASE.equals(command)) {
200 dimLightTo(brightness + 10, command);
202 dimLightTo(brightness - 10, command);
204 } else if (command instanceof OnOffType) {
205 if (OnOffType.ON.equals(command)) {
206 dimLightTo(brightnessBeforeOff, command);
208 dimLightTo(0, command);
211 logger.warn("Cannot handle command {} for thing {}", command, getThing().getUID());
216 * Helper method to dim light to given percent
218 private void dimLightTo(int percent, Command command) {
219 logger.debug(" DIM dimLightTo({}) bri={} briBeforeOff={}", percent, brightness, brightnessBeforeOff);
220 int newBrightness = percent;
221 if (newBrightness == UNKNOWN_STATE) {
222 // we do not know last brightness -> set dimmer to 100%
224 } else if (newBrightness <= 0) {
226 brightnessBeforeOff = brightness;
227 logger.debug(" DIM saved bri before sending bri=0 command to device");
228 } else if (newBrightness > 100) {
231 What newBrightnessWhat = Lighting.percentToWhat(newBrightness);
232 logger.debug(" DIM newBrightness={} newBrightnessWhat={}", newBrightness, newBrightnessWhat);
234 What brightnessWhat = null;
235 if (brightness != UNKNOWN_STATE) {
236 brightnessWhat = Lighting.percentToWhat(brightness);
238 if (brightnessWhat == null || !newBrightnessWhat.value().equals(brightnessWhat.value())) {
239 logger.debug(" DIM brightnessWhat {} --> {} WHAT level change needed", brightnessWhat,
241 Where w = deviceWhere;
244 lastBrightnessChangeSentTS = System.currentTimeMillis();
245 send(Lighting.requestDimTo(w.value(), newBrightnessWhat));
246 } catch (OWNException e) {
247 logger.warn("Exception while sending dimTo request for command {}: {}", command, e.getMessage());
251 logger.debug(" DIM brightnessWhat {} --> {} NO WHAT level change needed", brightnessWhat,
254 brightness = newBrightness;
255 updateState(CHANNEL_BRIGHTNESS, new PercentType(brightness));
256 logger.debug(" DIM---END bri={} briBeforeOff={}", brightness, brightnessBeforeOff);
260 protected String ownIdPrefix() {
261 return Who.LIGHTING.value().toString();
265 protected void handleMessage(BaseOpenMessage msg) {
266 logger.debug("handleMessage({}) for thing: {}", msg, thing.getUID());
267 super.handleMessage(msg);
268 ThingTypeUID thingType = thing.getThingTypeUID();
269 if (THING_TYPE_ZB_DIMMER.equals(thingType) || THING_TYPE_BUS_DIMMER.equals(thingType)) {
270 updateBrightness((Lighting) msg);
272 updateOnOffState((Lighting) msg);
277 * Updates brightness based on OWN Lighting message received
279 * @param msg the Lighting message received
281 private synchronized void updateBrightness(Lighting msg) {
282 logger.debug(" $BRI updateBrightness({}) || bri={} briBeforeOff={}", msg, brightness,
283 brightnessBeforeOff);
284 long now = System.currentTimeMillis();
285 long delta = now - lastBrightnessChangeSentTS;
286 boolean belowThresh = delta < BRIGHTNESS_STATUS_REQUEST_DELAY_MSEC;
287 logger.debug(" $BRI delta={}ms {}", delta, (belowThresh ? "< DELAY" : ""));
289 // we just sent a command from OH, so we can ignore this message from network
290 logger.debug(" $BRI a command was sent {} < {} ms --> no action needed", delta,
291 BRIGHTNESS_STATUS_REQUEST_DELAY_MSEC);
294 // if we have not just sent a requestStatus, on ON event we send requestStatus to know current level
295 long deltaStatusReq = now - lastStatusRequestSentTS;
296 if (deltaStatusReq > BRIGHTNESS_STATUS_REQUEST_INTERVAL_MSEC) {
297 logger.debug(" $BRI 'ON' is new notification from network, scheduling requestStatus...");
298 // we must wait BRIGHTNESS_STATUS_REQUEST_DELAY_MSEC to be sure dimmer has reached its final level
299 scheduler.schedule(() -> {
300 requestStatus(CHANNEL_BRIGHTNESS);
301 }, BRIGHTNESS_STATUS_REQUEST_DELAY_MSEC, TimeUnit.MILLISECONDS);
304 // otherwise we interpret this ON event as the requestStatus response event with level=1
305 // so we proceed to call updateBrightnessState()
306 logger.debug(" $BRI 'ON' is the requestStatus response level");
309 logger.debug(" $BRI update from network");
310 if (msg.getWhat() != null) {
311 updateBrightnessState(msg);
312 } else { // dimension notification
313 if (msg.getDim() == Lighting.DimLighting.DIMMER_LEVEL_100) {
316 newBrightness = msg.parseDimmerLevel100();
317 } catch (FrameException fe) {
318 logger.warn("updateBrightness() Wrong value for dimmerLevel100 in message: {}", msg);
321 logger.debug(" $BRI DIMMER_LEVEL_100 newBrightness={}", newBrightness);
322 updateState(CHANNEL_BRIGHTNESS, new PercentType(newBrightness));
323 if (newBrightness == 0) {
324 brightnessBeforeOff = brightness;
326 brightness = newBrightness;
328 logger.warn("updateBrightness() Cannot handle message {} for thing {}", msg, getThing().getUID());
333 logger.debug(" $BRI---END updateBrightness({}) || bri={} briBeforeOff={}", msg, brightness,
334 brightnessBeforeOff);
338 * Updates light brightness state based on a OWN Lighting message
340 * @param msg the Lighting message received
342 private void updateBrightnessState(Lighting msg) {
343 What w = msg.getWhat();
345 if (Lighting.WhatLighting.ON.equals(w)) {
346 w = Lighting.WhatLighting.DIMMER_LEVEL_2; // levels start at 2
348 int newBrightnessWhat = w.value();
349 int brightnessWhat = UNKNOWN_STATE;
350 if (brightness != UNKNOWN_STATE) {
351 brightnessWhat = Lighting.percentToWhat(brightness).value();
353 logger.debug(" $BRI brightnessWhat {} --> {}", brightnessWhat, newBrightnessWhat);
354 if (brightnessWhat != newBrightnessWhat) {
355 int newBrightness = Lighting.levelToPercent(newBrightnessWhat);
356 updateState(CHANNEL_BRIGHTNESS, new PercentType(newBrightness));
358 brightnessBeforeOff = brightness;
360 brightness = newBrightness;
361 logger.debug(" $BRI brightness CHANGED to {}", brightness);
363 logger.debug(" $BRI no change");
369 * Updates light on/off state based on a OWN Lighting event message received
371 * @param msg the Lighting message received
373 private void updateOnOffState(Lighting msg) {
374 OpenWebNetBridgeHandler brH = bridgeHandler;
376 if (msg.isOn() || msg.isOff()) {
379 if (brH.isBusGateway()) {
380 channelId = CHANNEL_SWITCH;
382 WhereZigBee w = (WhereZigBee) (msg.getWhere());
383 if (WhereZigBee.UNIT_02.equals(w.getUnit())) {
384 channelId = CHANNEL_SWITCH_02;
387 channelId = CHANNEL_SWITCH_01;
390 int currentSt = sw[switchId];
391 int newSt = (msg.isOn() ? 1 : 0);
392 if (newSt != currentSt) {
393 updateState(channelId, (newSt == 1 ? OnOffType.ON : OnOffType.OFF));
394 sw[switchId] = newSt;
395 logger.debug(" {} ONOFF CHANGED to {}", ownId, newSt);
397 logger.debug(" {} ONOFF no change", ownId);
400 logger.debug("updateOnOffState() Ignoring unsupported WHAT for thing {}. Frame={}", getThing().getUID(),
401 msg.getFrameValue());
408 protected Where buildBusWhere(String wStr) throws IllegalArgumentException {
409 return new WhereLightAutom(wStr);
413 * Returns a WHERE address string based on channelId string
415 * @param channelId the channelId string
418 private String toWhere(String channelId) {
419 Where w = deviceWhere;
421 OpenWebNetBridgeHandler brH = bridgeHandler;
423 if (brH.isBusGateway()) {
425 } else if (channelId.equals(CHANNEL_SWITCH_02)) {
426 return ((WhereZigBee) w).valueWithUnit(WhereZigBee.UNIT_02);
427 } else { // CHANNEL_SWITCH_01 or other channels
428 return ((WhereZigBee) w).valueWithUnit(WhereZigBee.UNIT_01);