001package jmri.implementation;
002
003import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
004
005import java.util.Date;
006
007import javax.annotation.Nonnull;
008
009import jmri.InstanceManager;
010import jmri.JmriException;
011import jmri.Timebase;
012import jmri.VariableLight;
013
014import org.slf4j.Logger;
015import org.slf4j.LoggerFactory;
016
017/**
018 * Abstract class providing partial implementation of the logic of the Light
019 * interface when the Intensity is variable.
020 * <p>
021 * Now it includes the transition code, but it only does the steps on the fast
022 * minute clock. Later it may do its own timing but this was simple to piggy
023 * back on the fast minute listener.
024 * <p>
025 * The structure is in part dictated by the limitations of the X10 protocol and
026 * implementations. However, it is not limited to X10 devices only. Other
027 * interfaces that have a way to provide a dimmable light should use it.
028 * <p>
029 * X10 has on/off commands, and separate commands for setting a variable
030 * intensity via "dim" commands. Some X10 implementations use relative dimming,
031 * some use absolute dimming. Some people set the dim level of their Lights and
032 * then just use on/off to turn control the lamps; in that case we don't want to
033 * send dim commands. Further, X10 communications is very slow, and sending a
034 * complete set of dim operations can take a long time. So the algorithm is:
035 * <ul>
036 * <li>Until the intensity has been explicitly set different from 1.0 or 0.0, no
037 * intensity commands are to be sent over the power line.
038 * </ul>
039 * <p>
040 * Unlike the parent class, this stores CurrentIntensity and TargetIntensity in
041 * separate variables.
042 *
043 * @author Dave Duchamp Copyright (C) 2004
044 * @author Ken Cameron Copyright (C) 2008,2009
045 * @author Bob Jacobsen Copyright (C) 2008,2009
046 */
047public abstract class AbstractVariableLight
048        extends AbstractLight implements VariableLight {
049
050    private final static Logger log = LoggerFactory.getLogger(AbstractVariableLight.class);
051
052    public AbstractVariableLight(String systemName, String userName) {
053        super(systemName, userName);
054        initClocks();
055    }
056
057    public AbstractVariableLight(String systemName) {
058        super(systemName);
059        initClocks();
060    }
061
062    /**
063     * System independent instance variables (saved between runs).
064     */
065//    protected double mMaxIntensity = 1.0;     // Uncomment when mMaxIntensity is removed from AbstractLight due to deprecation
066//    protected double mMinIntensity = 0.0;     // Uncomment when mMinIntensity is removed from AbstractLight due to deprecation
067
068    /**
069     * System independent operational instance variables (not saved between
070     * runs).
071     */
072//    protected double mCurrentIntensity = 0.0; // Uncomment when mCurrentIntensity is removed from AbstractLight due to deprecation
073
074    @Override
075    @Nonnull
076    public String describeState(int state) {
077        switch (state) {
078            case INTERMEDIATE: return Bundle.getMessage("LightStateIntermediate");
079            case TRANSITIONINGTOFULLON: return Bundle.getMessage("LightStateTransitioningToFullOn");
080            case TRANSITIONINGHIGHER: return Bundle.getMessage("LightStateTransitioningHigher");
081            case TRANSITIONINGLOWER: return Bundle.getMessage("LightStateTransitioningLower");
082            case TRANSITIONINGTOFULLOFF: return Bundle.getMessage("LightStateTransitioningToFullOff");
083            default: return super.describeState(state);
084        }
085    }
086
087    /**
088     * Handle a request for a state change. ON and OFF go to the MaxIntensity
089     * and MinIntensity, specifically, and all others are not permitted
090     * <p>
091     * ON and OFF avoid use of variable intensity if MaxIntensity = 1.0 or
092     * MinIntensity = 0.0, and no transition is being used.
093     */
094    @Override
095    public void setState(int newState) {
096        if (log.isDebugEnabled()) {
097            log.debug("setState {} was {}", newState, mState);
098        }
099        int oldState = mState;
100        if (newState != ON && newState != OFF) {
101            throw new IllegalArgumentException("cannot set state value " + newState);
102        }
103
104        // first, send the on command
105        sendOnOffCommand(newState);
106
107        if (newState == ON) {
108            // see how to handle intensity
109            if (getMaxIntensity() == 1.0 && getTransitionTime() <= 0) {
110                // treat as not variable light
111                if (log.isDebugEnabled()) {
112                    log.debug("setState({}) considers not variable for ON", newState);
113                }
114                // update the intensity without invoking the hardware
115                notifyTargetIntensityChange(1.0);
116            } else {
117                // requires an intensity change, check for transition
118                if (getTransitionTime() <= 0) {
119                    // no transition, just to directly to target using on/off
120                    if (log.isDebugEnabled()) {
121                        log.debug("setState({}) using variable intensity", newState);
122                    }
123                    // tell the hardware to change intensity
124                    sendIntensity(getMaxIntensity());
125                    // update the intensity value and listeners without invoking the hardware
126                    notifyTargetIntensityChange(getMaxIntensity());
127                } else {
128                    // using transition
129                    startTransition(getMaxIntensity());
130                }
131            }
132        }
133        if (newState == OFF) {
134            // see how to handle intensity
135            if (getMinIntensity() == 0.0 && getTransitionTime() <= 0) {
136                // treat as not variable light
137                if (log.isDebugEnabled()) {
138                    log.debug("setState({}) considers not variable for OFF", newState);
139                }
140                // update the intensity without invoking the hardware
141                notifyTargetIntensityChange(0.0);
142            } else {
143                // requires an intensity change
144                if (getTransitionTime() <= 0) {
145                    // no transition, just to directly to target using on/off
146                    if (log.isDebugEnabled()) {
147                        log.debug("setState({}) using variable intensity", newState);
148                    }
149                    // tell the hardware to change intensity
150                    sendIntensity(getMinIntensity());
151                    // update the intensity value and listeners without invoking the hardware
152                    notifyTargetIntensityChange(getMinIntensity());
153                } else {
154                    // using transition
155                    startTransition(getMinIntensity());
156                }
157            }
158        }
159
160        // notify of state change
161        notifyStateChange(oldState, newState);
162    }
163
164    /**
165     * Set the intended new intensity value for the Light. If transitions are in
166     * use, they will be applied.
167     * <p>
168     * Bound property between 0 and 1.
169     * <p>
170     * A value of 0.0 corresponds to full off, and a value of 1.0 corresponds to
171     * full on.
172     * <p>
173     * Values at or below the minIntensity property will result in the Light
174     * going to the OFF state immediately. Values at or above the maxIntensity
175     * property will result in the Light going to the ON state immediately.
176     *
177     * @throws IllegalArgumentException when intensity is less than 0.0 or more
178     *                                  than 1.0
179     */
180    @Override
181    public void setTargetIntensity(double intensity) {
182        if (log.isDebugEnabled()) {
183            log.debug("setTargetIntensity {}", intensity);
184        }
185        if (intensity < 0.0 || intensity > 1.0) {
186            throw new IllegalArgumentException("Target intensity value " + intensity + " not in legal range");
187        }
188
189        // limit
190        if (intensity > mMaxIntensity) {
191            intensity = mMaxIntensity;
192        }
193        if (intensity < mMinIntensity) {
194            intensity = mMinIntensity;
195        }
196
197        // see if there's a transition in use
198        if (getTransitionTime() > 0.0) {
199            startTransition(intensity);
200        } else {
201            // No transition in use, move immediately
202
203            // Set intensity and intermediate state
204            sendIntensity(intensity);
205            // update value and tell listeners
206            notifyTargetIntensityChange(intensity);
207
208            // decide if this is a state change operation
209            if (intensity >= mMaxIntensity) {
210                setState(ON);
211            } else if (intensity <= mMinIntensity) {
212                setState(OFF);
213            } else {
214                notifyStateChange(mState, INTERMEDIATE);
215            }
216        }
217    }
218
219    /**
220     * Set up to start a transition
221     * @param intensity target intensity
222     */
223    protected void startTransition(double intensity) {
224        // set target value
225        mTransitionTargetIntensity = intensity;
226
227        // set state
228        int nextState;
229        if (intensity >= getMaxIntensity()) {
230            nextState = TRANSITIONINGTOFULLON;
231        } else if (intensity <= getMinIntensity()) {
232            nextState = TRANSITIONINGTOFULLOFF;
233        } else if (intensity >= mCurrentIntensity) {
234            nextState = TRANSITIONINGHIGHER;
235        } else if (intensity <= mCurrentIntensity) {
236            nextState = TRANSITIONINGLOWER;
237        } else {
238            nextState = TRANSITIONING;  // not expected
239        }
240        notifyStateChange(mState, nextState);
241        // make sure clocks running to handle it
242        initClocks();
243    }
244
245    /**
246     * Send a Dim/Bright commands to the hardware to reach a specific intensity.
247     * @param intensity new intensity
248     */
249    abstract protected void sendIntensity(double intensity);
250
251    /**
252     * Send a On/Off Command to the hardware
253     * @param newState new state
254     */
255    abstract protected void sendOnOffCommand(int newState);
256
257    /**
258     * Variables needed for saved values
259     */
260    protected double mTransitionDuration = 0.0;
261
262    /**
263     * Variables needed but not saved to files/panels
264     */
265    protected double mTransitionTargetIntensity = 0.0;
266    protected Date mLastTransitionDate = null;
267    protected long mNextTransitionTs = 0;
268    protected Timebase internalClock = null;
269    protected javax.swing.Timer alarmSyncUpdate = null;
270    protected java.beans.PropertyChangeListener minuteChangeListener = null;
271
272    /**
273     * setup internal clock, start minute listener
274     */
275    private void initClocks() {
276        if (minuteChangeListener != null) {
277            return; // already done
278        }
279        // Create a Timebase listener for the Minute change events
280        internalClock = InstanceManager.getNullableDefault(jmri.Timebase.class);
281        if (internalClock == null) {
282            log.error("No Timebase Instance");
283            return;
284        }
285        minuteChangeListener = (java.beans.PropertyChangeEvent e) -> {
286            //process change to new minute
287            newInternalMinute();
288        };
289        internalClock.addMinuteChangeListener(minuteChangeListener);
290    }
291
292    /**
293     * Layout time has changed to a new minute. Process effect that might be
294     * having on intensity. Currently, this implementation assumes there's a
295     * fixed number of steps between min and max brightness.
296     */
297    @SuppressFBWarnings(value = "FE_FLOATING_POINT_EQUALITY", justification = "OK to compare floating point")
298    protected void newInternalMinute() {
299        double origCurrent = mCurrentIntensity;
300        int origState = mState;
301        int steps = getNumberOfSteps();
302
303        if ((mTransitionDuration > 0) && (steps > 0)) {
304            double stepsPerMinute = steps / mTransitionDuration;
305            double stepSize = 1 / (double) steps;
306            double intensityDiffPerMinute = stepSize * stepsPerMinute;
307            // if we are more than one step away, keep stepping
308            if (Math.abs(mCurrentIntensity - mTransitionTargetIntensity) != 0) {
309                if (log.isDebugEnabled()) {
310                    log.debug("before Target: {} Current: {}", mTransitionTargetIntensity, mCurrentIntensity);
311                }
312
313                if (mTransitionTargetIntensity > mCurrentIntensity) {
314                    mCurrentIntensity = mCurrentIntensity + intensityDiffPerMinute;
315                    if (mCurrentIntensity >= mTransitionTargetIntensity) {
316                        // Done!
317                        mCurrentIntensity = mTransitionTargetIntensity;
318                        if (mCurrentIntensity >= getMaxIntensity()) {
319                            mState = ON;
320                        } else {
321                            mState = INTERMEDIATE;
322                        }
323                    }
324                } else {
325                    mCurrentIntensity = mCurrentIntensity - intensityDiffPerMinute;
326                    if (mCurrentIntensity <= mTransitionTargetIntensity) {
327                        // Done!
328                        mCurrentIntensity = mTransitionTargetIntensity;
329                        if (mCurrentIntensity <= getMinIntensity()) {
330                            mState = OFF;
331                        } else {
332                            mState = INTERMEDIATE;
333                        }
334                    }
335                }
336
337                // command new intensity
338                sendIntensity(mCurrentIntensity);
339
340                if (log.isDebugEnabled()) {
341                    log.debug("after Target: {} Current: {}", mTransitionTargetIntensity, mCurrentIntensity);
342                }
343            }
344        }
345        if (origCurrent != mCurrentIntensity) {
346            firePropertyChange("CurrentIntensity", Double.valueOf(origCurrent), Double.valueOf(mCurrentIntensity));
347            if (log.isDebugEnabled()) {
348                log.debug("firePropertyChange intensity {} -> {}", origCurrent, mCurrentIntensity);
349            }
350        }
351        if (origState != mState) {
352            firePropertyChange("KnownState", Integer.valueOf(origState), Integer.valueOf(mState));
353            if (log.isDebugEnabled()) {
354                log.debug("firePropertyChange intensity {} -> {}", origCurrent, mCurrentIntensity);
355            }
356        }
357    }
358
359    /**
360     * Provide the number of steps available between min and max intensity
361     * @return number of steps
362     */
363    abstract protected int getNumberOfSteps();
364
365    /**
366     * Change the stored target intensity value and do notification, but don't
367     * change anything in the hardware
368     */
369    @SuppressFBWarnings(value = "FE_FLOATING_POINT_EQUALITY", justification = "OK to compare floating point")
370    @Override
371    protected void notifyTargetIntensityChange(double intensity) {
372        double oldValue = mCurrentIntensity;
373        mCurrentIntensity = intensity;
374        if (oldValue != intensity) {
375            firePropertyChange("TargetIntensity", oldValue, intensity);
376        }
377    }
378
379    /*.*
380     * Check if this object can handle variable intensity.
381     * <p>
382     * @return true, as this abstract class implements variable intensity.
383     *./
384    @Override
385    public boolean isIntensityVariable() {
386        return true;
387    }
388
389    /**
390     * Can the Light change its intensity setting slowly?
391     * <p>
392     * If true, this Light supports a non-zero value of the transitionTime
393     * property, which controls how long the Light will take to change from one
394     * intensity level to another.
395     * <p>
396     * Unbound property
397     * @return can transition
398     */
399    @Override
400    public boolean isTransitionAvailable() {
401        return true;
402    }
403
404    /**
405     * Set the fast-clock duration for a transition from full ON to full OFF or
406     * vice-versa.
407     * <p>
408     * Bound property
409     * @throws IllegalArgumentException if minutes is not valid
410     */
411    @Override
412    public void setTransitionTime(double minutes) {
413        if (minutes < 0.0) {
414            throw new IllegalArgumentException("Invalid transition time: " + minutes);
415        }
416        mTransitionDuration = minutes;
417    }
418
419    /**
420     * Get the number of fastclock minutes taken by a transition from full ON to
421     * full OFF or vice versa.
422     *
423     * @return 0.0 if the output intensity transition is instantaneous
424     */
425    @Override
426    public double getTransitionTime() {
427        return mTransitionDuration;
428    }
429
430    /**
431     * Convenience method for checking if the intensity of the light is
432     * currently changing due to a transition.
433     * <p>
434     * Bound property so that listeners can conveniently learn when the
435     * transition is over.
436     * @return is transitioning
437     */
438    @SuppressFBWarnings(value = "FE_FLOATING_POINT_EQUALITY", justification = "OK to compare floating point")
439    @Override
440    public boolean isTransitioning() {
441        if (mTransitionTargetIntensity != mCurrentIntensity) {
442            return true;
443        } else {
444            return false;
445        }
446    }
447
448    /**
449     * Get the current intensity value. If the Light is currently transitioning,
450     * this may be either an intermediate or final value.
451     * <p>
452     * A value of 0.0 corresponds to full off, and a value of 1.0 corresponds to
453     * full on.
454     *
455     * @return current intensity
456     */
457    @Override
458    public double getCurrentIntensity() {
459        return mCurrentIntensity;
460    }
461
462    /**
463     * Get the target intensity value for the current transition, if any. If the
464     * Light is not currently transitioning, this is the current intensity
465     * value.
466     * <p>
467     * A value of 0.0 corresponds to full off, and a value of 1.0 corresponds to
468     * full on.
469     * <p>
470     * Bound property
471     *
472     * @return target intensity
473     */
474    @Override
475    public double getTargetIntensity() {
476        return mCurrentIntensity;
477    }
478
479    /**
480     * Used when current state comes from layout
481     * @param value Observed current state
482     */
483    protected void setObservedAnalogValue(double value) {
484        int origState = mState;
485        double origCurrent = mCurrentIntensity;
486
487        if (value >= getMaxIntensity()) {
488            mState = ON;
489            mCurrentIntensity = getMaxIntensity();
490        } else if (value <= getMinIntensity()) {
491            mState = OFF;
492            mCurrentIntensity = getMinIntensity();
493        } else {
494            mState = INTERMEDIATE;
495            mCurrentIntensity = value;
496        }
497
498        mTransitionTargetIntensity = mCurrentIntensity;
499
500        firePropertyChange("CurrentIntensity", origCurrent, mCurrentIntensity);
501
502        if (origState != mState) {
503            firePropertyChange("KnownState", origState, mState);
504            if (log.isDebugEnabled()) {
505                log.debug("firePropertyChange intensity {} -> {}", origCurrent, mCurrentIntensity);
506            }
507        }
508
509    }
510
511    @Override
512    public void setCommandedAnalogValue(double value) throws JmriException {
513        int origState = mState;
514        double origCurrent = mCurrentIntensity;
515
516        if (mCurrentIntensity >= getMaxIntensity()) {
517            mState = ON;
518            mCurrentIntensity = getMaxIntensity();
519        } else if (mCurrentIntensity <= getMinIntensity()) {
520            mState = OFF;
521            mCurrentIntensity = getMinIntensity();
522        } else {
523            mState = INTERMEDIATE;
524            mCurrentIntensity = value;
525        }
526
527        mTransitionTargetIntensity = mCurrentIntensity;
528
529        // first, send the on command
530        sendOnOffCommand(mState);
531
532        // command new intensity
533        sendIntensity(mCurrentIntensity);
534        if (log.isDebugEnabled()) {
535            log.debug("set analog value: {}", value);
536        }
537
538        firePropertyChange("CurrentIntensity", origCurrent, mCurrentIntensity);
539        if (log.isDebugEnabled()) {
540            log.debug("firePropertyChange intensity {} -> {}", origCurrent, mCurrentIntensity);
541        }
542
543        if (origState != mState) {
544            firePropertyChange("KnownState", origState, mState);
545            if (log.isDebugEnabled()) {
546                log.debug("firePropertyChange intensity {} -> {}", origCurrent, mCurrentIntensity);
547            }
548        }
549    }
550
551    /**
552     * Get the current value of the minIntensity property.
553     * <p>
554     * A value of 0.0 corresponds to full off, and a value of 1.0 corresponds to
555     * full on.
556     *
557     * @return min intensity value
558     */
559    @Override
560    public double getMinIntensity() {
561        return mMinIntensity;
562    }
563
564    /**
565     * Set the value of the minIntensity property.
566     * <p>
567     * Bound property between 0 and 1.
568     * <p>
569     * A value of 0.0 corresponds to full off, and a value of 1.0 corresponds to
570     * full on.
571     *
572     * @param intensity intensity value
573     * @throws IllegalArgumentException when intensity is less than 0.0 or more
574     *                                  than 1.0
575     * @throws IllegalArgumentException when intensity is not less than the
576     *                                  current value of the maxIntensity
577     *                                  property
578     */
579    @SuppressFBWarnings(value = "FE_FLOATING_POINT_EQUALITY", justification = "OK to compare floating point")
580    @Override
581    public void setMinIntensity(double intensity) {
582        if (intensity < 0.0 || intensity > 1.0) {
583            throw new IllegalArgumentException("Illegal intensity value: " + intensity);
584        }
585        if (intensity >= mMaxIntensity) {
586            throw new IllegalArgumentException("Requested intensity " + intensity + " should be less than maxIntensity " + mMaxIntensity);
587        }
588
589        double oldValue = mMinIntensity;
590        mMinIntensity = intensity;
591
592        if (oldValue != intensity) {
593            firePropertyChange("MinIntensity", Double.valueOf(oldValue), Double.valueOf(intensity));
594        }
595    }
596
597    /**
598     * Get the current value of the maxIntensity property.
599     * <p>
600     * A value of 0.0 corresponds to full off, and a value of 1.0 corresponds to
601     * full on.
602     *
603     * @return max intensity
604     */
605    @Override
606    public double getMaxIntensity() {
607        return mMaxIntensity;
608    }
609
610    /**
611     * Set the value of the maxIntensity property.
612     * <p>
613     * Bound property between 0 and 1.
614     * <p>
615     * A value of 0.0 corresponds to full off, and a value of 1.0 corresponds to
616     * full on.
617     *
618     * @param intensity max intensity
619     * @throws IllegalArgumentException when intensity is less than 0.0 or more
620     *                                  than 1.0
621     * @throws IllegalArgumentException when intensity is not greater than the
622     *                                  current value of the minIntensity
623     *                                  property
624     */
625    @SuppressFBWarnings(value = "FE_FLOATING_POINT_EQUALITY", justification = "OK to compare floating point")
626    @Override
627    public void setMaxIntensity(double intensity) {
628        if (intensity < 0.0 || intensity > 1.0) {
629            throw new IllegalArgumentException("Illegal intensity value: " + intensity);
630        }
631        if (intensity <= mMinIntensity) {
632            throw new IllegalArgumentException("Requested intensity " + intensity + " must be higher than minIntensity " + mMinIntensity);
633        }
634
635        double oldValue = mMaxIntensity;
636        mMaxIntensity = intensity;
637
638        if (oldValue != intensity) {
639            firePropertyChange("MaxIntensity", oldValue, intensity);
640        }
641    }
642
643    /** {@inheritDoc} */
644    @Override
645    public double getState(double v) {
646        return getCommandedAnalogValue();
647    }
648
649    /** {@inheritDoc} */
650    @Override
651    public void setState(double newState) throws JmriException {
652        setCommandedAnalogValue(newState);
653    }
654
655    @Override
656    public double getResolution() {
657        return 1.0 / getNumberOfSteps();
658    }
659
660    @Override
661    public double getCommandedAnalogValue() {
662        return getCurrentIntensity();
663    }
664
665    @Override
666    public double getMin() {
667        return getMinIntensity();
668    }
669
670    @Override
671    public double getMax() {
672        return getMaxIntensity();
673    }
674
675    @Override
676    public AbsoluteOrRelative getAbsoluteOrRelative() {
677        return AbsoluteOrRelative.ABSOLUTE;
678    }
679
680}