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}