/*
 * Copyright (c) 2010-2026 Contributors to the openHAB project
 *
 * See the NOTICE file(s) distributed with this work for additional
 * information.
 *
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Public License 2.0 which is available at
 * http://www.eclipse.org/legal/epl-2.0
 *
 * SPDX-License-Identifier: EPL-2.0
 */
package org.openhab.binding.radiothermostat.internal.handler;

import static org.openhab.binding.radiothermostat.internal.RadioThermostatBindingConstants.*;

import java.text.NumberFormat;
import java.text.ParseException;
import java.time.ZonedDateTime;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collection;
import java.util.List;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;

import javax.measure.quantity.Temperature;

import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jetty.client.HttpClient;
import org.openhab.binding.radiothermostat.internal.RadioThermostatConfiguration;
import org.openhab.binding.radiothermostat.internal.RadioThermostatStateDescriptionProvider;
import org.openhab.binding.radiothermostat.internal.RadioThermostatThingActions;
import org.openhab.binding.radiothermostat.internal.communication.RadioThermostatConnector;
import org.openhab.binding.radiothermostat.internal.communication.RadioThermostatEvent;
import org.openhab.binding.radiothermostat.internal.communication.RadioThermostatEventListener;
import org.openhab.binding.radiothermostat.internal.dto.RadioThermostatDTO;
import org.openhab.binding.radiothermostat.internal.dto.RadioThermostatHumidityDTO;
import org.openhab.binding.radiothermostat.internal.dto.RadioThermostatRuntimeDTO;
import org.openhab.binding.radiothermostat.internal.dto.RadioThermostatTstatDTO;
import org.openhab.binding.radiothermostat.internal.util.RadioThermostatSchedule;
import org.openhab.core.library.types.DateTimeType;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.PointType;
import org.openhab.core.library.types.QuantityType;
import org.openhab.core.library.types.StringType;
import org.openhab.core.library.unit.ImperialUnits;
import org.openhab.core.thing.Channel;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingStatusDetail;
import org.openhab.core.thing.binding.BaseThingHandler;
import org.openhab.core.thing.binding.ThingHandlerService;
import org.openhab.core.types.Command;
import org.openhab.core.types.RefreshType;
import org.openhab.core.types.State;
import org.openhab.core.types.StateOption;
import org.openhab.core.types.UnDefType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.gson.Gson;

/**
 * The {@link RadioThermostatHandler} is responsible for handling commands, which are
 * sent to one of the channels.
 *
 * Based on the 'airquality' binding by Kuba Wolanin
 *
 * @author Michael Lobstein - Initial contribution
 */
@NonNullByDefault
public class RadioThermostatHandler extends BaseThingHandler implements RadioThermostatEventListener {
    private static final int DEFAULT_REFRESH_PERIOD = 2;
    private static final int DEFAULT_LOG_REFRESH_PERIOD = 10;

    private final RadioThermostatStateDescriptionProvider stateDescriptionProvider;
    private final Logger logger = LoggerFactory.getLogger(RadioThermostatHandler.class);

    private final Gson gson;
    private final RadioThermostatConnector connector;
    private final RadioThermostatDTO rthermData = new RadioThermostatDTO();
    private @Nullable RadioThermostatSchedule thermostatSchedule;

    private @Nullable ScheduledFuture<?> refreshJob;
    private @Nullable ScheduledFuture<?> logRefreshJob;
    private @Nullable ScheduledFuture<?> clockSyncJob;

    private int refreshPeriod = DEFAULT_REFRESH_PERIOD;
    private int logRefreshPeriod = DEFAULT_LOG_REFRESH_PERIOD;
    private boolean isCT80 = false;
    private boolean disableLogs = false;
    private boolean clockSync = false;
    private boolean remoteTempDeadband = false;
    private String setpointCmdKeyPrefix = "t_";
    private String heatProgramJson = BLANK;
    private String coolProgramJson = BLANK;

    public RadioThermostatHandler(Thing thing, RadioThermostatStateDescriptionProvider stateDescriptionProvider,
            HttpClient httpClient) {
        super(thing);
        this.stateDescriptionProvider = stateDescriptionProvider;
        gson = new Gson();
        connector = new RadioThermostatConnector(httpClient);
    }

