#!/usr/bin/env python3
# ***************************************************************************
#  IIIIII NNN  NNN  Copyright (C) 2026 Innovative Networks, Inc.
#    II    NNN  N   All Rights Reserved. Any redistribution or reproduction
#    II    N NN N   of part or all of the content of this program in any form
#    II    N  NNN   without expressed written consent of the copyright holder
#  IIIIII NNN  NNN  is strictly prohibited.  Please contact 
#                   admins@indigex.com for additional information.
# ***************************************************************************
#  check_snmp_minuteman_ups.py
#  Authors - Carson Rochon <crochon@indigex.com>, Joshua Lantz <jlantz@indigex.com> 
#
#  Purpose:  Checks various things on an Minumteman UPS using RFC1628 (UPS-MIB) and XPPC-MIB
#
#  Version History:
#       2026.03.03 - Initial Creation
#       2026.03.13 - Improved script output for readibility. Changed default temperature scale to F. Added threshold checks for battery temperature.
# ***************************************************************************
# pylint: disable=broad-exception-caught
# pylint: disable=bare-except
# pylint: disable=invalid-name
# pylint: disable=line-too-long
# ruff: noqa: E722

try:
    from inmon_utils import *
except:
    import sys
    print("Failed to import inmon_utils")
    sys.exit(3)
import argparse
import json
from datetime import datetime, date, timedelta
import logging
import pprint
import easysnmp # pylint: disable=import-error

parser = argparse.ArgumentParser(description="Checks health on Minuteman UPS devices.")

parser.add_argument(
    "-H", "--hostname",
    dest="hostname",
    help="Specify device hostname",
    required=True
)
parser.add_argument(
    "-p", "--parameters",
    dest="parameters",
    help="Specify parameters for warning, critical, etc in JSON format",
    default='{}'
)
parser.add_argument(
    "-c", "--community",
    dest="community",
    help="Specify SNMP community",
    default="public"
)
parser.add_argument(
    "-v", "--version",
    dest="version",
    help="Specify SNMP version",
    default="1"
)
parser.add_argument(
    "-t", "--test",
    dest="test",
    help="Specify test to run",
    choices=(['all', 'battery', 'input', 'output', 'ambient', 'health']),
    nargs='*',
    default="all"
)
parser.add_argument(
    "-o", "--overrides",
    dest="overrides",
    help="Specify overrides for sensor temperatures in the format specified by check_snmp_apcups.py -O/--override-help",
    nargs="*"
)
parser.add_argument(
    "-O", "--override-help",
    dest="override_help",
    help="Print help output for -o/--override",
    action="store_true"
)
parser.add_argument(
    "-d", "--debug",
    dest="debug",
    help="Enable debugging output",
    action="store_true"
)

args = parser.parse_args()

if args.override_help:
    print("""
        Overrides are specified in the format of JSON strings in the following format:
        {
            "temp": {
                    "max": <int>,
                    "min": <int>,
                    "output_uom": <F or C>
                    "input_uom": <F or C>
            },
            "humidity: {
                "max": <int>,
                "min": <int>,
            }
        }
    

        
          """)
    sys.exit(3)

# Try to load JSON parameters. If we fail, exit with UNKNOWN
# If the test argument is called and parameters is not defined or empty, then we call parameters as this, '{}'.

try:
    params = json.loads(args.parameters)
except Exception as e:
    print(f"[UNKNOWN] - 2 JSON is invalid: {e}")
    sys.exit(3)



#params = json.loads(args.parameters)

logger = logging.getLogger(__name__)

if args.debug:
    log_level = "DEBUG"
else:
    log_level = "WARN"

log_level = getattr(logging, log_level.upper())
logging.basicConfig(level=log_level)

logger.debug(pprint.pformat(params))

overrides = {}

if args.overrides:
    try:
        for ob in args.overrides:
            overrides.update(json.loads(ob))
    except Exception as e:
        print(f"[UNKNOWN] - Override JSON improper: {e}")
        sys.exit(3)

# Latin-1 character for the degree symbol
degree_symbol = chr(176)

# OID Bases
base            = ".1.3.6.1.2.1.33"  #RFC1628
xppc_base       = ".1.3.6.1.4.1.935" #XPPC-MIB

