public String getDescription() {
      Long lastUpTime = connectionLastUp.get();
      Long lastDownTime = connectionLastDown.get();
      Duration serviceFailedStabilizationDelay = getConnectionFailedStabilizationDelay();
      Duration serviceRecoveredStabilizationDelay = getConnectionRecoveredStabilizationDelay();

      return String.format(
          "endpoint=%s; connected=%s; timeNow=%s; lastUp=%s; lastDown=%s; lastPublished=%s; "
              + "currentFailurePeriod=%s; currentRecoveryPeriod=%s",
          getConfig(ENDPOINT),
          connected,
          Time.makeDateString(System.currentTimeMillis()),
          (lastUpTime != null ? Time.makeDateString(lastUpTime) : "<never>"),
          (lastDownTime != null ? Time.makeDateString(lastDownTime) : "<never>"),
          lastPublished,
          (currentFailureStartTime != null ? getTimeStringSince(currentFailureStartTime) : "<none>")
              + " (stabilization "
              + makeTimeStringRounded(serviceFailedStabilizationDelay)
              + ")",
          (currentRecoveryStartTime != null
                  ? getTimeStringSince(currentRecoveryStartTime)
                  : "<none>")
              + " (stabilization "
              + makeTimeStringRounded(serviceRecoveredStabilizationDelay)
              + ")");
    }
  protected String getExplanation(Lifecycle state) {
    Duration serviceFailedStabilizationDelay = getConfig(ENTITY_FAILED_STABILIZATION_DELAY);
    Duration serviceRecoveredStabilizationDelay = getConfig(ENTITY_RECOVERED_STABILIZATION_DELAY);

    return String.format(
        "location=%s; status=%s; lastPublished=%s; timeNow=%s; "
            + "currentFailurePeriod=%s; currentRecoveryPeriod=%s",
        entity.getLocations(),
        (state != null ? state : "<unreported>"),
        lastPublished,
        Time.makeDateString(System.currentTimeMillis()),
        (currentFailureStartTime != null ? getTimeStringSince(currentFailureStartTime) : "<none>")
            + " (stabilization "
            + Time.makeTimeStringRounded(serviceFailedStabilizationDelay)
            + ")",
        (currentRecoveryStartTime != null ? getTimeStringSince(currentRecoveryStartTime) : "<none>")
            + " (stabilization "
            + Time.makeTimeStringRounded(serviceRecoveredStabilizationDelay)
            + ")");
  }
  @Override
  protected void setActualState(Lifecycle state) {
    long now = System.currentTimeMillis();

    synchronized (mutex) {
      if (state == Lifecycle.ON_FIRE) {
        if (lastPublished == LastPublished.FAILED) {
          if (currentRecoveryStartTime != null) {
            if (LOG.isDebugEnabled())
              LOG.debug(
                  "{} health-check for {}, component was recovering, now failing: {}",
                  new Object[] {this, entity, getExplanation(state)});
            currentRecoveryStartTime = null;
            publishEntityRecoveredTime = null;
          } else {
            if (LOG.isTraceEnabled())
              LOG.trace(
                  "{} health-check for {}, component still failed: {}",
                  new Object[] {this, entity, getExplanation(state)});
          }
        } else {
          if (firstUpTime == null && getConfig(ENTITY_FAILED_ONLY_IF_PREVIOUSLY_UP)) {
            // suppress; won't publish
          } else if (currentFailureStartTime == null) {
            if (LOG.isDebugEnabled())
              LOG.debug(
                  "{} health-check for {}, component now failing: {}",
                  new Object[] {this, entity, getExplanation(state)});
            currentFailureStartTime = now;
            publishEntityFailedTime =
                currentFailureStartTime
                    + getConfig(ENTITY_FAILED_STABILIZATION_DELAY).toMilliseconds();
          } else {
            if (LOG.isTraceEnabled())
              LOG.trace(
                  "{} health-check for {}, component continuing failing: {}",
                  new Object[] {this, entity, getExplanation(state)});
          }
        }
        if (setEntityOnFireTime == null) {
          setEntityOnFireTime =
              now + getConfig(SERVICE_ON_FIRE_STABILIZATION_DELAY).toMilliseconds();
        }
        currentRecoveryStartTime = null;
        publishEntityRecoveredTime = null;

      } else if (state == Lifecycle.RUNNING) {
        if (lastPublished == LastPublished.FAILED) {
          if (currentRecoveryStartTime == null) {
            if (LOG.isDebugEnabled())
              LOG.debug(
                  "{} health-check for {}, component now recovering: {}",
                  new Object[] {this, entity, getExplanation(state)});
            currentRecoveryStartTime = now;
            publishEntityRecoveredTime =
                currentRecoveryStartTime
                    + getConfig(ENTITY_RECOVERED_STABILIZATION_DELAY).toMilliseconds();
          } else {
            if (LOG.isTraceEnabled())
              LOG.trace(
                  "{} health-check for {}, component continuing recovering: {}",
                  new Object[] {this, entity, getExplanation(state)});
          }
        } else {
          if (currentFailureStartTime != null) {
            if (LOG.isDebugEnabled())
              LOG.debug(
                  "{} health-check for {}, component was failing, now healthy: {}",
                  new Object[] {this, entity, getExplanation(state)});
          } else {
            if (LOG.isTraceEnabled())
              LOG.trace(
                  "{} health-check for {}, component still healthy: {}",
                  new Object[] {this, entity, getExplanation(state)});
          }
        }
        currentFailureStartTime = null;
        publishEntityFailedTime = null;
        setEntityOnFireTime = null;

      } else {
        if (LOG.isTraceEnabled())
          LOG.trace(
              "{} health-check for {}, in unconfirmed sate: {}",
              new Object[] {this, entity, getExplanation(state)});
      }

      long recomputeIn = Long.MAX_VALUE; // For whether to call recomputeAfterDelay

      if (publishEntityFailedTime != null) {
        long delayBeforeCheck = publishEntityFailedTime - now;
        if (delayBeforeCheck <= 0) {
          if (LOG.isDebugEnabled())
            LOG.debug(
                "{} publishing failed (state={}; currentFailureStartTime={}; now={}",
                new Object[] {
                  this,
                  state,
                  Time.makeDateString(currentFailureStartTime),
                  Time.makeDateString(now)
                });
          publishEntityFailedTime = null;
          lastPublished = LastPublished.FAILED;
          entity.emit(
              HASensors.ENTITY_FAILED,
              new HASensors.FailureDescriptor(entity, getFailureDescription(now)));
        } else {
          recomputeIn = Math.min(recomputeIn, delayBeforeCheck);
        }
      } else if (publishEntityRecoveredTime != null) {
        long delayBeforeCheck = publishEntityRecoveredTime - now;
        if (delayBeforeCheck <= 0) {
          if (LOG.isDebugEnabled())
            LOG.debug(
                "{} publishing recovered (state={}; currentRecoveryStartTime={}; now={}",
                new Object[] {
                  this,
                  state,
                  Time.makeDateString(currentRecoveryStartTime),
                  Time.makeDateString(now)
                });
          publishEntityRecoveredTime = null;
          lastPublished = LastPublished.RECOVERED;
          entity.emit(HASensors.ENTITY_RECOVERED, new HASensors.FailureDescriptor(entity, null));
        } else {
          recomputeIn = Math.min(recomputeIn, delayBeforeCheck);
        }
      }

      if (setEntityOnFireTime != null) {
        long delayBeforeCheck = setEntityOnFireTime - now;
        if (delayBeforeCheck <= 0) {
          if (LOG.isDebugEnabled())
            LOG.debug(
                "{} setting on-fire, now that deferred period has passed (state={})",
                new Object[] {this, state});
          setEntityOnFireTime = null;
          super.setActualState(state);
        } else {
          recomputeIn = Math.min(recomputeIn, delayBeforeCheck);
        }
      } else {
        super.setActualState(state);
      }

      if (recomputeIn < Long.MAX_VALUE) {
        recomputeAfterDelay(recomputeIn);
      }
    }
  }