#!/usr/bin/env python3
# ***************************************************************************
#  IIIIII NNN  NNN  Copyright (C) 2022 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@in-kc.com
#   Be Innovative.  for additional information.
# ***************************************************************************
#  check_snmp_apcups.py
#  Author - Ian Perry <iperry@indigex.com>
#
#  Purpose:  Checks various things on an APC UPS.
#
#  Version History:
#       2023.10.23 - Initial Creation
#       2024.08.14 - Added Override for Replacement Date
#       2025.02.03 - Fixed warnings, fixed output, fixed reference before assignment, ignored some warnings from pylint
# ***************************************************************************
# 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 OPNsense 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', 'comms', 'battery', 'input', 'output', 'test', '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:
        {
            <sensor_abbr><num>: {
                "temp": {
                    "max": <int>,
                    "high": <int>,
                    "low": <int>,
                    "min: <int>
                },
                "humidity: {
                    "max": <int>,
                    "high": <int>,
                    "low": <int>,
                    "min": <int>
                }
            }
        }

        Where <sensor_abbr> is either:
        "ext" for external environment monitor sensors
        "int" for integrated environment monitor sensors
        "uio" for universal i/o sensors (most common)
          """)
    sys.exit(3)

# Try to load JSON parameters. If we fail, exit with UNKNOWN
try:
    params = json.loads(args.parameters)
except Exception as e:
    print(f"[UNKNOWN] - JSON is invalid: {e}")
    sys.exit(3)

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)


# OID Bases
base            = "1.3.6.1.4.1.318.1.1"
oid_upstype     = f"{base}.1.1.1.1.0"
oid_ups_phase   = f"{base}.1.9"
oid_sysDescr    =  "1.3.6.1.2.1.1.1.0"

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

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


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

    Returns:
        float: Temperature converted to degrees Fahrenheit.
    """

    return (temp * 1.8) + 32

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



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

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