# Create default values used
return_status = 0
return_text   = ""
return_data   = ""

return_codes = ['[OK]', '[WARNING]', '[CRITICAL]', '[UNKNOWN]']
critical_checks = []
warning_checks = []
ok_checks = []
unknown_checks = []
append_text = []

def ctof(temp):
    """
    Convert temperature from Celsius to Fahrenheit.


    Args:
        temp (float): Temperature in degrees Celsius.

    Returns:
        float: Temperature converted to degrees Fahrenheit.
    """
    value = (temp * 1.8) + 32
    return round(value, 2)

def ftoc(temp):
    """
    Convert temperature from Fahrenheit to Celsius.



    Args:
        temp (float): Temperature in degrees Fahrenheit.

    Returns:
        float: Temperature converted to degrees Celsius.
    """
    value = (temp - 32) * (5/9)
    return round(value, 2)


def check_battery(session): # pylint: disable=redefined-outer-name
    """
    Check battery health on RFC-1628 standard UPS.

    Args:
        session (easysnmp.Session): The EasySNMP session object.


    Returns:
        None
    """

 
    global return_status # pylint: disable=global-statement
    global return_data # pylint: disable=global-statement

    def print_help():
        print('''Parameters must contain a battery dictionary in the format:
              params: {
                  "battery": {
                      "capacity":{
                            "crit": val,
                            "warn": val,
                      },
                      "runtime": {
                          "crit": val,
                          "warn": val
                      },
                      "temp": {
                          "max": val,
                          "min": val,
                          "high": val,
                          "low": val,
                          "uom": <F or C>
                      },
                      "lifespan": val
                  }
              }
              ''')

    if "battery" not in params:
        print_help()
        sys.exit(3)

    oid_battery_capacity        = f"{base}.1.2.4.0" 
    oid_battery_temperature     = f"{base}.1.2.7.0" 
    oid_battery_runtimeremain   = f"{base}.1.2.3.0" 
    oid_battery_replacement     = f"{xppc_base}.1.1.1.2.2.5.0" 
    oid_battery_installdate     = f"{xppc_base}.1.1.1.2.1.3.0"
    oid_xppc_env                = f"{xppc_base}.1.1.1.9"

    # Get battery capacity in whole percentage.
    capacity = int(session.get(oid_battery_capacity).value)
    logger.debug("Battery capacity: %s", capacity)
    if capacity <= params["battery"]["capacity"]["crit"]:
        critical_checks.append(f"Battery charge below critical threshold at {capacity}% ")
        return_status = max(return_status, 2)
    elif params["battery"]["capacity"]["crit"] <= capacity <= params["battery"]["capacity"]["warn"]:
        warning_checks.append(f"Battery charge below warning threshold at {capacity}% ")
        return_status = max(return_status, 1)
    elif params["battery"]["capacity"]["warn"] < capacity:
        ok_checks.append(f"Battery charge OK - {capacity}% ")
        # No return status change
    else:
        unknown_checks.append("Unknown value encountered in capacity check ") 
        return_status = max(return_status, 3)
    return_data += f'battery_capacity={capacity}%;{params["battery"]["capacity"]["warn"]};'
    return_data += f'{params["battery"]["capacity"]["crit"]} '

    # Get temperature in INT
    temp = int(session.get(oid_battery_temperature).value)

    temp_max = int(session.get(f"{oid_xppc_env}.2.1.0").value) / 10
    temp_min = int(session.get(f"{oid_xppc_env}.2.2.0").value) / 10

    # Convert to fahrenheit if needed/wanted.
    if "temp" in params and params["temp"] == "C":
        
        uom = "C"
    else:
        temp = (temp * 1.8) + 32
        uom = "F"

    battery_temp_param = params.get("battery", {}).get("temp", {})
    uom = battery_temp_param.get("uom", "F")

    # Handle Max/High Thresholds
    if "max" in battery_temp_param:
        temp_max = battery_temp_param["max"]
        logger.debug("Overriding max temperature threshold %s", temp_max)
    else:
        temp_max = ctof(temp_max)

    if "min" in battery_temp_param:
        temp_min = battery_temp_param["min"]
        logger.debug("Overriding high temperature threshold %s", temp_min)
    else:
        temp_min = ctof(temp_min)
        
    # The warning thresholds can be set via parameter only since there is no warning OID   
    # Run battery temperature checks
    if "low" in battery_temp_param and "high" in battery_temp_param:
        temp_low = battery_temp_param["low"]
        temp_high = battery_temp_param["high"]
        logger.debug("Setting low temperature threshold via parameter %s", temp_low)
        logger.debug("Setting high temperature threshold via parameter %s", temp_high)

        if temp >= temp_max or temp <= temp_min:
            critical_checks.append(f"Battery temperature of {temp}{uom} outside critical threshold of {temp_min}{uom} to {temp_max}{uom} ") 
            return_status = max(return_status, 2)
        elif temp >= temp_high or temp <= temp_low:
            warning_checks.append(f"Battery temperature of {temp}{uom} outside warning threshold of {temp_low}{uom} to {temp_high}{uom} ")
            return_status = max(return_status, 1)
        elif temp_low < temp < temp_high:
            ok_checks.append(f"Battery temperature of {temp}{uom} within normal range ")
        else:
            unknown_checks.append(f"Unknown value encountered checking the battery temperature ")
            return_status = max(return_status, 3)
        return_data += f'battery_temperature={temp}{uom};{temp_high};{temp_max} '
    else:
        if temp >= temp_max or temp <= temp_min:
            critical_checks.append(f"Battery temperature of {temp}{uom} outside critical threshold of {temp_min}{uom} to {temp_max}{uom} ") 
            return_status = max(return_status, 2)
        elif temp_min < temp < temp_max:
            ok_checks.append(f"Battery temperature of {temp}{uom} within normal range ")
        else:
            unknown_checks.append(f"Unknown value encountered checking the battery temperature ")
            return_status = max(return_status, 3)
        return_data += f'battery_temperature={temp}{uom};;{temp_max} '




    logger.debug("Battery temperature: %s %s", temp, uom)

    return_data += f"battery_temperature={temp}{uom} "

    runtime = int(session.get(oid_battery_runtimeremain).value) * 60
    logger.debug("Raw runtime: %s", runtime)
    logger.debug("Runtime in seconds: %s", runtime)

    if runtime <= float(params["battery"]["runtime"]["crit"]):
        critical_checks.append(f"Battery runtime below critical threshold at {str(timedelta(seconds=runtime))} ")
        return_status = max(return_status, 2)
    elif float(params["battery"]["runtime"]["crit"]) <= runtime <= float(params["battery"]["runtime"]["warn"]):
        warning_checks.append(f"Battery runtime below warning threshold at {str(timedelta(seconds=runtime))} ")
        return_status = max(return_status, 1)
    elif float(params["battery"]["runtime"]["warn"]) < runtime:
        ok_checks.append(f"Battery runtime OK - {str(timedelta(seconds=runtime))} ")
        # No return status change
    else:
        unknown_checks.append("Unknown value encountered in battery runtime check ")
        return_status = max(return_status, 3)
    return_data += f"'battery_runtime={runtime}s;{params["battery"]["runtime"]["warn"]};"
    return_data += f"{params["battery"]["runtime"]["crit"]} "

    # Get replacement status and the current date
    replace_status = int(session.get(oid_battery_replacement).value)
    logger.debug("Replacement status (int): %s", replace_status)
    current_date = date.today()
    current_date = datetime(current_date.year, current_date.month, current_date.day)

  

    replace_date_calc = session.get(oid_battery_installdate).value
    logger.debug("Install date collected from SNMP: %s", replace_date_calc)

    # Handle differing date formt
    replace_date_calc_data = -1
    for date_format in ('%m/%d/%Y', '%d/%m/%Y', '%m/%d/%y', '%d/%m/%y'):
        try:
            replace_date_calc_data = datetime.strptime(replace_date_calc, date_format)
            logger.debug("Parsed date with format %s", date_format)
            final_format = date_format
            break
        except ValueError:
            pass
    if replace_date_calc_data == -1:
        print(f"Unable to parse date {replace_date_calc} for replacement date information")
        sys.exit(3)

    # We take the last replacement date + 5 years.
    # If the date is given as a parameter, we use that regardless of what the calculated date
    if "lifespan" in params["battery"] and params["battery"]["lifespan"] != 0:
        replace_date_calc_data += timedelta(days=params["battery"]["lifespan"])
        replace_date_data = replace_date_calc_data
        logger.debug("Lifespan parameters found. Overriding calculated date with: %s", replace_date_data)
    else:
        replace_date_calc_data += timedelta(days=1826)
        logger.debug("Calculated replacement date: %s", replace_date_calc_data)
        replace_date_data = replace_date_calc_data

    replace_date = replace_date_calc_data.strftime(final_format)


    logger.debug("Replacement date data selected for evaluation: %s", replace_date_calc_data)
    if replace_status == 2:
        critical_checks.append(f"Battery needs replaced ")
        return_status = max(return_status, 2)
    if current_date > replace_date_calc_data:
        critical_checks.append(f"Battery is past replacement date {replace_date} ")
        return_status = max(return_status, 2)
    elif current_date > (replace_date_calc_data - timedelta(days=14)):
        critical_checks.append(f"Battery replacement date within next two weeks {replace_date} ")

        return_status = max(return_status, 2)
    elif current_date > (replace_date_calc_data - timedelta(days=30)):
        warning_checks.append(f"Battery replacement date within next 30 days {replace_date} ")
        return_status = max(return_status, 1)
    elif current_date <= (replace_date_calc_data - timedelta(days=30)):
        ok_checks.append(f"Battery replacement date more than 30 days in future {replace_date} ")
    else:
        unknown_checks.append("Unknown value encountered in battery replacement check ")
        return_status = max(return_status, 3)

