/**
 * @license
 * Copyright 2022-2026 Matter.js Authors
 * SPDX-License-Identifier: Apache-2.0
 */
import { OccupancySensingServer } from "#behaviors/occupancy-sensing";
import { TemperatureMeasurementServer } from "#behaviors/temperature-measurement";
import { Thermostat } from "#clusters/thermostat";
import {
  Bytes,
  cropValueRange,
  deepCopy,
  Entropy,
  ImplementationError,
  InternalError,
  Logger,
  Observable
} from "#general";
import { FieldElement } from "#model";
import { Node } from "#node/Node.js";
import { ServerNode } from "#node/ServerNode.js";
import { hasLocalActor, Val } from "#protocol";
import { ClusterType, StatusResponse } from "#types";
import { AtomicWriteHandler } from "./AtomicWriteHandler.js";
import { ThermostatBehavior } from "./ThermostatBehavior.js";
const logger = Logger.get("ThermostatServer");
const ThermostatBehaviorLogicBase = ThermostatBehavior.with(
  Thermostat.Feature.Heating,
  Thermostat.Feature.Cooling,
  Thermostat.Feature.Occupancy,
  Thermostat.Feature.AutoMode,
  Thermostat.Feature.Presets
);
const schema = ThermostatBehaviorLogicBase.schema.extend({
  children: [
    FieldElement({
      name: "PersistedPresets",
      type: "list",
      conformance: "[PRES]",
      quality: "N",
      children: [FieldElement({ name: "entry", type: "PresetStruct" })]
    })
  ]
});
class ThermostatBaseServer extends ThermostatBehaviorLogicBase {
  static schema = schema;
  async initialize() {
    if (this.features.scheduleConfiguration) {
      throw new ImplementationError("ScheduleConfiguration features is deprecated and not allowed to be enabled");
    }
    if (this.features.setback) {
      throw new ImplementationError("Setback feature is deprecated and not allowed to be enabled");
    }
    if (this.features.matterScheduleConfiguration) {
      logger.warn("MatterScheduleConfiguration feature is not yet implemented. Please do not activate it");
    }
    const options = this.endpoint.behaviors.optionsFor(ThermostatBaseServer);
    if (this.features.presets && this.state.persistedPresets === void 0) {
      this.state.persistedPresets = options?.presets ?? [];
    }
    if (this.state.minSetpointDeadBand > 127) {
      this.state.minSetpointDeadBand = 20;
    }
    if (this.state.minSetpointDeadBand < 0 || this.state.minSetpointDeadBand > 127) {
      throw new ImplementationError("minSetpointDeadBand is out of valid range 0..127");
    }
    const node = Node.forEndpoint(this.endpoint);
    this.reactTo(node.lifecycle.online, this.#nodeOnline);
    this.#setupValidations();
    this.#setupModeHandling();
    this.#setupThermostatLogic();
    this.#setupPresets();
    this.internal.minSetpointDeadBand = this.state.minSetpointDeadBand;
    this.internal.controlSequenceOfOperation = this.state.controlSequenceOfOperation;
  }
  #nodeOnline() {
    this.#setupTemperatureMeasurementIntegration();
    this.#setupOccupancyIntegration();
  }
  /**
   * The default implementation of the SetpointRaiseLower command. It handles all validation and setpoint adjustments
   * required by the Matter specification. This method only changes the Occupied setpoints.
   */
  setpointRaiseLower({ mode, amount }) {
    if (mode === Thermostat.SetpointRaiseLowerMode.Heat && !this.features.heating) {
      throw new StatusResponse.InvalidCommandError(
        "Heating feature is not supported but Heat mode was requested"
      );
    }
    if (mode === Thermostat.SetpointRaiseLowerMode.Cool && !this.features.cooling) {
      throw new StatusResponse.InvalidCommandError(
        "Cooling feature is not supported but Cool mode was requested"
      );
    }
    amount *= 10;
    if (mode === Thermostat.SetpointRaiseLowerMode.Both) {
      if (this.features.heating && this.features.cooling) {
        let desiredCoolingSetpoint = this.coolingSetpoint + amount;
        const coolLimit = desiredCoolingSetpoint - this.#clampSetpointToLimits("Cool", desiredCoolingSetpoint);
        let desiredHeatingSetpoint = this.heatingSetpoint + amount;
        const heatLimit = desiredHeatingSetpoint - this.#clampSetpointToLimits("Heat", desiredHeatingSetpoint);
        if (coolLimit !== 0 || heatLimit !== 0) {
          if (Math.abs(coolLimit) <= Math.abs(heatLimit)) {
            desiredHeatingSetpoint = desiredHeatingSetpoint - heatLimit;
            desiredCoolingSetpoint = desiredCoolingSetpoint - heatLimit;
          } else {
            desiredHeatingSetpoint = desiredHeatingSetpoint - coolLimit;
            desiredCoolingSetpoint = desiredCoolingSetpoint - coolLimit;
          }
        }
        this.coolingSetpoint = desiredCoolingSetpoint;
        this.heatingSetpoint = desiredHeatingSetpoint;
      } else if (this.features.cooling) {
        this.coolingSetpoint = this.#clampSetpointToLimits("Cool", this.coolingSetpoint + amount);
      } else {
        this.heatingSetpoint = this.#clampSetpointToLimits("Heat", this.heatingSetpoint + amount);
      }
      return;
    }
    if (mode === Thermostat.SetpointRaiseLowerMode.Cool) {
      const desiredCoolingSetpoint = this.#clampSetpointToLimits("Cool", this.coolingSetpoint + amount);
      if (this.features.autoMode) {
        let heatingSetpoint = this.heatingSetpoint;
        if (desiredCoolingSetpoint - heatingSetpoint < this.setpointDeadBand) {
          heatingSetpoint = desiredCoolingSetpoint - this.setpointDeadBand;
          if (heatingSetpoint === this.#clampSetpointToLimits("Heat", heatingSetpoint)) {
            this.heatingSetpoint = heatingSetpoint;
          } else {
            throw new StatusResponse.InvalidCommandError(
              "Could Not adjust heating setpoint to maintain dead band!"
            );
          }
        }
      }
      this.coolingSetpoint = desiredCoolingSetpoint;
      return;
    }
    if (mode === Thermostat.SetpointRaiseLowerMode.Heat) {
      const desiredHeatingSetpoint = this.#clampSetpointToLimits("Heat", this.heatingSetpoint + amount);
      if (this.features.autoMode) {
        let coolingSetpoint = this.coolingSetpoint;
        if (coolingSetpoint - desiredHeatingSetpoint < this.setpointDeadBand) {
          coolingSetpoint = desiredHeatingSetpoint + this.setpointDeadBand;
          if (coolingSetpoint === this.#clampSetpointToLimits("Cool", coolingSetpoint)) {
            this.coolingSetpoint = coolingSetpoint;
          } else {
            throw new StatusResponse.InvalidCommandError(
              "Could Not adjust cooling setpoint to maintain dead band!"
            );
          }
        }
      }
      this.heatingSetpoint = desiredHeatingSetpoint;
      return;
    }
    throw new StatusResponse.InvalidCommandError(`Unsupported SetpointRaiseLowerMode ${mode}`);
  }
  /**
   * Performs basic validation and sets the active preset handle when valid.
   * This fulfills the basic requirements of the SetActivePresetRequest matter command. Use this method if you need
   * to override setActivePresetRequest to ensure compliance.
   */
  handleSetActivePresetRequest({ presetHandle }) {
    let preset = void 0;
    if (presetHandle !== null) {
      preset = this.state.persistedPresets?.find(
        (p) => p.presetHandle !== null && Bytes.areEqual(p.presetHandle, presetHandle)
      );
      if (preset === void 0) {
        throw new StatusResponse.InvalidCommandError("Requested PresetHandle not found");
      }
    }
    logger.info(`Setting active preset handle to`, presetHandle);
    this.state.activePresetHandle = presetHandle;
    return preset;
  }
  /**
   * This default implementation of the SetActivePresetRequest command handler sets the active preset and
   * (additionally to specification requirements!) adjusts the setpoints to the preset values if defined.
   *
   * If you do not want this behavior, you can override this method but should call handleSetActivePresetRequest to
   * ensure compliance with the specification.
   */
  setActivePresetRequest({ presetHandle }) {
    const preset = this.handleSetActivePresetRequest({ presetHandle });
    if (preset !== void 0) {
      const { heatingSetpoint, coolingSetpoint } = preset;
      if (this.features.heating && heatingSetpoint !== null && heatingSetpoint !== void 0) {
        this.heatingSetpoint = this.#clampSetpointToLimits("Heat", heatingSetpoint);
      }
      if (this.features.cooling && coolingSetpoint !== null && coolingSetpoint !== void 0) {
        this.coolingSetpoint = this.#clampSetpointToLimits("Cool", coolingSetpoint);
      }
    }
  }
  /** Determines if the given context is from a command */
  #isCommandContext(context) {
    return "command" in context && context.command;
  }
  /**
   * Whether the thermostat is currently considered occupied
   * Uses the occupancy state if the feature is supported, otherwise always true
   */
  get occupied() {
    return this.features.occupancy ? this.state.occupancy?.occupied ?? true : true;
  }
  /** The current heating setpoint depending on occupancy */
  get heatingSetpoint() {
    if (this.occupied) {
      return this.state.occupiedHeatingSetpoint;
    }
    return this.state.unoccupiedHeatingSetpoint;
  }
  set heatingSetpoint(value) {
    if (this.occupied) {
      this.state.occupiedHeatingSetpoint = value;
    } else {
      this.state.unoccupiedHeatingSetpoint = value;
    }
  }
  /** The current cooling setpoint depending on occupancy */
  get coolingSetpoint() {
    if (this.occupied) {
      return this.state.occupiedCoolingSetpoint;
    }
    return this.state.unoccupiedCoolingSetpoint;
  }
  set coolingSetpoint(value) {
    if (this.occupied) {
      this.state.occupiedCoolingSetpoint = value;
    } else {
      this.state.unoccupiedCoolingSetpoint = value;
    }
  }
  /** Setup basic Thermostat state and logic */
  #setupThermostatLogic() {
    if (this.state.temperatureSetpointHold !== void 0) {
      if (this.state.temperatureSetpointHoldDuration === void 0) {
        this.state.temperatureSetpointHoldDuration = null;
      }
      if (this.state.setpointHoldExpiryTimestamp === void 0) {
      } else {
        logger.warn(
          "Handling for setpointHoldExpiryTimestamp is not yet implemented. To use this attribute you need to install the needed logic yourself"
        );
      }
    }
  }
  // TODO Add when we adjusted the epoch-s handling to be correct
  /*#handleTemperatureSetpointHoldChange(newValue: Thermostat.TemperatureSetpointHold) {
      if (newValue === Thermostat.TemperatureSetpointHold.SetpointHoldOn) {
          if (
              this.state.temperatureSetpointHoldDuration !== null &&
              this.state.temperatureSetpointHoldDuration! > 0
          ) {
              // TODO: convert to use of Seconds and such and real UTC time
              //  Also requires adjustment in encoding/decoding of the attribute
              const nowUtc = Time.nowMs - 946_684_800_000; // Still not really UTC, but ok for now
              this.state.setpointHoldExpiryTimestamp = Math.floor(
                  nowUtc / 1000 + this.state.temperatureSetpointHoldDuration! * 60,
              );
          }
      } else {
          this.state.setpointHoldExpiryTimestamp = null;
      }
  }*/
  /** Whether heating is allowed in the current ControlSequenceOfOperation and features */
  get heatingAllowed() {
    return this.features.heating && ![
      Thermostat.ControlSequenceOfOperation.CoolingOnly,
      Thermostat.ControlSequenceOfOperation.CoolingAndHeatingWithReheat
    ].includes(this.internal.controlSequenceOfOperation);
  }
  /** Whether cooling is allowed in the current ControlSequenceOfOperation and features */
  get coolingAllowed() {
    return this.features.cooling && ![
      Thermostat.ControlSequenceOfOperation.HeatingOnly,
      Thermostat.ControlSequenceOfOperation.HeatingWithReheat
    ].includes(this.internal.controlSequenceOfOperation);
  }
  /**
   * Adjust the running mode of the thermostat based on the new system mode when the thermostatRunningMode is supported
   */
  adjustRunningMode(newState) {
    if (this.state.thermostatRunningMode === void 0) {
      return;
    }
    switch (newState) {
      case Thermostat.ThermostatRunningMode.Heat:
        if (!this.heatingAllowed) {
          throw new ImplementationError("Heating is not allowed in the current ControlSequenceOfOperation");
        }
        break;
      case Thermostat.ThermostatRunningMode.Cool:
        if (!this.coolingAllowed) {
          throw new ImplementationError("Cooling is not allowed in the current ControlSequenceOfOperation");
        }
        break;
    }
    this.agent.asLocalActor(() => {
      this.state.thermostatRunningMode = newState;
    });
  }
  /**
   * Setup integration with TemperatureMeasurement cluster or external temperature state and intialize internal
   * localTemperature state.
   */
  #setupTemperatureMeasurementIntegration() {
    const preferRemoteTemperature = !!this.state.remoteSensing?.localTemperature;
    if (this.features.localTemperatureNotExposed) {
      if (preferRemoteTemperature) {
        throw new ImplementationError(
          "RemoteSensing cannot be set to LocalTemperature when LocalTemperatureNotExposed feature is enabled"
        );
      }
      logger.debug("LocalTemperatureNotExposed feature is enabled, ignoring local temperature measurement");
      this.state.localTemperature = null;
    }
    let localTemperature = null;
    const localTempEndpoint = this.state.localIndoorTemperatureMeasurementEndpoint;
    if (!preferRemoteTemperature && localTempEndpoint !== void 0) {
      const endpoints = this.env.get(ServerNode).endpoints;
      const endpoint = endpoints.has(localTempEndpoint) ? endpoints.for(localTempEndpoint) : void 0;
      if (endpoint !== void 0 && endpoint.behaviors.has(TemperatureMeasurementServer)) {
        logger.debug(
          `Using existing TemperatureMeasurement cluster on endpoint #${localTempEndpoint} for local temperature measurement`
        );
        if (this.state.externalMeasuredIndoorTemperature !== void 0) {
          logger.warn(
            "Both local TemperatureMeasurement cluster and externalMeasuredIndoorTemperature state are set, using local cluster"
          );
        }
        this.reactTo(
          endpoint.eventsOf(TemperatureMeasurementServer).measuredValue$Changed,
          this.#handleMeasuredTemperatureChange
        );
        localTemperature = endpoint.stateOf(TemperatureMeasurementServer).measuredValue;
      } else {
        logger.warn(
          `No TemperatureMeasurement cluster found on endpoint #${localTempEndpoint}, falling back to externalMeasuredIndoorTemperature state if set`
        );
      }
    } else {
      if (this.state.externalMeasuredIndoorTemperature === void 0) {
        if (this.state.localTemperatureCalibration !== void 0) {
          logger.warn(
            "No local TemperatureMeasurement cluster available, externalMeasuredIndoorTemperature state not set but localTemperatureCalibration is used: Ensure to correctly consider the calibration when updating the localTemperature value"
          );
        }
      } else {
        logger.info("Using measured temperature via externalMeasuredIndoorTemperature state");
        localTemperature = this.state.externalMeasuredIndoorTemperature ?? null;
      }
      this.reactTo(this.events.externalMeasuredIndoorTemperature$Changed, this.#handleMeasuredTemperatureChange);
    }
    if (localTemperature !== null) {
      this.#handleMeasuredTemperatureChange(localTemperature);
    }
  }
  /**
   * Handles changes to the measured temperature, applies calibration and update internal and official state.
   */
  #handleMeasuredTemperatureChange(temperature) {
    if (temperature !== null && this.state.localTemperatureCalibration !== void 0) {
      temperature += this.state.localTemperatureCalibration * 10;
    }
    if (!this.features.localTemperatureNotExposed) {
      this.state.localTemperature = temperature;
    }
    const oldTemperature = this.internal.localTemperature;
    if (temperature !== null && oldTemperature !== temperature) {
      this.internal.localTemperature = temperature;
      this.events.calibratedTemperature$Changed.emit(temperature, oldTemperature, this.context);
    }
  }
  /**
   * Setup integration with OccupancySensing cluster or external occupancy state and initialize internal occupancy
   * state.
   */
  #setupOccupancyIntegration() {
    if (!this.features.occupancy) {
      return;
    }
    let currentOccupancy = true;
    const preferRemoteOccupancy = !!this.state.remoteSensing?.occupancy;
    const localOccupancyEndpoint = this.state.localOccupancyMeasurementEndpoint;
    if (!preferRemoteOccupancy && localOccupancyEndpoint !== void 0) {
      const endpoints = this.env.get(ServerNode).endpoints;
      const endpoint = endpoints.has(localOccupancyEndpoint) ? endpoints.for(localOccupancyEndpoint) : void 0;
      if (endpoint !== void 0 && endpoint.behaviors.has(OccupancySensingServer)) {
        logger.debug(
          `Using existing OccupancySensing cluster on endpoint ${localOccupancyEndpoint} for local occupancy sensing`
        );
        if (this.state.externallyMeasuredOccupancy !== void 0) {
          logger.warn(
            "Both local OccupancySensing cluster and externallyMeasuredOccupancy state are set, using local cluster"
          );
        }
        this.reactTo(endpoint.eventsOf(OccupancySensingServer).occupancy$Changed, this.#handleOccupancyChange);
        currentOccupancy = !!endpoint.stateOf(OccupancySensingServer).occupancy.occupied;
      } else {
        logger.warn(
          `No OccupancySensing cluster found on endpoint ${localOccupancyEndpoint}, falling back to externallyMeasuredOccupancy state if set`
        );
      }
    } else {
      if (this.state.externallyMeasuredOccupancy === void 0) {
        logger.warn(
          "No local OccupancySensing cluster available and externallyMeasuredOccupancy state not set"
        );
      } else {
        logger.info("Using occupancy via externallyMeasuredOccupancy state");
        currentOccupancy = this.state.externallyMeasuredOccupancy;
      }
      this.reactTo(this.events.externallyMeasuredOccupancy$Changed, this.#handleExternalOccupancyChange);
    }
    this.#handleExternalOccupancyChange(currentOccupancy);
  }
  #handleExternalOccupancyChange(newValue) {
    this.state.occupancy = { occupied: newValue };
  }
  #handleOccupancyChange(newValue) {
    this.state.occupancy = newValue;
  }
  /** Setup all validations for the Thermostat behavior */
  #setupValidations() {
    this.#assertUserSetpointLimits("HeatSetpointLimit");
    this.#assertUserSetpointLimits("CoolSetpointLimit");
    this.#clampSetpointToLimits("Heat", this.state.occupiedHeatingSetpoint);
    this.#clampSetpointToLimits("Heat", this.state.unoccupiedHeatingSetpoint);
    this.#clampSetpointToLimits("Cool", this.state.occupiedCoolingSetpoint);
    this.#clampSetpointToLimits("Cool", this.state.unoccupiedCoolingSetpoint);
    this.maybeReactTo(this.events.absMinHeatSetpointLimit$Changing, this.#assertAbsMinHeatSetpointLimitChanging);
    this.maybeReactTo(this.events.minHeatSetpointLimit$Changing, this.#assertMinHeatSetpointLimitChanging);
    this.maybeReactTo(this.events.maxHeatSetpointLimit$Changing, this.#assertMaxHeatSetpointLimitChanging);
    this.maybeReactTo(this.events.absMaxHeatSetpointLimit$Changing, this.#assertAbsMaxHeatSetpointLimitChanging);
    this.maybeReactTo(this.events.absMinCoolSetpointLimit$Changing, this.#assertAbsMinCoolSetpointLimitChanging);
    this.maybeReactTo(this.events.minCoolSetpointLimit$Changing, this.#assertMinCoolSetpointLimitChanging);
    this.maybeReactTo(this.events.maxCoolSetpointLimit$Changing, this.#assertMaxCoolSetpointLimitChanging);
    this.maybeReactTo(this.events.absMaxCoolSetpointLimit$Changing, this.#assertAbsMaxCoolSetpointLimitChanging);
    this.maybeReactTo(this.events.occupiedHeatingSetpoint$Changing, this.#assertOccupiedHeatingSetpointChanging);
    this.maybeReactTo(
      this.events.unoccupiedHeatingSetpoint$Changing,
      this.#assertUnoccupiedHeatingSetpointChanging
    );
    this.maybeReactTo(this.events.occupiedCoolingSetpoint$Changing, this.#assertOccupiedCoolingSetpointChanging);
    this.maybeReactTo(
      this.events.unoccupiedCoolingSetpoint$Changing,
      this.#assertUnoccupiedCoolingSetpointChanging
    );
    this.maybeReactTo(this.events.remoteSensing$Changing, this.#assertRemoteSensingChanging);
    this.maybeReactTo(this.events.minSetpointDeadBand$Changing, this.#ensureMinSetpointDeadBandNotWritable);
    this.reactTo(
      this.events.controlSequenceOfOperation$Changing,
      this.#ensureControlSequenceOfOperationNotWritable
    );
    this.reactTo(this.events.systemMode$Changing, this.#assertSystemModeChanging);
    this.maybeReactTo(this.events.thermostatRunningMode$Changing, this.#assertThermostatRunningModeChanging);
  }
  #assertThermostatRunningModeChanging(newRunningMode) {
    const forbiddenRunningModes = new Array();
    switch (this.internal.controlSequenceOfOperation) {
      case Thermostat.ControlSequenceOfOperation.CoolingOnly:
      case Thermostat.ControlSequenceOfOperation.CoolingAndHeatingWithReheat:
        forbiddenRunningModes.push(Thermostat.ThermostatRunningMode.Heat);
        break;
      case Thermostat.ControlSequenceOfOperation.HeatingOnly:
      case Thermostat.ControlSequenceOfOperation.HeatingWithReheat:
        forbiddenRunningModes.push(Thermostat.ThermostatRunningMode.Cool);
        break;
    }
    if (forbiddenRunningModes.includes(newRunningMode)) {
      throw new StatusResponse.ConstraintErrorError(
        `ThermostatRunningMode ${Thermostat.ThermostatRunningMode[newRunningMode]} is not allowed with ControlSequenceOfOperation ${Thermostat.ControlSequenceOfOperation[this.internal.controlSequenceOfOperation]}`
      );
    }
  }
  #assertSystemModeChanging(newMode) {
    const forbiddenModes = new Array();
    switch (this.internal.controlSequenceOfOperation) {
      case Thermostat.ControlSequenceOfOperation.CoolingOnly:
      case Thermostat.ControlSequenceOfOperation.CoolingAndHeatingWithReheat:
        forbiddenModes.push(Thermostat.SystemMode.Heat, Thermostat.SystemMode.EmergencyHeat);
        break;
      case Thermostat.ControlSequenceOfOperation.HeatingOnly:
      case Thermostat.ControlSequenceOfOperation.HeatingWithReheat:
        forbiddenModes.push(Thermostat.SystemMode.Cool, Thermostat.SystemMode.Precooling);
        break;
    }
    if (forbiddenModes.includes(newMode)) {
      throw new StatusResponse.ConstraintErrorError(
        `SystemMode ${Thermostat.SystemMode[newMode]} is not allowed with ControlSequenceOfOperation ${Thermostat.ControlSequenceOfOperation[this.internal.controlSequenceOfOperation]}`
      );
    }
  }
  /** Attribute is not writable, revert any changes */
  #ensureControlSequenceOfOperationNotWritable() {
    this.state.controlSequenceOfOperation = this.internal.controlSequenceOfOperation;
  }
  /** Attribute is not writable, revert any changes, but also ensure proper errors when write try was invalid */
  #ensureMinSetpointDeadBandNotWritable(value) {
    if (value < 0 || value > 127) {
      throw new StatusResponse.ConstraintErrorError("MinSetpointDeadBand is out of valid range 0..127");
    }
    this.state.minSetpointDeadBand = this.internal.minSetpointDeadBand;
  }
  #assertRemoteSensingChanging(remoteSensing) {
    if (this.features.localTemperatureNotExposed && remoteSensing.localTemperature) {
      throw new StatusResponse.ConstraintErrorError(
        "LocalTemperature is not exposed, so RemoteSensing cannot be set to LocalTemperature"
      );
    }
  }
  #assertUnoccupiedCoolingSetpointChanging(setpoint, _old, context) {
    this.#assertSetpointWithinLimits("Cool", "Unoccupied", setpoint);
    this.#assertSetpointDeadband("Cooling", setpoint);
    if (!this.#isCommandContext(context)) {
      this.#ensureSetpointDeadband("Cooling", "unoccupied", setpoint);
      if (this.features.presets && this.state.activePresetHandle !== null && !this.occupied) {
        this.agent.asLocalActor(() => {
          this.state.activePresetHandle = null;
        });
      }
    }
  }
  #assertUnoccupiedHeatingSetpointChanging(setpoint, _old, context) {
    this.#assertSetpointWithinLimits("Heat", "Unoccupied", setpoint);
    this.#assertSetpointDeadband("Heating", setpoint);
    if (!this.#isCommandContext(context)) {
      this.#ensureSetpointDeadband("Heating", "unoccupied", setpoint);
      if (this.features.presets && this.state.activePresetHandle !== null && !this.occupied) {
        this.agent.asLocalActor(() => {
          this.state.activePresetHandle = null;
        });
      }
    }
  }
  #assertAbsMaxCoolSetpointLimitChanging(absMax) {
    this.#assertUserSetpointLimits("CoolSetpointLimit", { absMax });
  }
  #assertMaxCoolSetpointLimitChanging(max) {
    this.#assertUserSetpointLimits("CoolSetpointLimit", { max });
    if (this.features.autoMode) {
      if (max < this.heatSetpointMaximum + this.setpointDeadBand) {
        throw new StatusResponse.ConstraintErrorError(
          `maxCoolSetpointLimit (${max}) must be greater than or equal to maxHeatSetpointLimit (${this.heatSetpointMaximum}) plus minSetpointDeadBand (${this.setpointDeadBand})`
        );
      }
    }
  }
  #assertMinCoolSetpointLimitChanging(min) {
    this.#assertUserSetpointLimits("CoolSetpointLimit", { min });
    if (this.features.autoMode) {
      if (min < this.heatSetpointMinimum + this.setpointDeadBand) {
        throw new StatusResponse.ConstraintErrorError(
          `minCoolSetpointLimit (${min}) must be greater than or equal to minHeatSetpointLimit (${this.heatSetpointMinimum}) plus minSetpointDeadBand (${this.setpointDeadBand})`
        );
      }
    }
  }
  #assertAbsMinCoolSetpointLimitChanging(absMin) {
    this.#assertUserSetpointLimits("CoolSetpointLimit", { absMin });
  }
  #assertAbsMaxHeatSetpointLimitChanging(absMax) {
    this.#assertUserSetpointLimits("HeatSetpointLimit", { absMax });
  }
  #assertMaxHeatSetpointLimitChanging(max) {
    this.#assertUserSetpointLimits("HeatSetpointLimit", { max });
    if (this.features.autoMode) {
      if (max > this.coolSetpointMaximum - this.setpointDeadBand) {
        throw new StatusResponse.ConstraintErrorError(
          `maxHeatSetpointLimit (${max}) must be less than or equal to maxCoolSetpointLimit (${this.coolSetpointMaximum}) minus minSetpointDeadBand (${this.setpointDeadBand})`
        );
      }
    }
  }
  #assertMinHeatSetpointLimitChanging(min) {
    this.#assertUserSetpointLimits("HeatSetpointLimit", { min });
    if (this.features.autoMode) {
      if (min > this.coolSetpointMinimum - this.setpointDeadBand) {
        throw new StatusResponse.ConstraintErrorError(
          `minHeatSetpointLimit (${min}) must be less than or equal to minCoolSetpointLimit (${this.state.minCoolSetpointLimit}) minus minSetpointDeadBand (${this.setpointDeadBand})`
        );
      }
    }
  }
  #assertAbsMinHeatSetpointLimitChanging(absMin) {
    this.#assertUserSetpointLimits("HeatSetpointLimit", { absMin });
  }
  #assertOccupiedCoolingSetpointChanging(setpoint, _old, context) {
    this.#assertSetpointWithinLimits("Cool", "Occupied", setpoint);
    this.#assertSetpointDeadband("Cooling", setpoint);
    if (!this.#isCommandContext(context)) {
      this.#ensureSetpointDeadband("Cooling", "occupied", setpoint);
      if (this.features.presets && this.state.activePresetHandle !== null && this.occupied) {
        this.agent.asLocalActor(() => {
          this.state.activePresetHandle = null;
        });
      }
    }
  }
  #assertOccupiedHeatingSetpointChanging(setpoint, _old, context) {
    this.#assertSetpointWithinLimits("Heat", "Occupied", setpoint);
    this.#assertSetpointDeadband("Heating", setpoint);
    if (!this.#isCommandContext(context)) {
      this.#ensureSetpointDeadband("Heating", "occupied", setpoint);
      if (this.features.presets && this.state.activePresetHandle !== null && this.occupied) {
        this.agent.asLocalActor(() => {
          this.state.activePresetHandle = null;
        });
      }
    }
  }
  /**
   * The current mode the thermostat is considered to be in based on local temperature and setpoints
   */
  get temperatureConsideration() {
    const localTemp = this.internal.localTemperature;
    if (localTemp === null) {
      return void 0;
    }
    const minSetPointDeadband = this.setpointDeadBand;
    const heatingSetpoint = this.heatingSetpoint;
    const coolingSetpoint = this.coolingSetpoint;
    switch (this.state.systemMode) {
      case Thermostat.SystemMode.Heat:
        if (localTemp < heatingSetpoint) {
          return "belowTarget";
        }
        if (localTemp > coolingSetpoint) {
          return "onTarget";
        }
        break;
      case Thermostat.SystemMode.Cool:
        if (localTemp < heatingSetpoint) {
          return "onTarget";
        }
        if (localTemp > coolingSetpoint) {
          return "aboveTarget";
        }
        break;
      case Thermostat.SystemMode.Auto:
        if (localTemp < heatingSetpoint - minSetPointDeadband) {
          return "belowTarget";
        }
        if (localTemp > coolingSetpoint + minSetPointDeadband) {
          return "aboveTarget";
        }
        break;
    }
    return "onTarget";
  }
  get #heatDefaults() {
    return {
      absMin: 700,
      absMax: 3e3
    };
  }
  get #coolDefaults() {
    return {
      absMin: 1600,
      absMax: 3200
    };
  }
  /**
   * Used to validate generically that user configurable limits must be within device limits follow:
   * * AbsMinHeatSetpointLimit <= MinHeatSetpointLimit <= MaxHeatSetpointLimit <= AbsMaxHeatSetpointLimit
   * * AbsMinCoolSetpointLimit <= MinCoolSetpointLimit <= MaxCoolSetpointLimit <= AbsMaxCoolSetpointLimit
   * Values not provided are taken from the state
   */
  #assertUserSetpointLimits(scope, details = {}) {
    const defaults = scope === "HeatSetpointLimit" ? this.#heatDefaults : this.#coolDefaults;
    const {
      absMin = this.state[`absMin${scope}`] ?? defaults.absMin,
      min = this.state[`min${scope}`] ?? defaults.absMin,
      max = this.state[`max${scope}`] ?? defaults.absMax,
      absMax = this.state[`absMax${scope}`] ?? defaults.absMax
    } = details;
    logger.debug(
      `Validating user setpoint limits for ${scope}: absMin=${absMin}, min=${min}, max=${max}, absMax=${absMax}`
    );
    if (absMin > min) {
      throw new StatusResponse.ConstraintErrorError(
        `absMin${scope} (${absMin}) must be less than or equal to min${scope} (${min})`
      );
    }
    if (min > max) {
      throw new StatusResponse.ConstraintErrorError(
        `min${scope} (${min}) must be less than or equal to max${scope} (${max})`
      );
    }
    if (max > absMax) {
      throw new StatusResponse.ConstraintErrorError(
        `max${scope} (${max}) must be less than or equal to absMax${scope} (${absMax})`
      );
    }
  }
  get heatSetpointMinimum() {
    const absMin = this.state.absMinHeatSetpointLimit ?? this.#heatDefaults.absMin;
    const min = this.state.minHeatSetpointLimit ?? this.#heatDefaults.absMin;
    return Math.max(min, absMin);
  }
  get heatSetpointMaximum() {
    const absMax = this.state.absMaxHeatSetpointLimit ?? this.#heatDefaults.absMax;
    const max = this.state.maxHeatSetpointLimit ?? this.#heatDefaults.absMax;
    return Math.min(max, absMax);
  }
  get coolSetpointMinimum() {
    const absMin = this.state.absMinCoolSetpointLimit ?? this.#coolDefaults.absMin;
    const min = this.state.minCoolSetpointLimit ?? this.#coolDefaults.absMin;
    return Math.max(min, absMin);
  }
  get coolSetpointMaximum() {
    const absMax = this.state.absMaxCoolSetpointLimit ?? this.#coolDefaults.absMax;
    const max = this.state.maxCoolSetpointLimit ?? this.#coolDefaults.absMax;
    return Math.min(max, absMax);
  }
  get setpointDeadBand() {
    return this.features.autoMode ? this.internal.minSetpointDeadBand * 10 : 0;
  }
  #clampSetpointToLimits(scope, setpoint) {
    const limitMin = scope === "Heat" ? this.heatSetpointMinimum : this.coolSetpointMinimum;
    const limitMax = scope === "Heat" ? this.heatSetpointMaximum : this.coolSetpointMaximum;
    const result = cropValueRange(setpoint, limitMin, limitMax);
    if (result !== setpoint) {
      logger.debug(
        `${scope} setpoint (${setpoint}) is out of limits [${limitMin}, ${limitMax}], clamping to ${result}`
      );
    }
    return result;
  }
  /**
   * Used to validate that Setpoints must be within user configurable limits
   */
  #assertSetpointWithinLimits(scope, type, setpoint) {
    const limitMin = scope === "Heat" ? this.heatSetpointMinimum : this.coolSetpointMinimum;
    const limitMax = scope === "Heat" ? this.heatSetpointMaximum : this.coolSetpointMaximum;
    if (limitMin !== void 0 && setpoint < limitMin) {
      throw new StatusResponse.ConstraintErrorError(
        `${scope}${type}Setpoint (${setpoint}) must be greater than or equal to min${scope}SetpointLimit (${limitMin})`
      );
    }
    if (limitMax !== void 0 && setpoint > limitMax) {
      throw new StatusResponse.ConstraintErrorError(
        `${scope}${type}Setpoint (${setpoint}) must be less than or equal to max${scope}SetpointLimit (${limitMax})`
      );
    }
  }
  /**
   * Attempts to ensure that a change to the heating/cooling setpoint maintains the deadband with the cooling/heating
   * setpoint by adjusting the cooling setpoint
   */
  #ensureSetpointDeadband(scope, type, value) {
    if (!this.features.autoMode) {
      return;
    }
    const otherType = scope === "Heating" ? "Cooling" : "Heating";
    const deadband = this.setpointDeadBand;
    const otherSetpoint = otherType === "Heating" ? this.heatingSetpoint : this.coolingSetpoint;
    const otherLimit = otherType === "Heating" ? this.heatSetpointMinimum : this.coolSetpointMaximum;
    if (otherType === "Cooling") {
      const minValidSetpoint = value + deadband;
      logger.debug(
        `Ensuring deadband for ${type}${otherType}Setpoint, min valid setpoint is ${minValidSetpoint}`
      );
      if (otherSetpoint >= minValidSetpoint) {
        return;
      }
      if (minValidSetpoint > otherLimit) {
        throw new StatusResponse.ConstraintErrorError(
          `Cannot adjust cooling setpoint to maintain deadband, would exceed max cooling setpoint (${otherLimit})`
        );
      }
      logger.debug(`Adjusting ${type}${otherType}Setpoint to ${minValidSetpoint} to maintain deadband`);
      this.state[`${type}${otherType}Setpoint`] = minValidSetpoint;
    } else {
      const maxValidSetpoint = value - deadband;
      logger.debug(
        `Ensuring deadband for ${type}${otherType}Setpoint, max valid setpoint is ${maxValidSetpoint}`
      );
      if (otherSetpoint <= maxValidSetpoint) {
        return;
      }
      if (maxValidSetpoint < otherLimit) {
        throw new StatusResponse.ConstraintErrorError(
          `Cannot adjust heating setpoint to maintain deadband, would exceed min heating setpoint (${otherLimit})`
        );
      }
      logger.debug(`Adjusting ${type}${otherType}Setpoint to ${maxValidSetpoint} to maintain deadband`);
      this.state[`${type}${otherType}Setpoint`] = maxValidSetpoint;
    }
  }
  /**
   * Checks to see if it's possible to adjust the heating/cooling setpoint to preserve a given deadband if the
   * cooling/heating setpoint is changed
   */
  #assertSetpointDeadband(type, value) {
    if (!this.features.autoMode) {
      return;
    }
    const deadband = this.setpointDeadBand;
    const otherValue = type === "Heating" ? this.coolSetpointMaximum : this.heatSetpointMinimum;
    if (type === "Heating" && value + deadband > otherValue) {
      throw new StatusResponse.ConstraintErrorError(
        `HeatingSetpoint (${value}) plus deadband (${deadband}) exceeds CoolingSetpoint (${otherValue})`
      );
    } else if (type === "Cooling" && value - deadband < otherValue) {
      throw new StatusResponse.ConstraintErrorError(
        `CoolingSetpoint (${value}) minus deadband (${deadband}) is less than HeatingSetpoint (${otherValue})`
      );
    }
  }
  #setupModeHandling() {
    this.reactTo(this.events.systemMode$Changed, this.#handleSystemModeChange);
    this.maybeReactTo(this.events.thermostatRunningMode$Changed, this.#handleThermostatRunningModeChange);
    if (this.state.useAutomaticModeManagement && this.state.thermostatRunningMode !== void 0) {
      this.reactTo(this.events.calibratedTemperature$Changed, this.#handleTemperatureChangeForMode);
      this.#handleTemperatureChangeForMode(this.internal.localTemperature);
    }
  }
  #handleSystemModeChange(newMode) {
    if (this.state.thermostatRunningMode !== void 0 && newMode !== Thermostat.SystemMode.Auto) {
      this.agent.asLocalActor(() => {
        if (newMode === Thermostat.SystemMode.Off) {
          this.state.thermostatRunningMode = Thermostat.ThermostatRunningMode.Off;
        } else if (newMode === Thermostat.SystemMode.Heat) {
          this.state.thermostatRunningMode = Thermostat.ThermostatRunningMode.Heat;
        } else if (newMode === Thermostat.SystemMode.Cool) {
          this.state.thermostatRunningMode = Thermostat.ThermostatRunningMode.Cool;
        }
      });
    }
  }
  #handleThermostatRunningModeChange(newRunningMode) {
    if (this.state.piCoolingDemand !== void 0) {
      if (newRunningMode === Thermostat.ThermostatRunningMode.Off || newRunningMode === Thermostat.ThermostatRunningMode.Heat) {
        this.state.piCoolingDemand = 0;
      }
    }
    if (this.state.piHeatingDemand !== void 0) {
      if (newRunningMode === Thermostat.ThermostatRunningMode.Off || newRunningMode === Thermostat.ThermostatRunningMode.Cool) {
        this.state.piHeatingDemand = 0;
      }
    }
  }
  /**
   * Handles temperature changes to automatically adjust the system mode based on the current temperature
   * consideration. This logic is disabled by default and will be enabled by setting useAutomaticModeManagement to
   * true.
   */
  #handleTemperatureChangeForMode(temperature) {
    if (temperature === null) {
      return;
    }
    const consideration = this.temperatureConsideration;
    switch (this.state.systemMode) {
      case Thermostat.SystemMode.Heat:
        switch (consideration) {
          case "belowTarget":
            this.adjustRunningMode(Thermostat.ThermostatRunningMode.Heat);
            break;
          default:
            this.adjustRunningMode(Thermostat.ThermostatRunningMode.Off);
            break;
        }
        break;
      case Thermostat.SystemMode.Cool:
        switch (consideration) {
          case "aboveTarget":
            this.adjustRunningMode(Thermostat.ThermostatRunningMode.Cool);
            break;
          default:
            this.adjustRunningMode(Thermostat.ThermostatRunningMode.Off);
            break;
        }
        break;
      case Thermostat.SystemMode.Auto:
        switch (consideration) {
          case "belowTarget":
            this.adjustRunningMode(Thermostat.ThermostatRunningMode.Heat);
            break;
          case "aboveTarget":
            this.adjustRunningMode(Thermostat.ThermostatRunningMode.Cool);
            break;
          default:
            this.adjustRunningMode(Thermostat.ThermostatRunningMode.Off);
            break;
        }
        break;
    }
  }
  #setupPresets() {
    if (!this.features.presets) {
      return;
    }
    this.reactTo(this.events.presets$AtomicChanging, this.#handlePresetsChanging);
    this.reactTo(this.events.presets$AtomicChanged, this.#handlePresetsChanged);
    this.reactTo(this.events.persistedPresets$Changing, this.#handlePresetsChanging);
    this.reactTo(this.events.persistedPresets$Changed, this.#handlePersistedPresetsChanged);
    this.reactTo(this.events.updatePresets, this.#updatePresets, { lock: true });
  }
  /** Handles changes to the Presets attribute and ensures persistedPresets are updated accordingly */
  #updatePresets(newPresets) {
    this.state.persistedPresets = newPresets;
  }
  /**
   * Handles "In-flight" validation of newly written Presets via atomic-write and does the required validations.
   */
  #handlePresetsChanging(newPresets, oldPresets) {
    if (newPresets.length > this.state.numberOfPresets) {
      throw new StatusResponse.ResourceExhaustedError(
        `Number of presets (${newPresets.length}) exceeds NumberOfPresets (${this.state.numberOfPresets})`
      );
    }
    const oldPresetsMap = /* @__PURE__ */ new Map();
    if (oldPresets !== void 0) {
      for (const preset of oldPresets) {
        if (preset.presetHandle !== null) {
          const presetHex = Bytes.toHex(preset.presetHandle);
          oldPresetsMap.set(presetHex, preset);
        }
      }
    }
    const persistedPresetsMap = /* @__PURE__ */ new Map();
    if (this.state.persistedPresets !== void 0) {
      for (const preset of this.state.persistedPresets) {
        if (preset.presetHandle === null) {
          throw new InternalError("Persisted preset is missing presetHandle, this should not happen");
        }
        const presetHex = Bytes.toHex(preset.presetHandle);
        persistedPresetsMap.set(presetHex, preset);
      }
    }
    const presetTypeMap = /* @__PURE__ */ new Map();
    for (const type of this.state.presetTypes) {
      presetTypeMap.set(type.presetScenario, type);
    }
    const presetScenarioNames = /* @__PURE__ */ new Map();
    const presetScenarioCounts = /* @__PURE__ */ new Map();
    const newPresetsSet = /* @__PURE__ */ new Set();
    const newBuildInPresets = /* @__PURE__ */ new Set();
    for (const preset of newPresets) {
      if (preset.presetHandle !== null) {
        const presetHex = Bytes.toHex(preset.presetHandle);
        if (newPresetsSet.has(presetHex)) {
          throw new StatusResponse.ConstraintErrorError(`Duplicate presetHandle ${presetHex} in new Presets`);
        }
        if (this.state.persistedPresets !== void 0) {
          const persistedPreset = persistedPresetsMap.get(presetHex);
          if (persistedPreset === void 0) {
            throw new StatusResponse.NotFoundError(
              `Preset with presetHandle ${presetHex} does not exist in old Presets, cannot add new Presets with non-null presetHandle`
            );
          }
          if (preset.builtIn !== null && persistedPreset.builtIn !== preset.builtIn) {
            throw new StatusResponse.ConstraintErrorError(
              `Cannot change built-in status of preset with presetHandle ${presetHex}`
            );
          }
        }
        newPresetsSet.add(presetHex);
      } else if (preset.builtIn) {
        throw new StatusResponse.ConstraintErrorError(`Can not add a new built-in preset`);
      }
      const presetType = presetTypeMap.get(preset.presetScenario);
      if (presetType === void 0) {
        throw new StatusResponse.ConstraintErrorError(
          `No PresetType defined for scenario ${Thermostat.PresetScenario[preset.presetScenario]}`
        );
      }
      if (preset.name !== void 0) {
        const scenarioNames = presetScenarioNames.get(preset.presetScenario) ?? [];
        if (scenarioNames.includes(preset.name)) {
          throw new StatusResponse.ConstraintErrorError(
            `Duplicate preset name "${preset.name}" for scenario ${Thermostat.PresetScenario[preset.presetScenario]}`
          );
        }
        if (!presetType.presetTypeFeatures.supportsNames) {
          throw new StatusResponse.ConstraintErrorError(
            `Preset names are not supported for scenario ${Thermostat.PresetScenario[preset.presetScenario]}`
          );
        }
        scenarioNames.push(preset.name);
        presetScenarioNames.set(preset.presetScenario, scenarioNames);
      }
      const count = presetScenarioCounts.get(preset.presetScenario) ?? 0;
      if (count === presetType.numberOfPresets) {
        throw new StatusResponse.ResourceExhaustedError(
          `Number of presets (${count}) for scenario ${Thermostat.PresetScenario[preset.presetScenario]} exceeds allowed number (${presetType.numberOfPresets})`
        );
      }
      presetScenarioCounts.set(preset.presetScenario, count + 1);
      if (this.features.cooling) {
        if (preset.coolingSetpoint === void 0) {
          throw new StatusResponse.ConstraintErrorError(
            `Preset for scenario ${Thermostat.PresetScenario[preset.presetScenario]} is missing required coolingSetpoint`
          );
        }
        if (preset.coolingSetpoint < this.coolSetpointMinimum || preset.coolingSetpoint > this.coolSetpointMaximum) {
          throw new StatusResponse.ConstraintErrorError(
            `Preset coolingSetpoint (${preset.coolingSetpoint}) for scenario ${Thermostat.PresetScenario[preset.presetScenario]} is out of bounds [${this.coolSetpointMinimum}, ${this.coolSetpointMaximum}]`
          );
        }
      }
      if (this.features.heating) {
        if (preset.heatingSetpoint === void 0) {
          throw new StatusResponse.ConstraintErrorError(
            `Preset for scenario ${Thermostat.PresetScenario[preset.presetScenario]} is missing required heatingSetpoint`
          );
        }
        if (preset.heatingSetpoint < this.heatSetpointMinimum || preset.heatingSetpoint > this.heatSetpointMaximum) {
          throw new StatusResponse.ConstraintErrorError(
            `Preset heatingSetpoint (${preset.heatingSetpoint}) for scenario ${Thermostat.PresetScenario[preset.presetScenario]} is out of bounds [${this.heatSetpointMinimum}, ${this.heatSetpointMaximum}]`
          );
        }
      }
      if (preset.builtIn && preset.presetHandle !== null) {
        newBuildInPresets.add(Bytes.toHex(preset.presetHandle));
      }
    }
  }
  /**
   * Handles additional validation of preset changes when all chunks were written in an atomic write operation.
   */
  #handlePresetsChanged(newPresets, oldPresets) {
    this.#handlePersistedPresetsChanged(newPresets, oldPresets);
    const oldPresetsMap = /* @__PURE__ */ new Map();
    const oldBuildInPresets = /* @__PURE__ */ new Set();
    if (oldPresets !== void 0) {
      for (const preset of oldPresets) {
        if (preset.presetHandle === null) {
          throw new InternalError("Old preset is missing presetHandle, this must not happen");
        }
        const presetHex = Bytes.toHex(preset.presetHandle);
        oldPresetsMap.set(presetHex, preset);
        if (preset.builtIn) {
          oldBuildInPresets.add(presetHex);
        }
      }
    }
    for (const preset of newPresets) {
      if (preset.presetHandle === null) {
        if (preset.builtIn) {
          throw new StatusResponse.ConstraintErrorError(
            `Preset for scenario ${Thermostat.PresetScenario[preset.presetScenario]} is built-in and must have a non-null presetHandle`
          );
        }
      }
    }
  }
  /**
   * Handles additional validation and required value adjustments of persistedPresets changes when all chunks were
   * written in an atomic write.
   */
  #handlePersistedPresetsChanged(newPresets, oldPresets) {
    if (oldPresets === void 0) {
      logger.debug(
        "Old presets is undefined, skipping some checks. This should only happen on setup of the behavior."
      );
    }
    const entropy = this.endpoint.env.get(Entropy);
    let changed = false;
    const newPresetHandles = /* @__PURE__ */ new Set();
    for (const preset of newPresets) {
      if (preset.presetHandle === null) {
        logger.error("Preset is missing presetHandle, generating a new one");
        preset.presetHandle = entropy.randomBytes(16);
        changed = true;
      }
      newPresetHandles.add(Bytes.toHex(preset.presetHandle));
      if (oldPresets === void 0) {
        if (preset.builtIn === null) {
          preset.builtIn = false;
          changed = true;
        }
      } else {
        if (preset.builtIn === null) {
          const oldPreset = oldPresets.find(
            (p) => p.presetHandle && preset.presetHandle && Bytes.areEqual(p.presetHandle, preset.presetHandle)
          );
          if (oldPreset !== void 0) {
            preset.builtIn = oldPreset.builtIn;
          } else {
            preset.builtIn = false;
          }
          changed = true;
        }
      }
    }
    const newBuildInPresets = /* @__PURE__ */ new Set();
    for (const preset of newPresets) {
      if (preset.builtIn) {
        newBuildInPresets.add(Bytes.toHex(preset.presetHandle));
      }
    }
    const oldBuildInPresets = /* @__PURE__ */ new Set();
    if (oldPresets !== void 0) {
      for (const preset of oldPresets) {
        if (preset.builtIn) {
          oldBuildInPresets.add(Bytes.toHex(preset.presetHandle));
        }
      }
    }
    for (const oldBuiltInPreset of oldBuildInPresets) {
      if (!newBuildInPresets.has(oldBuiltInPreset)) {
        throw new StatusResponse.ConstraintErrorError(
          `Cannot remove built-in preset with presetHandle ${oldBuiltInPreset}`
        );
      }
    }
    if (this.state.activePresetHandle !== null && !newPresetHandles.has(Bytes.toHex(this.state.activePresetHandle))) {
      throw new StatusResponse.InvalidInStateError(`ActivePresetHandle references non-existing presetHandle`);
    }
    if (changed) {
      logger.error("PresetHandles or BuiltIn flags were updated, updating persistedPresets");
      this.state.persistedPresets = deepCopy(newPresets);
    }
  }
  async [Symbol.asyncDispose]() {
    this.endpoint.env.close(AtomicWriteHandler);
  }
  /** Implementation of the atomic request handling */
  async atomicRequest(request) {
    const atomicWriteHandler = this.endpoint.env.get(AtomicWriteHandler);
    const { requestType } = request;
    switch (requestType) {
      case Thermostat.RequestType.BeginWrite:
        return atomicWriteHandler.beginWrite(request, this.context, this.endpoint, this.type);
      case Thermostat.RequestType.CommitWrite:
        return await atomicWriteHandler.commitWrite(
          request,
          this.context,
          this.endpoint,
          this.type,
          this.state
        );
      case Thermostat.RequestType.RollbackWrite:
        return atomicWriteHandler.rollbackWrite(request, this.context, this.endpoint, this.type);
    }
  }
}
((ThermostatBaseServer2) => {
  class State extends ThermostatBehaviorLogicBase.State {
    /**
     * Otherwise measured temperature in Matter format as uint16 with a factor of 100. A calibration offset is applied
     * additionally from localTemperatureCalibration if set.
     * Use this if you have an external temperature sensor that should be used for thermostat control instead of a
     * local temperature measurement cluster.
     */
    externalMeasuredIndoorTemperature;
    /**
     * Endpoint (Number or string-Id) where to find the indoor temperature measurement cluster to use as
     * local temperature measurement for the thermostat behavior.
     */
    localIndoorTemperatureMeasurementEndpoint;
    /**
     * Otherwise measured occupancy as boolean.
     * Use this if you have an external occupancy sensor that should be used for thermostat control instead of a
     * internal occupancy sensing cluster.
     */
    externallyMeasuredOccupancy;
    /**
     * Endpoint (Number or string-Id) where to find the occupancy sensing cluster to use as
     * local occupancy measurement for the thermostat behavior.
     */
    localOccupancyMeasurementEndpoint;
    /**
     * Use to enable the automatic mode management, implemented by this standard implementation.  This is beyond
     * Matter specification! It reacts to temperature changes to adjust system running mode automatically. It also
     * requires the Auto feature to be  enabled and the ThermostatRunningMode attribute to be present.
     */
    useAutomaticModeManagement = false;
    /**
     * Persisted presets stored in the device, needed because the original "presets" is a virtual property
     */
    persistedPresets;
    /**
     * Implementation of the needed Preset attribute logic for Atomic Write handling.
     */
    [Val.properties](endpoint, session) {
      const properties = {};
      if (endpoint.behaviors.optionsFor(ThermostatBaseServer2)?.presets !== void 0 || endpoint.behaviors.defaultsFor(ThermostatBaseServer2)?.presets !== void 0) {
        Object.defineProperty(properties, "presets", {
          /**
           * Getter will return a pending atomic write state when there is one, otherwise the stored value or
           * the default value.
           */
          get() {
            const pendingValue = endpoint.env.get(AtomicWriteHandler).pendingValueForAttributeAndPeer(
              session,
              endpoint,
              ThermostatBaseServer2,
              Thermostat.Complete.attributes.presets.id
            );
            if (pendingValue !== void 0) {
              return pendingValue;
            }
            let value = endpoint.stateOf(ThermostatBaseServer2.id).persistedPresets;
            if (value === void 0) {
              value = endpoint.behaviors.optionsFor(ThermostatBaseServer2)?.presets;
            }
            return value ?? [];
          },
          /**
           * Setter will either emit an update event directly when in local actor context or command context,
           * otherwise it will go through the AtomicWriteHandler to ensure proper atomic write handling.
           */
          set(value) {
            if (hasLocalActor(session) || "command" in session && session.command) {
              endpoint.eventsOf(ThermostatBaseServer2.id).updatePresets.emit(value);
            } else {
              endpoint.env.get(AtomicWriteHandler).writeAttribute(
                session,
                endpoint,
                ThermostatBaseServer2,
                Thermostat.Complete.attributes.presets.id,
                value
              );
            }
          }
        });
      }
      return properties;
    }
  }
  ThermostatBaseServer2.State = State;
  class Events extends ThermostatBehaviorLogicBase.Events {
    externalMeasuredIndoorTemperature$Changed = Observable();
    externallyMeasuredOccupancy$Changed = Observable();
    persistedPresets$Changed = Observable();
    persistedPresets$Changing = Observable();
    /**
     * Custom event emitted when the calibrated temperature changes.
     */
    calibratedTemperature$Changed = Observable();
    /**
     * Custom event emitted when the Presets attribute is "virtually" changing as part of an atomic write operation.
     * Info: The events is currently needed to be a pure Observable to get errors thrown in the event handler be
     *  reported back to the emitter.
     */
    presets$AtomicChanging = Observable();
    /**
     * Custom event emitted when the Presets attribute has "virtually" changed as part of an atomic write operation.
     * Info: The events is currently needed to be a pure Observable to get errors thrown in the event handler be
     * reported back to the emitter.
     */
    presets$AtomicChanged = Observable();
    /**
     * Custom event emitted to inform the behavior implementation of an update of the PersistedPresets attribute.
     */
    updatePresets = Observable();
  }
  ThermostatBaseServer2.Events = Events;
  class Internal {
    /**
     * Local temperature in Matter format as uint16 with a factor of 100. It is the same value as the one reported
     * in the localTemperature Attribute, but also present when the LocalTemperatureNotExposed feature is enabled.
     * Means all logic and calculations are always done with this value.
     * The value will be updated on initialization and when the localTemperature Attribute changes.
     */
    localTemperature = null;
    /**
     * Storing fixed value internally to ensure it can not be modified.
     * This value will be initialized when the behavior is initialized and is static afterward.
     */
    minSetpointDeadBand = 0;
    /**
     * Storing fixed value internally to ensure it can not be modified.
     * This value will be initialized when the behavior is initialized and is static afterward.
     */
    controlSequenceOfOperation;
  }
  ThermostatBaseServer2.Internal = Internal;
})(ThermostatBaseServer || (ThermostatBaseServer = {}));
class ThermostatServer extends ThermostatBaseServer.for(ClusterType(Thermostat.Base)) {
}
export {
  ThermostatBaseServer,
  ThermostatServer
};
//# sourceMappingURL=ThermostatServer.js.map
