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