def check_input(session): # pylint: disable=redefined-outer-name
    '''
    Checks the input voltage and frequency of the UPS, and the reason for the last transfer.

    Parameters:
    params (dict): A dictionary containing the following parameters:
        input (dict): A dictionary containing the following parameters:
            nominal (int): The nominal input voltage of the UPS.

    Returns:
    None

    '''

    global return_status # pylint: disable=global-statement
    global return_data # pylint: disable=global-statement

    def print_help():
        print('''Parameter dictionary must be provided in the following format:
            params: {
                "input": {
                    "nominal": 120
                }
            }
            ''')

    oid_ups_input                   = f"{base}.1.3.3.1.3.1"
    oid_ups_input_nominal           = f"{base}.1.9.1.0"       
    oid_ups_input_hertz             = f"{base}.1.3.3.1.2.1"             
    xppc_ups_smart_input            = f"{xppc_base}.1.1.1.3.2"

    # Check for nominal voltage, if it's not available, access through parameters.
    try:
        nominal_voltage = int(session.get(oid_ups_input_nominal).value)
        logger.debug("Obtained nominal input voltage from UPS %s", nominal_voltage)
    except easysnmp.exceptions.EasySNMPNoSuchNameError:
        if "input" not in params or "nominal" not in params["input"]:
            print_help()
            sys.exit(3)
        else:
            nominal_voltage = params["input"]["nominal"]
            logger.debug("Obtained nominal input voltage from parameters: %s", nominal_voltage)

    # These values are for accepable multiplier ranges. See documentation on utility power.
    try:
        mch = overrides["input"]["voltage"]["crit"]["high"]
    except:
        mch = 1.058
    try:
        mwh = overrides["input"]["voltage"]["warn"]["high"]
    except:
        mwh = 1.050
    try:
        mwl = overrides["input"]["voltage"]["warn"]["low"]
    except:
        mwl = 0.950
    try:
        mcl = overrides["input"]["voltage"]["crit"]["low"]
    except:
        mcl = 0.917

    # Initialize variables
    voltage = []
    frequency = []
    last_transfer = (int(session.get(f"{xppc_ups_smart_input}.5.0").value)) 

    # None of the minuteman UPS's we manage seem to support an OID for a phase table.
    # Don't have any UPSs running on three phase to test gathering data here for.
    voltage.append(int(session.get(f"{oid_ups_input}").value))
    frequency.append(int(session.get(f"{oid_ups_input_hertz}").value)/10)

    logger.debug("Voltage table: %s", pprint.pformat(voltage))
    logger.debug("Frequency table: %s", pprint.pformat(frequency))


    for i, volt in enumerate(voltage):
        if volt <= (nominal_voltage * mcl) or volt >= (nominal_voltage * mch):
            critical_checks.append(f"Input voltage on phase {i+1} outside critical threshold - {volt}V ") 
            return_status = max(return_status, 2)
        elif volt <= (nominal_voltage * mwl) or volt >= (nominal_voltage * mwh):
            warning_checks.append(f"Input voltage on phase {i+1} outside warning threshold - {volt}V ") 
            return_status = max(return_status, 1)
        elif volt > (nominal_voltage * mcl) and volt < (nominal_voltage * mch):
            ok_checks.append(f"Input voltage on phase {i+1} OK - {volt}V ")
        else:
            unknown_checks.append(f"Unknown value occurred checking input voltage on phase {i+1} ")
            return_status = max(return_status, 3)

        # Acceptable line frequency is 58.5 to 60.5 https://www.openintl.com/california-rule-21-interconnection/
        if frequency[i] < 58.5 or frequency[i] > 60.5:
            critical_checks.append(f"Input frequency on phase {i+1} outside critical threshold - {frequency[i]}Hz")
            return_status = max(return_status, 2)
        elif 58.5 <= frequency[i] <= 60.5:
            ok_checks.append(f"Input frequency on phase {i+1} OK - {frequency[i]}Hz ") 
        else:
            unknown_checks.append(f"Unknown value occurred checking input frequency on phase {i+1} ") 
            return_status = max(return_status, 3)

        return_data += f"phase{i+1}_voltage={volt}V;{round((nominal_voltage * mwh), 2)};"
        return_data += f"{round((nominal_voltage * mch), 2)} phase{i+1}_frequency={frequency[i]}Hz;;60.5 "

    # Reasons for last transfer.
    if last_transfer == 1:
        append_text.append("No line transfer yet ")
    elif last_transfer == 2:
        append_text.append("Last transfer due to high line voltage ")
    elif last_transfer == 3:
        append_text.append("Last transfer due to brownout ")
    elif last_transfer == 4:
        append_text.append("Last transfer due to blackout ")
    elif last_transfer == 5:
        append_text.append("Last transfer due to small momentary sag ")
    elif last_transfer == 6:
        append_text.append("Last transfer due to deep momentary sag ")
    elif last_transfer == 7:
        append_text.append("Last transfer due to small momentary spike ")
    elif last_transfer == 8:
        append_text.append("Last transfer due to large momentary spike ")
    else:
        append_text.append("Unknown value encountered in transfer check;")
        return_status = max(return_status, 3)


