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.dmx.internal.multiverse;
15 import java.util.ArrayList;
16 import java.util.HashMap;
17 import java.util.List;
19 import java.util.Map.Entry;
21 import org.eclipse.jdt.annotation.NonNullByDefault;
22 import org.eclipse.jdt.annotation.Nullable;
23 import org.openhab.binding.dmx.internal.DmxBindingConstants.ListenerType;
24 import org.openhab.binding.dmx.internal.DmxThingHandler;
25 import org.openhab.binding.dmx.internal.Util;
26 import org.openhab.binding.dmx.internal.action.ActionState;
27 import org.openhab.binding.dmx.internal.action.BaseAction;
28 import org.openhab.core.library.types.OnOffType;
29 import org.openhab.core.library.types.PercentType;
30 import org.openhab.core.thing.ChannelUID;
31 import org.openhab.core.thing.Thing;
32 import org.slf4j.Logger;
33 import org.slf4j.LoggerFactory;
36 * The {@link DmxChannel} extends {@link BaseDmxChannel} with actions and values
39 * @author Jan N. Klug - Initial contribution
40 * @author Davy Vanherbergen - Initial contribution
43 public class DmxChannel extends BaseDmxChannel {
44 public static final int MIN_VALUE = 0;
45 public static final int MAX_VALUE = 255;
47 private final Logger logger = LoggerFactory.getLogger(DmxChannel.class);
49 private int value = MIN_VALUE;
50 private int suspendedValue = MIN_VALUE;
51 private int lastStateValue = -1;
53 private boolean isSuspended = false;
54 private int refreshTime = 0;
55 private long lastStateTimestamp = 0;
57 private final List<BaseAction> actions = new ArrayList<>();
58 private final List<BaseAction> suspendedActions = new ArrayList<>();
59 private final List<Thing> registeredThings = new ArrayList<>();
61 private final Map<ChannelUID, DmxThingHandler> onOffListeners = new HashMap<>();
62 private final Map<ChannelUID, DmxThingHandler> valueListeners = new HashMap<>();
63 private @Nullable Entry<ChannelUID, DmxThingHandler> actionListener = null;
65 public DmxChannel(int universeId, int dmxChannelId, int refreshTime) {
66 super(universeId, dmxChannelId);
67 this.refreshTime = refreshTime;
70 public DmxChannel(BaseDmxChannel dmxChannel, int refreshTime) {
72 this.refreshTime = refreshTime;
76 * register a thing with this channel
78 * @param thing a Thing object
80 public void registerThing(Thing thing) {
81 if (!registeredThings.contains(thing)) {
82 logger.debug("registering {} for DMX Channel {}", thing, this);
83 registeredThings.add(thing);
88 * unregister a thing from this object
90 * @param thing a Thing object
92 public void unregisterThing(Thing thing) {
93 if (registeredThings.contains(thing)) {
94 logger.debug("removing {} from DMX Channel {}", thing, this);
95 registeredThings.remove(thing);
100 * check if DMX Channel has any registered objects
102 * @return true or false
104 public boolean hasRegisteredThings() {
105 return !registeredThings.isEmpty();
109 * set a DMX channel value
111 * @param value Integer value (0-255)
113 public void setValue(int value) {
114 this.value = Util.toDmxValue(value) << 8;
115 logger.trace("set dmx channel {} to value {}", this, this.value >> 8);
119 * set a DMX channel value
121 * @param value PercentType (0-100)
123 public void setValue(PercentType value) {
124 this.value = Util.toDmxValue(value) << 8;
125 logger.trace("set dmx channel {} to value {}", this, this.value >> 8);
129 * get the value of this DMX channel
131 * @return value as Integer (0-255)
133 public int getValue() {
134 return Util.toDmxValue(value >> 8);
138 * get the high resolution value of this DMX channel
140 * @return value as Integer (0-65535)
142 public int getHiResValue() {
147 * suspends current value and actions
149 public synchronized void suspendAction() {
151 logger.info("second suspend for actions in DMX channel {}, previous will be lost", this);
153 logger.trace("suspending actions and value for channel {}", this);
156 suspendedValue = value;
157 suspendedActions.clear();
158 if (hasRunningActions()) {
159 suspendedActions.addAll(actions);
166 * resumes previously suspended actions
168 public synchronized void resumeAction() throws IllegalStateException {
171 if (!suspendedActions.isEmpty()) {
172 actions.addAll(suspendedActions);
173 suspendedActions.clear();
174 logger.trace("resuming suspended actions for DMX channel {}", this);
176 value = suspendedValue;
177 logger.trace("resuming suspended value for DMX channel {}", this);
181 throw new IllegalStateException("trying to resume actions in non-suspended DMX channel " + this.toString());
186 * check suspended state
188 * @return true or false
190 public boolean isSuspended() {
195 * clear all running actions
197 public synchronized void clearAction() {
198 logger.trace("clearing all actions for DMX channel {}", this);
200 // remove action listener
201 Map.Entry<ChannelUID, DmxThingHandler> actionListener = this.actionListener;
202 if (actionListener != null) {
203 actionListener.getValue().updateSwitchState(actionListener.getKey(), OnOffType.OFF);
204 this.actionListener = null;
209 * Replace the current list of channel actions with the provided one.
211 * @param channelAction action for this channel.
213 public synchronized void setChannelAction(BaseAction channelAction) {
215 actions.add(channelAction);
216 logger.trace("set action {} for DMX channel {}", channelAction, this);
220 * Add a channel action to the current list of channel actions.
222 * @param channelAction action for this channel.
224 public synchronized void addChannelAction(BaseAction channelAction) {
225 actions.add(channelAction);
226 logger.trace("added action {} to channel {} (total {} actions)", channelAction, this, actions.size());
230 * @return true if there are running actions
232 public boolean hasRunningActions() {
233 return !actions.isEmpty();
237 * Move to the next action in the action chain. This method is used by
238 * automatic chains and to manually move to the next action if actions are
239 * set as indefinite (e.g. endless hold). This allows the user to toggle
240 * through a fixed set of values.
242 public synchronized void switchToNextAction() {
243 // push action to the back of the action list
244 BaseAction action = actions.get(0);
248 logger.trace("switching to next action {} on channel {}", actions.get(0), this);
252 * Get the new value for this channel as determined by active actions or the
255 * @param calculationTime UNIX timestamp
256 * @return value 0-255
258 public synchronized Integer getNewValue(long calculationTime) {
259 return (getNewHiResValue(calculationTime) >> 8);
263 * Get the new value for this channel as determined by active actions or the
266 * @param calculationTime UNIX timestamp
267 * @return value 0-65535
269 public synchronized Integer getNewHiResValue(long calculationTime) {
270 if (hasRunningActions()) {
271 logger.trace("checking actions, list is {}", actions);
272 BaseAction action = actions.get(0);
273 value = action.getNewValue(this, calculationTime);
274 if (action.getState() == ActionState.COMPLETED && hasRunningActions()) {
275 switchToNextAction();
276 } else if (action.getState() == ActionState.COMPLETEDFINAL) {
281 // send updates not more than once in a second, and only on value change
282 if ((lastStateValue != value) && (calculationTime - lastStateTimestamp > refreshTime)) {
283 // notify value listeners if value changed
284 for (Entry<ChannelUID, DmxThingHandler> listener : valueListeners.entrySet()) {
285 int dmxValue = Util.toDmxValue(value >> 8);
286 (listener.getValue()).updateChannelValue(listener.getKey(), dmxValue);
287 logger.trace("sending VALUE={} (raw={}) status update to listener {} ({})", dmxValue, value,
288 listener.getValue(), listener.getKey());
291 // notify on/off listeners if on/off state changed
292 if ((lastStateValue == 0) || (value == 0)) {
293 OnOffType state = OnOffType.from(value != 0);
294 for (Entry<ChannelUID, DmxThingHandler> listener : onOffListeners.entrySet()) {
295 (listener.getValue()).updateSwitchState(listener.getKey(), state);
296 logger.trace("sending ONOFF={} (raw={}), status update to listener {}", state, value,
301 lastStateValue = value;
302 lastStateTimestamp = calculationTime;
309 * add a channel listener for state updates
311 * @param thingChannel the channel the listener is linked to
312 * @param listener the listener itself
314 public void addListener(ChannelUID thingChannel, DmxThingHandler listener, ListenerType type) {
317 if (valueListeners.containsKey(thingChannel)) {
318 logger.trace("VALUE listener {} already exists in channel {}", thingChannel, this);
320 valueListeners.put(thingChannel, listener);
321 logger.debug("adding VALUE listener {} to channel {}", thingChannel, this);
325 Map.Entry<ChannelUID, DmxThingHandler> actionListener = this.actionListener;
326 if (actionListener != null) {
327 logger.info("replacing ACTION listener {} with {} in channel {}", actionListener.getValue(),
330 logger.debug("adding ACTION listener {} in channel {}", listener, this);
332 this.actionListener = Map.entry(thingChannel, listener);
338 * remove listener from channel
340 * @param thingChannel the channel that shall no longer receive updates
342 public void removeListener(ChannelUID thingChannel) {
343 boolean foundListener = false;
344 if (onOffListeners.containsKey(thingChannel)) {
345 onOffListeners.remove(thingChannel);
346 foundListener = true;
347 logger.debug("removing ONOFF listener {} from DMX channel {}", thingChannel, this);
349 if (valueListeners.containsKey(thingChannel)) {
350 valueListeners.remove(thingChannel);
351 foundListener = true;
352 logger.debug("removing VALUE listener {} from DMX channel {}", thingChannel, this);
354 Map.Entry<ChannelUID, DmxThingHandler> actionListener = this.actionListener;
355 if (actionListener != null && actionListener.getKey().equals(thingChannel)) {
356 this.actionListener = null;
357 foundListener = true;
358 logger.debug("removing ACTION listener {} from DMX channel {}", thingChannel, this);
360 if (!foundListener) {
361 logger.trace("listener {} not found in DMX channel {}", thingChannel, this);