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.AbstractMap;
16 import java.util.ArrayList;
17 import java.util.HashMap;
18 import java.util.List;
20 import java.util.Map.Entry;
22 import org.openhab.binding.dmx.internal.DmxBindingConstants.ListenerType;
23 import org.openhab.binding.dmx.internal.DmxThingHandler;
24 import org.openhab.binding.dmx.internal.Util;
25 import org.openhab.binding.dmx.internal.action.ActionState;
26 import org.openhab.binding.dmx.internal.action.BaseAction;
27 import org.openhab.core.library.types.OnOffType;
28 import org.openhab.core.library.types.PercentType;
29 import org.openhab.core.thing.ChannelUID;
30 import org.openhab.core.thing.Thing;
31 import org.slf4j.Logger;
32 import org.slf4j.LoggerFactory;
35 * The {@link DmxChannel} extends {@link BaseDmxChannel} with actions and values
38 * @author Jan N. Klug - Initial contribution
39 * @author Davy Vanherbergen - Initial contribution
41 public class DmxChannel extends BaseDmxChannel {
42 public static final int MIN_VALUE = 0;
43 public static final int MAX_VALUE = 255;
45 private final Logger logger = LoggerFactory.getLogger(DmxChannel.class);
47 private int value = MIN_VALUE;
48 private int suspendedValue = MIN_VALUE;
49 private int lastStateValue = -1;
51 private boolean isSuspended = false;
52 private int refreshTime = 0;
53 private long lastStateTimestamp = 0;
55 private final List<BaseAction> actions = new ArrayList<>();
56 private final List<BaseAction> suspendedActions = new ArrayList<>();
57 private final List<Thing> registeredThings = new ArrayList<>();
59 private final Map<ChannelUID, DmxThingHandler> onOffListeners = new HashMap<>();
60 private final Map<ChannelUID, DmxThingHandler> valueListeners = new HashMap<>();
61 private Entry<ChannelUID, DmxThingHandler> actionListener = null;
63 public DmxChannel(int universeId, int dmxChannelId, int refreshTime) {
64 super(universeId, dmxChannelId);
65 this.refreshTime = refreshTime;
68 public DmxChannel(BaseDmxChannel dmxChannel, int refreshTime) {
70 this.refreshTime = refreshTime;
74 * register a thing with this channel
76 * @param thing a Thing object
78 public void registerThing(Thing thing) {
79 if (!registeredThings.contains(thing)) {
80 logger.debug("registering {} for DMX Channel {}", thing, this);
81 registeredThings.add(thing);
86 * unregister a thing from this object
88 * @param thing a Thing object
90 public void unregisterThing(Thing thing) {
91 if (registeredThings.contains(thing)) {
92 logger.debug("removing {} from DMX Channel {}", thing, this);
93 registeredThings.remove(thing);
98 * check if DMX Channel has any registered objects
100 * @return true or false
102 public boolean hasRegisteredThings() {
103 return !registeredThings.isEmpty();
107 * set a DMX channel value
109 * @param value Integer value (0-255)
111 public void setValue(int value) {
112 this.value = Util.toDmxValue(value) << 8;
113 logger.trace("set dmx channel {} to value {}", this, this.value >> 8);
117 * set a DMX channel value
119 * @param value PercentType (0-100)
121 public void setValue(PercentType value) {
122 this.value = Util.toDmxValue(value) << 8;
123 logger.trace("set dmx channel {} to value {}", this, this.value >> 8);
127 * get the value of this DMX channel
129 * @return value as Integer (0-255)
131 public int getValue() {
132 return Util.toDmxValue(value >> 8);
136 * get the high resolution value of this DMX channel
138 * @return value as Integer (0-65535)
140 public int getHiResValue() {
145 * suspends current value and actions
147 public synchronized void suspendAction() {
149 logger.info("second suspend for actions in DMX channel {}, previous will be lost", this);
151 logger.trace("suspending actions and value for channel {}", this);
154 suspendedValue = value;
155 suspendedActions.clear();
156 if (hasRunningActions()) {
157 suspendedActions.addAll(actions);
164 * resumes previously suspended actions
166 public synchronized void resumeAction() throws IllegalStateException {
169 if (!suspendedActions.isEmpty()) {
170 actions.addAll(suspendedActions);
171 suspendedActions.clear();
172 logger.trace("resuming suspended actions for DMX channel {}", this);
174 value = suspendedValue;
175 logger.trace("resuming suspended value for DMX channel {}", this);
179 throw new IllegalStateException("trying to resume actions in non-suspended DMX channel " + this.toString());
184 * check suspended state
186 * @return true or false
188 public boolean isSuspended() {
193 * clear all running actions
195 public synchronized void clearAction() {
196 logger.trace("clearing all actions for DMX channel {}", this);
198 // remove action listener
199 if (actionListener != null) {
200 actionListener.getValue().updateSwitchState(actionListener.getKey(), OnOffType.OFF);
201 actionListener = null;
206 * Replace the current list of channel actions with the provided one.
208 * @param channelAction action for this channel.
210 public synchronized void setChannelAction(BaseAction channelAction) {
212 actions.add(channelAction);
213 logger.trace("set action {} for DMX channel {}", channelAction, this);
217 * Add a channel action to the current list of channel actions.
219 * @param channelAction action for this channel.
221 public synchronized void addChannelAction(BaseAction channelAction) {
222 actions.add(channelAction);
223 logger.trace("added action {} to channel {} (total {} actions)", channelAction, this, actions.size());
227 * @return true if there are running actions
229 public boolean hasRunningActions() {
230 return !actions.isEmpty();
234 * Move to the next action in the action chain. This method is used by
235 * automatic chains and to manually move to the next action if actions are
236 * set as indefinite (e.g. endless hold). This allows the user to toggle
237 * through a fixed set of values.
239 public synchronized void switchToNextAction() {
240 // push action to the back of the action list
241 BaseAction action = actions.get(0);
245 logger.trace("switching to next action {} on channel {}", actions.get(0), this);
249 * Get the new value for this channel as determined by active actions or the
252 * @param calculationTime UNIX timestamp
253 * @return value 0-255
255 public synchronized Integer getNewValue(long calculationTime) {
256 return (getNewHiResValue(calculationTime) >> 8);
260 * Get the new value for this channel as determined by active actions or the
263 * @param calculationTime UNIX timestamp
264 * @return value 0-65535
266 public synchronized Integer getNewHiResValue(long calculationTime) {
267 if (hasRunningActions()) {
268 logger.trace("checking actions, list is {}", actions);
269 BaseAction action = actions.get(0);
270 value = action.getNewValue(this, calculationTime);
271 if (action.getState() == ActionState.COMPLETED && hasRunningActions()) {
272 switchToNextAction();
273 } else if (action.getState() == ActionState.COMPLETEDFINAL) {
278 // send updates not more than once in a second, and only on value change
279 if ((lastStateValue != value) && (calculationTime - lastStateTimestamp > refreshTime)) {
280 // notify value listeners if value changed
281 for (Entry<ChannelUID, DmxThingHandler> listener : valueListeners.entrySet()) {
282 int dmxValue = Util.toDmxValue(value >> 8);
283 (listener.getValue()).updateChannelValue(listener.getKey(), dmxValue);
284 logger.trace("sending VALUE={} (raw={}) status update to listener {} ({})", dmxValue, value,
285 listener.getValue(), listener.getKey());
288 // notify on/off listeners if on/off state changed
289 if ((lastStateValue == 0) || (value == 0)) {
290 OnOffType state = (value == 0) ? OnOffType.OFF : OnOffType.ON;
291 for (Entry<ChannelUID, DmxThingHandler> listener : onOffListeners.entrySet()) {
292 (listener.getValue()).updateSwitchState(listener.getKey(), state);
293 logger.trace("sending ONOFF={} (raw={}), status update to listener {}", state, value,
298 lastStateValue = value;
299 lastStateTimestamp = calculationTime;
306 * add a channel listener for state updates
308 * @param thingChannel the channel the listener is linked to
309 * @param listener the listener itself
311 public void addListener(ChannelUID thingChannel, DmxThingHandler listener, ListenerType type) {
314 if (valueListeners.containsKey(thingChannel)) {
315 logger.trace("VALUE listener {} already exists in channel {}", thingChannel, this);
317 valueListeners.put(thingChannel, listener);
318 logger.debug("adding VALUE listener {} to channel {}", thingChannel, this);
322 if (actionListener != null) {
323 logger.info("replacing ACTION listener {} with {} in channel {}", actionListener.getValue(),
326 logger.debug("adding ACTION listener {} in channel {}", listener, this);
328 actionListener = new AbstractMap.SimpleEntry<>(thingChannel, listener);
334 * remove listener from channel
336 * @param thingChannel the channel that shall no longer receive updates
338 public void removeListener(ChannelUID thingChannel) {
339 boolean foundListener = false;
340 if (onOffListeners.containsKey(thingChannel)) {
341 onOffListeners.remove(thingChannel);
342 foundListener = true;
343 logger.debug("removing ONOFF listener {} from DMX channel {}", thingChannel, this);
345 if (valueListeners.containsKey(thingChannel)) {
346 valueListeners.remove(thingChannel);
347 foundListener = true;
348 logger.debug("removing VALUE listener {} from DMX channel {}", thingChannel, this);
350 if (actionListener != null && actionListener.getKey().equals(thingChannel)) {
351 actionListener = null;
352 foundListener = true;
353 logger.debug("removing ACTION listener {} from DMX channel {}", thingChannel, this);
355 if (!foundListener) {
356 logger.trace("listener {} not found in DMX channel {}", thingChannel, this);