def check_output(session): # pylint: disable=redefined-outer-name
    """
    Check the output of the UPS using RFC1628.

    Parameters must contain an output dictionary in the format:

        params: {
            "output": {
                "nominal": 120,
                "load": {
                    "crit": 90,
                    "warn": 80
                }
            }
        }

    The check will iterate over the number of phases reported by the UPS and check that the voltage and frequency on each
    phase are within the acceptable range. The acceptable range is based on the nominal voltage, with the following
    multipliers:

        - critical high: 1.058
        - warning high: 1.050
        - warning low: 0.950
        - critical low: 0.917

    The check will also check the output load, which is reported as a percentage of the maximum possible output current. The
    acceptable range for the output load is based on the load dictionary in the parameters.

    Data is returned in the format:

        data: {
            "phase1_voltage": 120V;100;110. phase1_frequency=60Hz;58.5;60.5. phase2_voltage=120V;100;110. phase2_frequency=60Hz;58.5;60.5. ...
        }
    """

    global return_status # pylint: disable=global-statement
    global return_data # pylint: disable=global-statement
    
    def print_help():
        print('''Parameters must contain an output dictionary in the format:
            params: {
                "output": {
                    "nominal": 120,
                    "load": {
                        "crit": 90,
                        "warn": 80
                    }
                }
            }
            ''')

    oid_ups_output              = f"{base}.1.4" 
    oid_ups_output_nominal      = f"{base}.1.9.3.0"      
    if "output" not in params:
        print_help()
        sys.exit(3)

    # Check for nominal voltage, if it's not available, access through parameters.

    try:
        nominal_voltage = int(session.get(oid_ups_output_nominal).value)
        logger.debug("Obtained nominal output voltage from UPS %s", nominal_voltage)
    except easysnmp.exceptions.EasySNMPNoSuchNameError:
        if "output" not in params or "nominal" not in params["output"]:
            print_help()
            sys.exit(3)
        else:
            nominal_voltage = params["output"]["nominal"]
            logger.debug("Obtained nominal output voltage from parameters: %s", nominal_voltage)
    

    # Check for nominal voltag in params:
    

    # These values are for accepable multiplier ranges. See documentation on utility power.
    mch = 1.058
    mwh = 1.050
    mwl = 0.917
    mcl = 0.880

    # Initialize Variables
    voltage = []
    frequency = []

    # Run off of the input phase table if possible
    try:
        _ = session.get(f"{xppc_base}.1.1.1.3.1.1.0").value
    # Don't have any UPSs running on three phase to test gathering data here for.
    except easysnmp.exceptions.EasySNMPNoSuchNameError:
        voltage.append(int(session.get(f"{oid_ups_output}.4.1.2.1").value))
        frequency.append(int(session.get(f"{oid_ups_output}.2.0").value)/10)

    logger.debug("Voltage table: %s", pprint.pformat(voltage))
    logger.debug("Frequency table: %s", pprint.pformat(frequency))


    for i, volt in enumerate(voltage):
        if volt <= (nominal_voltage * mcl) or volt >= (nominal_voltage * mch):
            critical_checks.append(f"Output voltage on phase {i+1} outside critical threshold - {volt}V ")
            return_status = max(return_status, 2)
        elif volt <= (nominal_voltage * mwl) or volt >= (nominal_voltage * mwh):
            warning_checks.append(f"Output voltage on phase {i+1} outside warning threshold - {volt}V ")
            return_status = max(return_status, 1)
        elif volt > (nominal_voltage * mcl) and volt < (nominal_voltage * mch):
            ok_checks.append(f"Output voltage on phase {i+1} OK - {volt}V ")
        else:
            unknown_checks.append(f"Unknown value occurred checking output voltage on phase {i+1} ")
            return_status = max(return_status, 3)

        # Output frequency is different than input, see https://www.se.com/th/en/faqs/FA340026/
        if frequency[i] < 57.0 or frequency[i] > 63.0:
            critical_checks.append(f"Output frequency on phase {i+1} outside critical threshold - {frequency[i]}Hz ")
            return_status = max(return_status, 2)
        elif 57.0 <= frequency[i] <= 63.0:
            ok_checks.append(f"Output frequency on phase {i+1} OK - {frequency[i]}Hz ")
        else:
            unknown_checks.append(f"Unknown value occurred checking output frequency on phase {i+1} ")
            return_status = max(return_status, 3)

        return_data += f"phase{i+1}_voltage={volt}V;{round((nominal_voltage * mwh), 2)};"
        return_data += f"{round((nominal_voltage * mch), 2)} phase{i+1}_frequency={frequency[i]}Hz;;63.0 "

    output_load = int(session.get(f"{oid_ups_output}.4.1.5.1").value) 
    logger.debug("Raw output load: %s", output_load)
    if "load" not in params["output"]:
        print_help()
        sys.exit(3)
    else:
        warn_load = params["output"]["load"]["warn"]
        crit_load = params["output"]["load"]["crit"]


    if output_load >= crit_load:
        critical_checks.append(f"Output load above critical threshold - {output_load}% ")
        return_status = max(return_status, 2)
    elif output_load >= warn_load:
        warning_checks.append(f"Output load above warning threshold - {output_load}% ")
        return_status = max(return_status, 1)
    elif output_load < warn_load:
        ok_checks.append(f"Output load OK - {output_load}% ")
    else:
        unknown_checks.append("Unknown value encountered in output load check ")
        return_status = max(return_status, 3)

    # Get data for things we don't necessarily alert for
    output_current = int(session.get(f"{oid_ups_output}.4.1.3.1").value)
    logger.debug("Output current raw (dA): %s", output_current)
    output_current = output_current / 10
    return_text = f"Output current {output_current}A. "
    return_data += f"output_load={output_load}%;{warn_load};{crit_load} output_current={output_current}A "