    @Override
    public void initialize() {
        logger.debug("Initializing RadioThermostat handler.");
        RadioThermostatConfiguration config = getConfigAs(RadioThermostatConfiguration.class);

        final String hostName = config.hostName;
        final Integer refresh = config.refresh;
        final Integer logRefresh = config.logRefresh;
        this.isCT80 = config.isCT80;
        this.disableLogs = config.disableLogs;
        this.clockSync = config.clockSync;
        this.remoteTempDeadband = config.remoteTempDeadband;

        if (hostName == null || hostName.isBlank()) {
            updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
                    "@text/offline.configuration-error-hostname");
            return;
        }

        if (refresh != null) {
            this.refreshPeriod = refresh;
        }

        if (logRefresh != null) {
            this.logRefreshPeriod = logRefresh;
        }

        connector.setThermostatHostName(hostName);
        connector.addEventListener(this);

        // The setpoint mode is controlled by the name of setpoint attribute sent to the thermostat.
        // Temporary mode uses setpoint names prefixed with "t_" while absolute mode uses "a_"
        if ("absolute".equals(config.setpointMode)) {
            this.setpointCmdKeyPrefix = "a_";
        }

        // populate fan mode options based on thermostat model
        stateDescriptionProvider.setStateOptions(new ChannelUID(getThing().getUID(), FAN_MODE), getFanModeOptions());

        // if we are not a CT-80, remove the humidity & program mode channel
        if (!this.isCT80) {
            List<Channel> channels = new ArrayList<>(this.getThing().getChannels());
            channels.removeIf(c -> (c.getUID().getId().equals(HUMIDITY)));
            channels.removeIf(c -> (c.getUID().getId().equals(PROGRAM_MODE)));
            updateThing(editThing().withChannels(channels).build());
        }

        final RadioThermostatSchedule localSchedule = thermostatSchedule = new RadioThermostatSchedule(config);

