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