def check_ambient(session): # pylint: disable=redefined-outer-name
    """
    Checks the ambient environmental conditions using SNMP.

    This function retrieves and evaluates temperature and humidity readings from an a UPS's environmental
    sensors using RFC1628 and XPPC-MIB. It assesses these readings against
    predefined thresholds to determine their status, updating global status and text variables accordingly.
    Thresholds can be overridden via the `overrides` parameter.

    Args:
        session (easysnmp.Session): The SNMP session object used to communicate with the UPS using XPPC-MIB and RFC1628.

    Returns:
        None
    """
    global return_status # pylint: disable=global-statement
    global return_data # pylint: disable=global-statement

    oid_xppc_env = f"{xppc_base}.1.1.1.9"

    # Set default unit of measurement for input and output
    input_uom = "C" 
    output_uom = "F"
    if "offset" in params and "external" in params["offset"]:
        external_offset = params["offset"]["external"]
        logger.debug("External offset: %s", external_offset)
    else:
        external_offset = 0

    # Get thresholds
    temp_max = int(session.get(f"{oid_xppc_env}.2.1.0").value) / 10
    temp_min = int(session.get(f"{oid_xppc_env}.2.2.0").value) / 10
    humidity_max = int(session.get(f"{oid_xppc_env}.2.3.0").value)
    humidity_min = int(session.get(f"{oid_xppc_env}.2.4.0").value)


    logger.debug("Temperature thresholds obtained from SNMP: max %s, min %s", 
    temp_max, temp_min)

    # Get temperature and humidity values
    sensor_temp = int(session.get(f"{oid_xppc_env}.1.1.0").value) / 10
    sensor_humidity = int(session.get(f"{oid_xppc_env}.1.2.0").value)
    

    # Convert units as needed. This should convert the thresholds and sensor temperature either way if needed. Overrides are taken literally, without any conversion.
    if input_uom == "F" and output_uom == "C":
        sensor_temp = round(ftoc(sensor_temp), 2)
        temp_max = ftoc(temp_max)
        temp_min = ftoc(temp_min)
    elif input_uom == "C" and output_uom == "F":
        sensor_temp = round(ctof(sensor_temp), 2)
        temp_max = ctof(temp_max)
        temp_min = ctof(temp_min)

    # Override our thresholds and unit of measurement if requested. 
    if  "temp" in overrides:
        logger.debug("Overriding obtained thresholds and uom.")
        temp = overrides["temp"]
        temp_max  = temp["max"]  if "max"  in temp else temp_max
        temp_min  = temp["min"]  if "min"  in temp else temp_min
        output_uom = temp["output_uom"]  if "output_uom"  in temp else "F"
        input_uom = temp["input_uom"]  if "input_uom"  in temp else "C"

    if  "humidity" in overrides:
        logger.debug("Overriding obtained thresholds.")
        humidity = overrides["humidity"]
        humidity_max  = humidity["max"]  if "max"  in humidity else humidity_max
        humidity_min  = humidity["min"]  if "min"  in humidity else humidity_min


    logger.debug("Final temperature thresholds: max %s, min %s", 
    temp_max, temp_min)

    logger.debug("Final humidity thresholds: max %s, min %s", 
    humidity_max, humidity_min)

    logger.debug("Temp: %s, humidity: %s",
    sensor_temp, sensor_humidity)

    logger.debug("Converted temperature and humidity: temperature %s, humidity %s", 
    sensor_temp, sensor_humidity)

    if sensor_temp >= temp_max or sensor_temp <= temp_min:
        return_text = "Temperature outside critical temperature threshold of " 
        return_text += f"{temp_min}{output_uom} to {temp_max}{output_uom} - {sensor_temp}{output_uom} "
        critical_checks.append(return_text)
        return_status = max(return_status, 2)
    elif temp_min < sensor_temp < temp_max:
        ok_checks.append(f"Temperature within normal temperature range - {sensor_temp}{output_uom} ")
    else:
        unknown_checks.append(f"Unknown value encountered while checking temperature ")
        return_status = max(return_status, 3)

    return_data += f"temp={sensor_temp}{output_uom};;{temp_max} "

    if sensor_humidity >= humidity_max or sensor_humidity <= humidity_min:
        return_text = f"Humidity outside critical humidity threshold of "
        return_text += f"{humidity_min} to {humidity_max} - {sensor_humidity}% "
        critical_checks.append(return_text)
        return_status = max(return_status, 2)
    elif humidity_min < sensor_humidity < humidity_max:
        ok_checks.append(f"Humidity within normal humidity range - {sensor_humidity}% ")
    else:
        unknown_checks.append(f"Unknown value encountered while checking humidity ")
        return_status = max(return_status, 3)

    return_data += f"humidity={sensor_humidity}%;;{humidity_max} "

    