        try {
            heatProgramJson = localSchedule.getHeatProgramJson();
        } catch (IllegalStateException e) {
            logger.debug("Invalid HEATING program schedule: {}", e.getMessage());
            updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
                    "@text/offline.configuration-error-heating-program");
            return;
        }

        try {
            coolProgramJson = localSchedule.getCoolProgramJson();
        } catch (IllegalStateException e) {
            logger.debug("Invalid COOLING program schedule: {}", e.getMessage());
            updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
                    "@text/offline.configuration-error-cooling-program");
            return;
        }

        updateStatus(ThingStatus.UNKNOWN);

        startAutomaticRefresh();

        if (!this.disableLogs || this.isCT80) {
            startAutomaticLogRefresh();
        }

        if (this.clockSync) {
            scheduleClockSyncJob();
        }
    }

    @Override
    public Collection<Class<? extends ThingHandlerService>> getServices() {
        return List.of(RadioThermostatThingActions.class);
    }

    /**
     * Start the job to periodically update data from the thermostat
     */
    private void startAutomaticRefresh() {
        ScheduledFuture<?> refreshJob = this.refreshJob;
        if (refreshJob == null || refreshJob.isCancelled()) {
            Runnable runnable = () -> {
                // populate the heat and cool programs on the thermostat from the user configuration,
                // the commands will be sent each time the refresh job runs until a success response is seen
                if (!heatProgramJson.isEmpty()) {
                    final String response = connector.sendCommand(null, null, heatProgramJson, HEAT_PROGRAM_RESOURCE);
                    if (response.contains("success")) {
                        heatProgramJson = BLANK;
                    }
                }

                if (!coolProgramJson.isEmpty()) {
                    final String response = connector.sendCommand(null, null, coolProgramJson, COOL_PROGRAM_RESOURCE);
                    if (response.contains("success")) {
                        coolProgramJson = BLANK;
                    }
                }

                // send an async call to the thermostat to get the 'tstat' data
                connector.getAsyncThermostatData(DEFAULT_RESOURCE);
            };

            refreshJob = null;
            this.refreshJob = scheduler.scheduleWithFixedDelay(runnable, 0, refreshPeriod, TimeUnit.MINUTES);
        }
    }

    /**
     * Schedule the clock sync job
     */
    private void scheduleClockSyncJob() {
        ScheduledFuture<?> clockSyncJob = this.clockSyncJob;
        if (clockSyncJob == null || clockSyncJob.isCancelled()) {
            clockSyncJob = null;
            this.clockSyncJob = scheduler.schedule(this::syncThermostatClock, 1, TimeUnit.MINUTES);
        }
    }

    /**
     * Sync the thermostat's clock with the host system clock
     */
    private void syncThermostatClock() {
        boolean success = false;

        // don't sync clock if override is on because it will reset temporary hold
        final Integer override = rthermData.getThermostatData().getOverride();
        if (override == null || override.compareTo(0) == 0) {
            Calendar c = Calendar.getInstance();

            // The thermostat week starts as Monday = 0, subtract 2 since in standard DoW Monday = 2
            int thermDayOfWeek = c.get(Calendar.DAY_OF_WEEK) - 2;
            // Sunday will be -1, so add 7 to make it 6
            if (thermDayOfWeek < 0) {
                thermDayOfWeek += 7;
            }

            success = connector.sendCommand(null, null,
                    String.format(JSON_TIME, thermDayOfWeek, c.get(Calendar.HOUR_OF_DAY), c.get(Calendar.MINUTE)),
                    TIME_RESOURCE).contains("success");
        }

        // if sync call was successful run again in one hour, if un-successful try again in one minute
        this.clockSyncJob = scheduler.schedule(this::syncThermostatClock, (success ? 60 : 1), TimeUnit.MINUTES);
    }

    /**
     * Start the job to periodically update humidity and runtime date from the thermostat
     */
    private void startAutomaticLogRefresh() {
        ScheduledFuture<?> logRefreshJob = this.logRefreshJob;
        if (logRefreshJob == null || logRefreshJob.isCancelled()) {
            Runnable runnable = () -> {
                // Request humidity data from the thermostat if we are a CT80
                if (this.isCT80) {
                    // send an async call to the thermostat to get the humidity data
                    connector.getAsyncThermostatData(HUMIDITY_RESOURCE);
                }

                if (!this.disableLogs) {
                    // send an async call to the thermostat to get the runtime data
                    connector.getAsyncThermostatData(RUNTIME_RESOURCE);
                }
            };

            logRefreshJob = null;
            this.logRefreshJob = scheduler.scheduleWithFixedDelay(runnable, (!this.clockSync ? 1 : 2), logRefreshPeriod,
                    TimeUnit.MINUTES);
        }
    }

    @Override
    public void dispose() {
        logger.debug("Disposing the RadioThermostat handler.");
        connector.removeEventListener(this);

        // Disable Remote Temp and Message Area on shutdown
        if (ThingStatus.ONLINE.equals(this.getThing().getStatus())) {
            final boolean isRemoteTempLinked = isLinked(REMOTE_TEMP);
            final boolean isMessageLinked = isLinked(MESSAGE);

            scheduler.schedule(() -> {
                if (isRemoteTempLinked) {
                    connector.sendCommand("rem_mode", "0", REMOTE_TEMP_RESOURCE);
                }

                if (isMessageLinked) {
                    connector.sendCommand("mode", "0", PMA_RESOURCE);
                }
            }, 0, TimeUnit.SECONDS);
        }

        ScheduledFuture<?> refreshJob = this.refreshJob;
        if (refreshJob != null) {
            refreshJob.cancel(true);
            this.refreshJob = null;
        }

        ScheduledFuture<?> logRefreshJob = this.logRefreshJob;
        if (logRefreshJob != null) {
            logRefreshJob.cancel(true);
            this.logRefreshJob = null;
        }

        ScheduledFuture<?> clockSyncJob = this.clockSyncJob;
        if (clockSyncJob != null) {
            clockSyncJob.cancel(true);
            this.clockSyncJob = null;
        }
    }

    public void handleRawCommand(@Nullable String rawCommand) {
        connector.sendCommand(null, null, rawCommand, DEFAULT_RESOURCE);
    }

    public void handleRawCommand(@Nullable String rawCommand, String resource) {
        connector.sendCommand(null, null, rawCommand, resource);
    }

    @Override
    public void handleCommand(ChannelUID channelUID, Command command) {
        if (command instanceof RefreshType) {
            updateChannel(channelUID.getId(), rthermData);
        } else {
            Integer cmdInt = -1;
            final String cmdStr = command.toString();
            try {
                // parse out an Integer from the string
                // ie '70.5 F' becomes 70, also handles negative numbers
                cmdInt = NumberFormat.getInstance().parse(cmdStr).intValue();
            } catch (ParseException e) {
                logger.debug("Command: {} -> Not an integer", cmdStr);
            }

            switch (channelUID.getId()) {
                case MODE:
                    // only do if commanded mode is different than current mode
                    if (!cmdInt.equals(rthermData.getThermostatData().getMode())) {
                        connector.sendCommand("tmode", cmdStr, DEFAULT_RESOURCE);

                        // set the new operating mode, reset everything else,
                        // because refreshing the tstat data below is really slow.
                        rthermData.getThermostatData().setMode(cmdInt);
                        rthermData.getThermostatData().setHeatTarget(Double.valueOf(0));
                        rthermData.getThermostatData().setCoolTarget(Double.valueOf(0));
                        updateChannel(SET_POINT, rthermData);
                        rthermData.getThermostatData().setHold(0);
                        updateChannel(HOLD, rthermData);
                        updateChannel(NEXT_TEMP, rthermData);
                        updateChannel(NEXT_TIME, rthermData);
                        rthermData.getThermostatData().setProgramMode(-1);
                        updateChannel(PROGRAM_MODE, rthermData);

                        // now just trigger a refresh of the thermostat to get the new active setpoint
                        // this takes a while for the JSON request to complete (async).
                        connector.getAsyncThermostatData(DEFAULT_RESOURCE);
                    }
                    break;
                case FAN_MODE:
                    rthermData.getThermostatData().setFanMode(cmdInt);
                    connector.sendCommand("fmode", cmdStr, DEFAULT_RESOURCE);
                    break;
                case PROGRAM_MODE:
                    rthermData.getThermostatData().setProgramMode(cmdInt);
                    connector.sendCommand("program_mode", cmdStr, DEFAULT_RESOURCE);
                    break;
                case HOLD:
                    if (command instanceof OnOffType && command == OnOffType.ON) {
                        rthermData.getThermostatData().setHold(1);
                        connector.sendCommand("hold", "1", DEFAULT_RESOURCE);
                    } else if (command instanceof OnOffType && command == OnOffType.OFF) {
                        rthermData.getThermostatData().setHold(0);
                        connector.sendCommand("hold", "0", DEFAULT_RESOURCE);
                    }
                    updateChannel(NEXT_TEMP, rthermData);
                    updateChannel(NEXT_TIME, rthermData);
                    break;
                case SET_POINT:
                    final String cmdKey;
                    if (rthermData.getThermostatData().getMode() == 1) {
                        cmdKey = this.setpointCmdKeyPrefix + "heat";
                        rthermData.getThermostatData().setHeatTarget(Double.valueOf(cmdInt));
                        rthermData.getThermostatData().setOverride(1);
                    } else if (rthermData.getThermostatData().getMode() == 2) {
                        cmdKey = this.setpointCmdKeyPrefix + "cool";
                        rthermData.getThermostatData().setCoolTarget(Double.valueOf(cmdInt));
                        rthermData.getThermostatData().setOverride(1);
                    } else {
                        // don't do anything if we are not in heat or cool mode
                        break;
                    }
                    connector.sendCommand(cmdKey, cmdInt.toString(), DEFAULT_RESOURCE);
                    break;
                case REMOTE_TEMP:
                    if (cmdInt != -1) {
                        final QuantityType<Temperature> remoteTemp = ((QuantityType<Temperature>) command)
                                .toUnit(ImperialUnits.FAHRENHEIT);
                        if (remoteTemp != null) {
                            connector.sendCommand("rem_temp", handleRemoteTempDeadband(remoteTemp),
                                    REMOTE_TEMP_RESOURCE);
                        }
                    } else {
                        connector.sendCommand("rem_mode", "0", REMOTE_TEMP_RESOURCE);
                    }
                    break;
                case MESSAGE:
                    if (!cmdStr.isEmpty()) {
                        connector.sendCommand(null, null, String.format(JSON_PMA, cmdStr), PMA_RESOURCE);
                    } else {
                        connector.sendCommand("mode", "0", PMA_RESOURCE);
                    }
                    break;
                default:
                    logger.warn("Unsupported command: {}", command.toString());
            }
        }
    }

    /**
     * Handle a RadioThermostat event received from the listeners
     *
     * @param event the event received from the listeners
     */
    @Override
    public void onNewMessageEvent(RadioThermostatEvent event) {
        logger.debug("onNewMessageEvent: key {} = {}", event.getKey(), event.getValue());

        final String evtKey = event.getKey();
        final String evtVal = event.getValue();

        if (KEY_ERROR.equals(evtKey)) {
            updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR,
                    "@text/offline.communication-error-get-data");
        } else {
            updateStatus(ThingStatus.ONLINE, ThingStatusDetail.NONE);

            // Map the JSON response to the correct object and update appropriate channels
            switch (evtKey) {
                case DEFAULT_RESOURCE:
                    rthermData.setThermostatData(gson.fromJson(evtVal, RadioThermostatTstatDTO.class));
                    // if thermostat returned -1 for temperature, skip this update
                    if (rthermData.getThermostatData().getTemperature() >= 0) {
                        updateAllChannels();
                    }
                    break;
                case HUMIDITY_RESOURCE:
                    final RadioThermostatHumidityDTO dto = gson.fromJson(evtVal, RadioThermostatHumidityDTO.class);
                    // if thermostat returned -1 for humidity, skip this update
                    if (dto != null && dto.getHumidity() >= 0) {
                        rthermData.setHumidity(dto.getHumidity());
                        updateChannel(HUMIDITY, rthermData);
                    }
                    break;
                case RUNTIME_RESOURCE:
                    rthermData.setRuntime(gson.fromJson(evtVal, RadioThermostatRuntimeDTO.class));
                    updateChannel(TODAY_HEAT_RUNTIME, rthermData);
                    updateChannel(TODAY_COOL_RUNTIME, rthermData);
                    updateChannel(YESTERDAY_HEAT_RUNTIME, rthermData);
                    updateChannel(YESTERDAY_COOL_RUNTIME, rthermData);
                    break;
            }
        }
    }

    /**
     * Update the channel from the last Thermostat data retrieved
     *
     * @param channelId the id identifying the channel to be updated
     */
    private void updateChannel(String channelId, RadioThermostatDTO rthermData) {
        if (isLinked(channelId)) {
            final Object value;
            try {
                value = getValue(channelId, rthermData, thermostatSchedule);
            } catch (Exception e) {
                logger.debug("Error setting {} value", channelId.toUpperCase());
                return;
            }

            State state = null;
            if (value == null) {
                state = UnDefType.UNDEF;
            } else if (value instanceof PointType pointCommand) {
                state = pointCommand;
            } else if (value instanceof ZonedDateTime zonedDateTimeCommand) {
                state = new DateTimeType(zonedDateTimeCommand);
            } else if (value instanceof QuantityType<?> quantityCommand) {
                state = quantityCommand;
            } else if (value instanceof Number numberCommand) {
                state = new DecimalType(numberCommand);
            } else if (value instanceof String) {
                state = new StringType(value.toString());
            } else if (value instanceof OnOffType onOffCommand) {
                state = onOffCommand;
            } else {
                logger.warn("Update channel {}: Unsupported value type {}", channelId,
                        value.getClass().getSimpleName());
            }
            logger.debug("Update channel {} with state {} ({})", channelId, (state == null) ? "null" : state.toString(),
                    (value == null) ? "null" : value.getClass().getSimpleName());

            // Update the channel
            if (state != null) {
                updateState(channelId, state);
            }
        }
    }

    /**
     * Update a given channelId from the thermostat data
     *
     * @param channelId the channel id to be updated
     * @param data the RadioThermostat dto
     * @return the value to be set in the state
     */
    public static @Nullable Object getValue(String channelId, RadioThermostatDTO data,
            @Nullable RadioThermostatSchedule thermostatSchedule) {
        switch (channelId) {
            case TEMPERATURE:
                if (data.getThermostatData().getTemperature() != null) {
                    return new QuantityType<>(data.getThermostatData().getTemperature(), API_TEMPERATURE_UNIT);
                } else {
                    return null;
                }
            case HUMIDITY:
                if (data.getHumidity() != null) {
                    return new QuantityType<>(data.getHumidity(), API_HUMIDITY_UNIT);
                } else {
                    return null;
                }
            case MODE:
                return data.getThermostatData().getMode();
            case FAN_MODE:
                return data.getThermostatData().getFanMode();
            case PROGRAM_MODE:
                return data.getThermostatData().getProgramMode();
            case SET_POINT:
                if (data.getThermostatData().getSetpoint() != 0) {
                    return new QuantityType<>(data.getThermostatData().getSetpoint(), API_TEMPERATURE_UNIT);
                } else {
                    return null;
                }
            case OVERRIDE:
                return data.getThermostatData().getOverride();
            case HOLD:
                return OnOffType.from(data.getThermostatData().getHold() == 1);
            case STATUS:
                return data.getThermostatData().getStatus();
            case FAN_STATUS:
                // workaround for some thermostats that don't report that the fan is on during heating or cooling
                if (data.getThermostatData().getStatus() > 0) {
                    return 1;
                } else {
                    return data.getThermostatData().getFanStatus();
                }
            case DAY:
                return data.getThermostatData().getTime().getDayOfWeek();
            case HOUR:
                return data.getThermostatData().getTime().getHour();
            case MINUTE:
                return data.getThermostatData().getTime().getMinute();
            case DATE_STAMP:
                return data.getThermostatData().getTime().getThemostatDateTime();
            case TODAY_HEAT_RUNTIME:
                return new QuantityType<>(data.getRuntime().getToday().getHeatTime().getRuntime(), API_MINUTES_UNIT);
            case TODAY_COOL_RUNTIME:
                return new QuantityType<>(data.getRuntime().getToday().getCoolTime().getRuntime(), API_MINUTES_UNIT);
            case YESTERDAY_HEAT_RUNTIME:
                return new QuantityType<>(data.getRuntime().getYesterday().getHeatTime().getRuntime(),
                        API_MINUTES_UNIT);
            case YESTERDAY_COOL_RUNTIME:
                return new QuantityType<>(data.getRuntime().getYesterday().getCoolTime().getRuntime(),
                        API_MINUTES_UNIT);
            case NEXT_TEMP:
                if (thermostatSchedule != null) {
                    final Integer nextTemp = thermostatSchedule.getNextTemp(data.getThermostatData());
                    if (nextTemp != null) {
                        return new QuantityType<>(nextTemp, API_TEMPERATURE_UNIT);
                    }
                }
                return null;
            case NEXT_TIME:
                if (thermostatSchedule != null) {
                    final ZonedDateTime nextTime = thermostatSchedule.getNextTime(data.getThermostatData());
                    if (nextTime != null) {
                        return nextTime;
                    }
                }
                return null;
        }
        return null;
    }

    /**
     * Updates all channels from rthermData
     */
    private void updateAllChannels() {
        // Update all channels from rthermData
        getThing().getChannels().forEach(channel -> {
            if (!NO_UPDATE_CHANNEL_IDS.contains(channel.getUID().getId())) {
                updateChannel(channel.getUID().getId(), rthermData);
            }
        });
    }

    /**
     * Build a list of fan modes based on what model thermostat is used
     *
     * @return list of state options for thermostat fan modes
     */
    private List<StateOption> getFanModeOptions() {
        final List<StateOption> fanModeOptions = new ArrayList<>(this.isCT80 ? 3 : 2);

        fanModeOptions.add(new StateOption("0", "@text/options.fan-option-auto"));
        if (this.isCT80) {
            fanModeOptions.add(new StateOption("1", "@text/options.fan-option-circulate"));
        }
        fanModeOptions.add(new StateOption("2", "@text/options.fan-option-on"));

        return fanModeOptions;
    }

    private String handleRemoteTempDeadband(QuantityType<Temperature> remoteTemp) {
        if (!this.remoteTempDeadband) {
            return String.valueOf(remoteTemp.intValue());
        }

        final int remoteTempRounded;
        final boolean isHeatModeOff = Integer.valueOf(1).equals(rthermData.getThermostatData().getMode())
                && Integer.valueOf(0).equals(rthermData.getThermostatData().getStatus());
        final boolean isCoolModeOff = Integer.valueOf(2).equals(rthermData.getThermostatData().getMode())
                && Integer.valueOf(0).equals(rthermData.getThermostatData().getStatus());

        // hvac heating on OR hvac off in cooling mode, round temperature down, e.g. 69.5 = 69
        if (Integer.valueOf(1).equals(rthermData.getThermostatData().getStatus()) || isCoolModeOff) {
            remoteTempRounded = (int) Math.floor(remoteTemp.doubleValue());
            logger.debug("remote temp: {}, rounding down to {}", remoteTemp, remoteTempRounded);
        } else if (Integer.valueOf(2).equals(rthermData.getThermostatData().getStatus()) || isHeatModeOff) {
            // hvac cooling on OR hvac off in heat mode, round temperature up, e.g. 69.1 = 70
            remoteTempRounded = (int) Math.ceil(remoteTemp.doubleValue());
            logger.debug("remote temp: {}, rounding up to {}", remoteTemp, remoteTempRounded);
        } else {
            // hvac mode off (or unknown), round temperature, e.g. 69.5 = 70
            remoteTempRounded = (int) Math.round(remoteTemp.doubleValue());
            logger.debug("remote temp: {}, rounding to {}", remoteTemp, remoteTempRounded);
        }
        return String.valueOf(remoteTempRounded);
    }
}