def check_comms(session): # pylint: disable=redefined-outer-name
    """
    Check communication with the APC UPS NMC.

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

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

    oid_comms = f"{base}.1.8.1.0"

    # Just testing communication with the UPS.
    comms_status = int(session.get(oid_comms).value)
    logger.debug("Comms_status: %s", comms_status)
    if comms_status != 1:
        return_text = f"APC NMC communication with UPS failed. {return_text}"
        return_status = max(return_status, 2)
    elif comms_status == 1:
        return_text += "APC NMC communication with UPS normal. "

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

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


    Returns:
        None
    """

    global return_text # pylint: disable=global-statement
    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
                      }
                  }
              }
              ''')

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

    oid_battery_capacity        = f"{base}.1.2.2.1.0"
    oid_battery_temperature     = f"{base}.1.2.2.2.0"
    oid_battery_runtimeremain   = f"{base}.1.2.2.3.0"
    oid_battery_replacement     = f"{base}.1.2.2.4.0"
    oid_battery_replacedate     = f"{base}.1.2.2.21.0"
    oid_battery_installdate     = f"{base}.1.2.1.3.0"

    # 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"]:
        return_text = f"Battery charge below critical threshold at {capacity}%. {return_text}"
        return_status = max(return_status, 2)
    elif params["battery"]["capacity"]["crit"] <= capacity <= params["battery"]["capacity"]["warn"]:
        return_text += f"Battery charge below warning threshold at {capacity}%. "
        return_status = max(return_status, 1)
    elif params["battery"]["capacity"]["warn"] < capacity:
        return_text += f"Battery charge OK - {capacity}%. "
        # No return status change
    else:
        return_text += "Unknown value encountered in capacity check. "
        return_status = max(return_status, 3)
    return_data += f'battery_capacity={capacity}%;{params["battery"]["capacity"]["warn"]};{params["battery"]["capacity"]["crit"]} '

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

    # Convert to fahrenheit if needed/wanted. We don't alert based on battery temperatures.
    # High battery temperature will reduce longevity but will not cause failures.
    if "temp" in params and params["temp"] == "F":
        temp = (temp * 1.8) + 32
        uom = "F"
    else:
        uom = "C"

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

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

    # Get remaining runtime. Value is in TIMETICKS (1/100 second)
    runtime = int(session.get(oid_battery_runtimeremain).value)
    logger.debug("Raw runtime: %s", runtime)
    runtime = runtime / 100
    logger.debug("Runtime in seconds: %s", runtime)

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

    # Get replacement status and 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)

    # Get the calculated replacement date, if we can.
    try:
        replace_date_get = session.get(oid_battery_replacedate).value
        logger.debug("Able to obtain replacement date from UPS, replacement date %s", replace_date_get)
        replace_date_get_data = datetime.strptime(replace_date_get, '%m/%d/%Y')
    except easysnmp.exceptions.EasySNMPNoSuchNameError:
        # Unable to get a replacement date, set at today's date.
        replace_date_get_data = current_date

    replace_date_calc = session.get(oid_battery_installdate).value
    logger.debug("Unable to obtain replacement date from SNMP, calculating based off install date %s", replace_date_calc)

    # For some reason, differing APC devices have differing date formats.
    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. {return_text}")
        sys.exit(3)
    # If we can't get the reccommended replacement date, we take the last replacement date + 5 years.
    replace_date_calc_data = replace_date_calc_data + timedelta(days=1826)

    # The replacement date is the calculated replacement date, or 5 years after install, whichever is greater.
    replace_date_data = max(replace_date_get_data, replace_date_calc_data)
    replace_date = replace_date_data.strftime(final_format)

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

        return_status = max(return_status, 2)
    elif current_date > (replace_date_data - timedelta(days=30)):
        return_text += f"Battery replacement date within next 30 days {replace_date}. "
        return_status = max(return_status, 1)
    elif current_date <= (replace_date_data - timedelta(days=30)):
        return_text += f"Battery replacement date more than 30 days in future {replace_date}. "
    else:
        return_text += "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_text # pylint: disable=global-statement
    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"
    oid_ups_input_nominal           = f"{base}.1.3.2.7.0"

    # Check for nominal voltage, if it's not available, access through parameters.
    try:
        nominal_voltage = 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 = []

    # Run off of the input phase table if possible
    try:
        _ = session.get(f"{oid_ups_phase}.2.1").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_input}.1.0").value)/10)
        frequency.append(int(session.get(f"{oid_ups_input}.4.0").value)/10)

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

    # Get last transfer reason
    last_transfer = int(session.get(f"{base}.1.3.2.5.0").value)
    logger.debug("Reason for last transfer: %s", last_transfer)

    for i, volt in enumerate(voltage):
        if volt <= (nominal_voltage * mcl) or volt >= (nominal_voltage * mch):
            return_text = f"Input voltage on phase {i+1} outside critical threshold - {volt}V. {return_text}"
            return_status = max(return_status, 2)
        elif volt <= (nominal_voltage * mwl) or volt >= (nominal_voltage * mwh):
            return_text += 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):
            return_text += f"Input voltage on phase {i+1} OK - {volt}V. "
        else:
            return_text += 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:
            return_text = f"Input frequency on phase {i+1} outside critical threshold - {frequency[i]}Hz. {return_text}"
            return_status = max(return_status, 2)
        elif 58.5 <= frequency[i] <= 60.5:
            return_text += f"Input frequency on phase {i+1} OK - {frequency[i]}Hz. "
        else:
            return_text += 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)};{round((nominal_voltage * mch), 2)} phase{i+1}_frequency={frequency[i]}Hz;;60.5 "


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

def check_output(session): # pylint: disable=redefined-outer-name
    """
    Check the output section of the APC UPS SNMP MIB.

    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_text # pylint: disable=global-statement
    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.3"

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

    # Check for nominal voltage:
    if "nominal" in params["output"]:
        nominal_voltage = params["output"]["nominal"]
        logger.debug("Obtained nominal output voltage from parameters: %s", nominal_voltage)
    else:
        print("Nominal output voltage must be provided in parameters['output']['nominal']")
        sys.exit(3)

    # 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"{oid_ups_phase}.2.1").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}.1.0").value)/10)
        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):
            return_text = f"Output voltage on phase {i+1} outside critical threshold - {volt}V. {return_text}"
            return_status = max(return_status, 2)
        elif volt <= (nominal_voltage * mwl) or volt >= (nominal_voltage * mwh):
            return_text += 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):
            return_text += f"Output voltage on phase {i+1} OK - {volt}V. "
        else:
            return_text += 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:
            return_text = f"Output frequency on phase {i+1} outside critical threshold - {frequency[i]}Hz. {return_text}"
            return_status = max(return_status, 2)
        elif 57.0 <= frequency[i] <= 63.0:
            return_text += f"Output frequency on phase {i+1} OK - {frequency[i]}Hz. "
        else:
            return_text += 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)};{round((nominal_voltage * mch), 2)} phase{i+1}_frequency={frequency[i]}Hz;;63.0 "

    # Output load is in 1/10%
    output_load = int(session.get(f"{oid_ups_output}.3.0").value)
    logger.debug("Raw output load: %s", output_load)
    output_load = output_load / 10
    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:
        return_text = f"Output load above critical threshold - {output_load}%. {return_text}"
        return_status = max(return_status, 2)
    elif output_load >= warn_load:
        return_text += f"Output load above warning threshold - {output_load}%. "
        return_status = max(return_status, 1)
    elif output_load < warn_load:
        return_text += f"Output load OK - {output_load}%. "
    else:
        return_text += "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.0").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_test(session): # pylint: disable=redefined-outer-name
    """
    Check the last self-test result of the UPS. If the test failed or was invalid, 
    return CRITICAL. If the test was successful, return OKAY. If the test was more 
    than 15 days ago, return WARNING. If the test is currently in progress, 
    append that to the return message. If the test returned an unknown value, 
    return UNKNOWN.

    :param session: An instance of the easysnmp Session class
    :type session: easysnmp.Session
    """
    global return_text # pylint: disable=global-statement
    global return_status # pylint: disable=global-statement

    oid_test_result = f"{base}.1.7.2.3.0"
    oid_test_date   = f"{base}.1.7.2.4.0"

    # Get test data
    test_status = int(session.get(oid_test_result).value)
    test_date   = session.get(oid_test_date).value
    logger.debug("Test status %s on test date %s", test_status, test_date)
    test_date_data = datetime.strptime(test_date, '%m/%d/%Y')

    # Various test results.
    if test_status == 2:
        return_text = f"Last test on {test_date} failed. {return_text}"
        return_status = max(return_status, 2)
    elif test_status == 3:
        return_text = f"Last test on {test_date} was invalid. {return_text}"
        return_status = max(return_status, 2)
    elif test_status == 4:
        return_text += "Test currently in progress. "
    elif test_status == 1:
        return_text += f"Test on {test_date} passed. "
    else:
        return_text += "Unknown value encountered checking test data. "
        return_status = max(return_status, 3)

    # If last test date was more than around 2 weeks ago, we want that fixed.
    current_date = date.today()
    current_date = datetime(current_date.year, current_date.month, current_date.day)
    if test_date_data < (current_date - timedelta(days=15)):
        return_text = f"Last test date was more than 15 days ago. {return_text}"
        return_status = max(return_status, 2)
    elif test_date_data >= (current_date - timedelta(days=15)):
        return_text += "Last test was within the last 15 days. "
    else:
        return_text += "Unknown value encountered when checking test date. "
        return_status = max(return_status, 3)

def check_ambient(session): # pylint: disable=redefined-outer-name
    """
    Checks the ambient environmental conditions using SNMP for external, integrated, and universal sensors.

    This function retrieves and evaluates temperature and humidity readings from an APC UPS's environmental 
    sensors, including external, integrated, and universal IO sensors. 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 APC UPS.

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

    oid_iem = f"{base}.10"
    oid_uio = f"{base}.25.1"

    # There are two pieces to this, one for the IEM (integrated environment monitor)
    # and one for UIO (universal IO)
    if "temp" in params:
        uom = params["temp"]
    else:
        uom = "C"


    # Sensor Counts
    # Get a count of total external, integrated, and universal sensors.
    try:
        num_external = int(session.get(f"{oid_iem}.1.3.2.0").value)
        external_status = int(session.get(f"{oid_iem}.1.3.1.0").value)
        logger.debug("Number of external sensors: %s with status %s", num_external, external_status)

        # This is for IEM communications status. 1 is no connection and there has not been
        # 2 is for connection established, and 3 is for connection lost.
        if external_status == 1:
            num_external = 0
        elif external_status == 3:
            return_text = f"IEM Communication lost. {return_text}"
            return_status = max(return_status, 2)
    except easysnmp.exceptions.EasySNMPNoSuchNameError:
        num_external = 0

    try:
        num_integrated = int(session.get(f"{oid_iem}.2.3.1.0").value)
        logger.debug("Number of integrated sensors: %s", num_integrated)
    except easysnmp.exceptions.EasySNMPNoSuchNameError:
        num_integrated = 0

    try:
        num_universal = int(session.get(f"{oid_uio}.1.0").value)
        logger.debug("Number of universal sensors: %s", num_universal)
    except easysnmp.exceptions.EasySNMPNoSuchNameError:
        num_universal = 0

    if "temp" in params:
        uom = params["temp"]
    else:
        uom = "C"

    # IEM
    ## IEM External

    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
    for i in range(0, num_external):
        sensor_number = i + 1 + external_offset
        sensor_temp = int(session.get(f"{oid_iem}.1.3.3.1.4.{sensor_number}").value)
        sensor_temp_units = int(session.get(f"{oid_iem}.1.3.3.1.5.{sensor_number}").value)
        sensor_humidity = int(session.get(f"{oid_iem}.1.3.3.1.6.{sensor_number}").value)
        logger.debug("External sensor number %s temp: %s, temp_units: %s, humidity: %s",
                     sensor_number, sensor_temp, sensor_temp_units, sensor_humidity)

        # Get thresholds.
        if sensor_temp != -1:
            temp_high = int(session.get(f"{oid_iem}.1.2.2.1.3.{sensor_number}").value)
            temp_low = int(session.get(f"{oid_iem}.1.2.2.1.4.{sensor_number}").value)
            temp_max = int(session.get(f"{oid_iem}.1.2.2.1.12.{sensor_number}").value)
            temp_min = int(session.get(f"{oid_iem}.1.2.2.1.13.{sensor_number}").value)
            logger.debug("Temperature thresholds obtained from SNMP: max %s, high %s, low %s, min %s",
                         temp_max, temp_high, temp_low, temp_min)

            if f"ext{i+external_offset}" in overrides and "temp" in overrides[f"ext{i+external_offset}"]:
                logger.debug("Overriding obtained thresholds.")
                temp = overrides[f"ext{sensor_number}"]["temp"]
                temp_max  = temp["max"]  if "max"  in temp else temp_max
                temp_high = temp["high"] if "high" in temp else temp_high
                temp_low  = temp["low"]  if "low"  in temp else temp_low
                temp_min  = temp["min"]  if "min"  in temp else temp_min

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

            if uom == "F" and sensor_temp_units == 1:
                sensor_temp = ctof(sensor_temp)
                temp_high = ctof(temp_high)
                temp_low = ctof(temp_low)
                temp_max = ctof(temp_max)
                temp_min = ctof(temp_min)
            elif uom == "C" and sensor_temp_units == 2:
                sensor_temp = ftoc(sensor_temp)
                temp_high = ftoc(temp_high)
                temp_low = ftoc(temp_low)
                temp_max = ftoc(temp_max)
                temp_min = ftoc(temp_min)

            if sensor_temp >= temp_max or sensor_temp <= temp_min:
                return_text = f"External probe {sensor_number} outside critical temperature threshold of {temp_min}{uom} to {temp_max}{uom} - {sensor_temp}{uom}. {return_text}"
                return_status = max(return_status, 2)
            elif sensor_temp >= temp_high or sensor_temp <= temp_low:
                return_text += f"External probe {sensor_number} outside warning temperature threshold of {temp_low}{uom} to {temp_high}{uom} - {sensor_temp}{uom}. "
                return_status = max(return_status, 1)
            elif temp_low < sensor_temp < temp_high:
                return_text += f"External probe {sensor_number} within normal temperature range - {sensor_temp}{uom}. "
            else:
                return_text += f"Unknown value encountered checking external probe {sensor_number}. "
                return_status = max(return_status, 3)

            return_data += f"iem_ex{sensor_number}_temp={sensor_temp}{uom};{temp_high};{temp_max} "


        if sensor_humidity > 0:
            # Since we can only set thresholds on UPSs down to 10 and up to 90
            humidity_high = int(session.get(f"{oid_iem}.1.2.2.1.6.{sensor_number}").value)
            humidity_low = int(session.get(f"{oid_iem}.1.2.2.1.7.{sensor_number}").value)
            humidity_max = int(session.get(f"{oid_iem}.1.2.2.1.14.{sensor_number}").value)
            humidity_min = int(session.get(f"{oid_iem}.1.2.2.1.15.{sensor_number}").value)
            logger.debug("Humidity thresholds obtained from SNMP: max %s, high %s, low %s, min %s",
                         humidity_max, humidity_high, humidity_low, humidity_min)
            if f"ext{i+external_offset}" in overrides and "humidity" in overrides[f"ext{i+external_offset}"]:
                logger.debug("Overriding obtained thresholds.")
                humidity = overrides[f"ext{sensor_number}"]["humidity"]
                humidity_max  = humidity["max"]  if "max"  in humidity else humidity_max
                humidity_high = humidity["high"] if "high" in humidity else humidity_high
                humidity_low  = humidity["low"]  if "low"  in humidity else humidity_low
                humidity_min  = humidity["min"]  if "min"  in humidity else humidity_min

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

            if sensor_humidity >= humidity_max or sensor_humidity <= humidity_min:
                return_text = f"External probe {sensor_number} outside critical humidity threshold of {humidity_min}% to {humidity_max}% - {sensor_humidity}%. {return_text}"
                return_status = max(return_status, 2)
            elif sensor_humidity >= humidity_high or sensor_humidity <= humidity_low:
                return_text += f"External probe {sensor_number} outside warning humidity threshold of {humidity_low}% to {humidity_high}% - {sensor_humidity}%. "
                return_status = max(return_status, 1)
            elif humidity_low < sensor_humidity < humidity_high:
                return_text += f"External probe {sensor_number} within normal humidity range - {sensor_humidity}%. "
            else:
                return_text += f"Unknown value encountered checking external probe {sensor_number}. "
                return_status = max(return_status, 3)

            return_data += f"iem_ex{sensor_number}_humidity={sensor_humidity}%;{humidity_high};{humidity_max} "


    if "offset" in params and "integrated" in params["offset"]:
        integrated_offset = params["offset"]["integrated"]
    else:
        integrated_offset = 0
    for i in range(0, num_integrated):
        sensor_number = i + 1 + integrated_offset
        sensor_temp = int(session.get(f"{oid_iem}.2.3.2.1.4.{sensor_number}").value)
        sensor_temp_units = int(session.get(f"{oid_iem}.2.3.2.1.5.{sensor_number}").value)
        sensor_humidity = int(session.get(f"{oid_iem}.2.3.2.1.6.{sensor_number}").value)
        logger.debug("Integrated sensor number %s temp: %s, temp_units: %s, humidity: %s",
                     sensor_number, sensor_temp, sensor_temp_units, sensor_humidity)

        # Get thresholds.
        if sensor_temp != -1:
            temp_high = int(session.get(f"{oid_iem}.2.2.2.1.3.{sensor_number}").value)
            temp_low = int(session.get(f"{oid_iem}.2.2.2.1.4.{sensor_number}").value)
            temp_max = int(session.get(f"{oid_iem}.2.2.2.1.12.{sensor_number}").value)
            temp_min = int(session.get(f"{oid_iem}.2.2.2.1.13.{sensor_number}").value)
            logger.debug("Temperature thresholds obtained from SNMP: max %s, high %s, low %s, min %s",
                         temp_max, temp_high, temp_low, temp_min)
            if f"int{sensor_number}" in overrides and "temp" in overrides[f"int{sensor_number}"]:
                logger.debug("Overriding obtained thresholds.")
                temp = overrides[f"int{sensor_number}"]["temp"]
                temp_max  = temp["max"]  if "max"  in temp else temp_max
                temp_high = temp["high"] if "high" in temp else temp_high
                temp_low  = temp["low"]  if "low"  in temp else temp_low
                temp_min  = temp["min"]  if "min"  in temp else temp_min

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

            if uom == "F" and sensor_temp_units == 1:
                sensor_temp = ctof(sensor_temp)
                temp_high = ctof(temp_high)
                temp_low = ctof(temp_low)
                temp_max = ctof(temp_max)
                temp_min = ctof(temp_min)
            elif uom == "C" and sensor_temp_units == 2:
                sensor_temp = ftoc(sensor_temp)
                temp_high = ftoc(temp_high)
                temp_low = ftoc(temp_low)
                temp_max = ftoc(temp_max)
                temp_min = ftoc(temp_min)

            if sensor_temp >= temp_max or sensor_temp <= temp_min:
                return_text = f"Integrated probe {sensor_number} outside critical temperature threshold of {temp_min}{uom} to {temp_max}{uom} - {sensor_temp}{uom}. {return_text}"
                return_status = max(return_status, 2)
            elif sensor_temp >= temp_high or sensor_temp <= temp_low:
                return_text += f"Integrated probe {sensor_number} outside warning temperature threshold of {temp_low}{uom} to {temp_high}{uom} - {sensor_temp}{uom}. "
                return_status = max(return_status, 1)
            elif temp_low < sensor_temp < temp_high:
                return_text += f"Integrated probe {sensor_number} within normal temperature range - {sensor_temp}{uom}. "
            else:
                return_text += f"Unknown value encountered checking integrated probe {sensor_number}. "
                return_status = max(return_status, 3)

            return_data += f"iem_in{sensor_number}_temp={sensor_temp}{uom};{temp_high};{temp_max} "


        if sensor_humidity > 0:
            # Since we can only set thresholds on UPSs down to 10 and up to 90
            humidity_high = int(session.get(f"{oid_iem}.2.2.2.1.6.{sensor_number}").value)
            humidity_low = int(session.get(f"{oid_iem}.2.2.2.1.7.{sensor_number}").value)
            humidity_max = int(session.get(f"{oid_iem}.2.2.2.1.14.{sensor_number}").value)
            humidity_min = int(session.get(f"{oid_iem}.2.2.2.1.15.{sensor_number}").value)
            logger.debug("Humidity thresholds obtained from SNMP: max %s, high %s, low %s, min %s",
                         humidity_max, humidity_high, humidity_low, humidity_min)
            if f"int{sensor_number}" in overrides and "humidity" in overrides[f"int{sensor_number}"]:
                logger.debug("Overriding obtained thresholds.")
                humidity = overrides[f"int{sensor_number}"]["humidity"]
                humidity_max  = humidity["max"]  if "max"  in humidity else humidity_max
                humidity_high = humidity["high"] if "high" in humidity else humidity_high
                humidity_low  = humidity["low"]  if "low"  in humidity else humidity_low
                humidity_min  = humidity["min"]  if "min"  in humidity else humidity_min

            if sensor_humidity >= humidity_max or sensor_humidity <= humidity_min:
                return_text = f"Integrated probe {sensor_number} outside critical humidity threshold of {humidity_min}% to {humidity_max}% - {sensor_humidity}%. {return_text}"
                return_status = max(return_status, 2)
            elif sensor_humidity >= humidity_high or sensor_humidity <= humidity_low:
                return_text += f"Integrated probe {sensor_number} outside warning humidity threshold of {humidity_low}% to {humidity_high}% - {sensor_humidity}%. "
                return_status = max(return_status, 1)
            elif humidity_low < sensor_humidity < humidity_high:
                return_text += f"Integrated probe {sensor_number} within normal humidity range - {sensor_humidity}%. "
            else:
                return_text += f"Unknown value encountered checking integrated probe {sensor_number}. "
                return_status = max(return_status, 3)

            return_data += f"iem_in{sensor_number}_humidity={sensor_humidity}%;{humidity_high};{humidity_max} "

    if "offset" in params and "universal" in params["offset"]:
        universal_offset = params["offset"]["universal"]
    else:
        universal_offset = 0
    for i in range(0, num_universal):
        sensor_number = i + 1 + universal_offset
        if uom == "F":
            sensor_temp = int(session.get(f"{oid_uio}.2.1.5.{sensor_number}.1").value)
        else:
            sensor_temp = int(session.get(f"{oid_uio}.2.1.6.{sensor_number}.1").value)

        sensor_humidity = int(session.get(f"{oid_uio}.2.1.7.{sensor_number}.1").value)

        # Get thresholds.
        if sensor_temp != -1:
            temp_high = int(session.get(f"{oid_uio}.4.1.7.{sensor_number}.1").value)
            temp_low = int(session.get(f"{oid_uio}.4.1.6.{sensor_number}.1").value)
            temp_max = int(session.get(f"{oid_uio}.4.1.8.{sensor_number}.1").value)
            temp_min = int(session.get(f"{oid_uio}.4.1.5.{sensor_number}.1").value)
            logger.debug("Temperature thresholds obtained from SNMP: max %s, high %s, low %s, min %s",
                         temp_max, temp_high, temp_low, temp_min)
            if f"uio{sensor_number}" in overrides and "temp" in overrides[f"uio{sensor_number}"]:
                logger.debug("Overriding obtained thresholds.")
                temp = overrides[f"uio{sensor_number}"]["temp"]
                temp_max  = temp["max"]  if "max"  in temp else temp_max
                temp_high = temp["high"] if "high" in temp else temp_high
                temp_low  = temp["low"]  if "low"  in temp else temp_low
                temp_min  = temp["min"]  if "min"  in temp else temp_min

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

            # These thresholds are always in deg C
            if uom == "F":
                temp_high = ctof(temp_high)
                temp_low = ctof(temp_low)
                temp_max = ctof(temp_max)
                temp_min = ctof(temp_min)

            if sensor_temp >= temp_max or sensor_temp <= temp_min:
                return_text = f"Universal probe {sensor_number} outside critical temperature threshold of {temp_min}{uom} to {temp_max}{uom} - {sensor_temp}{uom}. {return_text}"
                return_status = max(return_status, 2)
            elif sensor_temp >= temp_high or sensor_temp <= temp_low:
                return_text += f"Universal probe {sensor_number} outside warning temperature threshold of {temp_low}{uom} to {temp_high}{uom} - {sensor_temp}{uom}. "
                return_status = max(return_status, 1)
            elif temp_low < sensor_temp < temp_high:
                return_text += f"Universal probe {sensor_number} within normal temperature range - {sensor_temp}{uom}. "
            else:
                return_text += f"Unknown value encountered checking universal probe {sensor_number}. "
                return_status = max(return_status, 3)

            return_data += f"uio{sensor_number}_temp={sensor_temp}{uom};{temp_high};{temp_max} "
        else:
            return_text += f"Universal sensor {sensor_number} returned an invalid reading due to lost communication."
            return_status = max(return_status, 3)


        if sensor_humidity != -1:
            # Since we can only set thresholds on UPSs down to 10 and up to 90
            humidity_high = int(session.get(f"{oid_uio}.4.1.16.{sensor_number}.1").value)
            humidity_low = int(session.get(f"{oid_uio}.4.1.15.{sensor_number}.1").value)
            humidity_max = int(session.get(f"{oid_uio}.4.1.17.{sensor_number}.1").value)
            humidity_min = int(session.get(f"{oid_uio}.4.1.14.{sensor_number}.1").value)
            if f"uio{sensor_number}" in overrides and "humidity" in overrides[f"uio{sensor_number}"]:
                logger.debug("Overriding obtained thresholds.")
                humidity = overrides[f"uio{sensor_number}"]["humidity"]
                humidity_max  = humidity["max"]  if "max"  in humidity else humidity_max
                humidity_high = humidity["high"] if "high" in humidity else humidity_high
                humidity_low  = humidity["low"]  if "low"  in humidity else humidity_low
                humidity_min  = humidity["min"]  if "min"  in humidity else humidity_min

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


            # Output text per T20250129.0007
            if sensor_humidity > humidity_max:
                return_text = f"Universal probe {sensor_number} CRITICAL, {sensor_humidity}% > {humidity_max}% maximum. {return_text}"
                return_status = max(return_status, 2)
            elif sensor_humidity < humidity_min:
                return_text = f"Universal probe {sensor_number} CRITICAL, {sensor_humidity}% < {humidity_min}% minimum. {return_text}"
                return_status = max(return_status, 2)
            elif sensor_humidity < humidity_low:
                return_text = f"Universal probe {sensor_number} WARNING, {sensor_humidity}% < {humidity_low}% low. {return_text}"
                return_status = max(return_status, 1)
            elif sensor_humidity > humidity_high:
                return_text = f"Universal probe {sensor_number} WARNING, {sensor_humidity}% > {humidity_high}% high. {return_text}"
                return_status = max(return_status, 1)
            elif humidity_low <= sensor_humidity <= humidity_high:
                return_text += f"Universal probe {sensor_number} within normal humidity range - {sensor_humidity}% . "
            else:
                return_text += f"Unknown value encountered checking universal probe {sensor_number}. "
                return_status = max(return_status, 3)

            return_data += f"uio{sensor_number}_humidity={sensor_humidity}%;{humidity_high};{humidity_max} "

    if (num_external + num_integrated + num_universal) == 0:
        return_text += "This device doesn't support ambient environment monitoring. "
        
def check_health(session):
    global return_text # pylint: disable=global-statement
    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"{base}.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),
        ("off", 2),
        ("rebooting", 2),
        ("switched bypass", 2),
        ("hardware failure bypass", 2),
        ("sleeping until power return", 2),
        ("on smart trim", 1),
        ("eco mode", 2),
        ("hot standby", 2),
        ("on battery test", 1),
        ("emergency static bypass", 2),
        ("static bypass standby", 3),
        ("power saving mode", 2),
        ("spot mode", 2),
        ("eConversion", 2),
        ("charger spot mode", 2),
        ("inverter spot mode", 2),
        ("active load", 2),
        ("inverter standby", 2),
        ("charger only", 2),
        ("distributed energy reserve", 2),
        ("self test", 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

    return_text += 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 "comms" in tests:
    check_comms(session)
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 "test" in tests:
    check_test(session)
if "all" in tests or "ambient" in tests:
    check_ambient(session)
if "all" in tests or "health" in tests:
    check_health(session)

return_codes = ['[OK]', '[WARNING]', '[CRITICAL]', '[UNKNOWN]']
return_text = f"{return_codes[return_status]} - {''.join(return_text.rsplit(';', 1))}"
if return_data != "":
    return_text = f"{return_text} | {return_data}"

print(return_text)
sys.exit(return_status)