def check_health(session):
    """
    Checks UPS Health

    This function checks the health of the UPS by getting it's status via XPPC-MIB.

    Args:
        session (easysnmp.Session): The SNMP session object used to communicate with the UPS using XPPC-MIB and RFC1628.

    Returns:
        None
    """

    global return_status # pylint: disable=global-statement
    # Check the health of the UPS. This includes things that aren't explicitly related to the other categories.
    # UPS Basic Output Status
    basic_output_status_oid = f"{xppc_base}.1.1.1.4.1.1.0" # .1.3.6.1.4.1.935 .1.1.1.4.1.1.0
    logger.debug("Checking OID %s", basic_output_status_oid)
    basic_output_status = int(session.get(basic_output_status_oid).value)
    results = [
        ("", 3), # Index starts at 1
        ("unknown", 3),
        ("online", 0), 
        ("on battery", 2), 
        ("on smart boost", 1), 
        ("timed sleeping", 2), 
        ("software bypass", 2), 
        ("rebooting", 2), 
        ("standby", 2), 
        ("on buck", 1), 
    ]

    overrides = {}

    if args.overrides:
        try:
            for ob in args.overrides:
                overrides.update(json.loads(ob))
        except Exception as e:
            print(f"[UNKNOWN] - Override JSON improper: {e}")
            sys.exit(3)

    # Add functionality to ignore certain statuses
    ret = results[basic_output_status][1]
    if "health" in overrides and "ignore" in overrides["health"]:
        if results[basic_output_status][0] == overrides["health"]["ignore"] \
        or results[basic_output_status][0] in overrides["health"]["ignore"]:
            ret = 0

    match ret:
        case 0:
            ok_checks.append(f"UPS mode is {results[basic_output_status][0]} ")
        case 1:
            warning_checks.append(f"UPS mode is {results[basic_output_status][0]} ")
        case 2:
            critical_checks.append(f"UPS mode is {results[basic_output_status][0]} ")
        case 3:
            unknown_checks.append(f"UPS mode is {results[basic_output_status][0]} ")

    return_status = max(return_status, ret)

# Create SNMP session
session = easysnmp.Session(hostname=args.hostname, community=args.community, version=int(args.version))

tests = args.test

if "all" in tests or "battery" in tests:
    check_battery(session) 
if "all" in tests or "input" in tests:
    check_input(session) 
if "all" in tests or "output" in tests:
    check_output(session) 
if "all" in tests or "ambient" in tests:
    check_ambient(session)
if "all" in tests or "health" in tests:
    check_health(session)






critical_checks = ["[CRITICAL] - " + x for x in critical_checks]
unknown_checks = ["[UNKNOWN] - " + x for x in unknown_checks]
warning_checks = ["[WARNING] - " + x for x in warning_checks]
ok_checks = ["[OK] - " + x for x in ok_checks]

all_checks = critical_checks + unknown_checks + warning_checks + ok_checks + append_text

return_text = "\n".join(all_checks)
alerting_count = len(critical_checks) + len(unknown_checks) + len(warning_checks)
ok_check_count = len(ok_checks)

if alerting_count > 1:
    return_text = f"{return_codes[return_status]} - {alerting_count} alert conditions exist \n" + return_text
elif alerting_count == 0 and len(ok_checks) > 1:
    return_text = f"{return_codes[return_status]} - {ok_check_count} conditions OK \n" + return_text

if return_data != "":
    return_text = f"{return_text} | {return_data}"

print(return_text)
sys.exit(return_status)

