#!/usr/bin/python
# -*- encoding: utf-8; py-indent-offset: 4 -*-
# +------------------------------------------------------------------+
# |             ____ _               _        __  __ _  __           |
# |            / ___| |__   ___  ___| | __   |  \/  | |/ /           |
# |           | |   | '_ \ / _ \/ __| |/ /   | |\/| | ' /            |
# |           | |___| | | |  __/ (__|   <    | |  | | . \            |
# |            \____|_| |_|\___|\___|_|\_\___|_|  |_|_|\_\           |
# |                                                                  |
# | Copyright Mathias Kettner 2014             mk@mathias-kettner.de |
# +------------------------------------------------------------------+
#
# This file is part of Check_MK.
# The official homepage is at http://mathias-kettner.de/check_mk.
#
# check_mk is free software;  you can redistribute it and/or modify it
# under the  terms of the  GNU General Public License  as published by
# the Free Software Foundation in version 2.  check_mk is  distributed
# in the hope that it will be useful, but WITHOUT ANY WARRANTY;  with-
# out even the implied warranty of  MERCHANTABILITY  or  FITNESS FOR A
# PARTICULAR PURPOSE. See the  GNU General Public License for more de-
# tails. You should have  received  a copy of the  GNU  General Public
# License along with GNU Make; see the file  COPYING.  If  not,  write
# to the Free Software Foundation, Inc., 51 Franklin St,  Fifth Floor,
# Boston, MA 02110-1301 USA.

# .1.3.6.1.4.1.318.1.1.1.2.1.1.0 2
# .1.3.6.1.4.1.318.1.1.1.4.1.1.0 2
# .1.3.6.1.4.1.318.1.1.1.11.1.1.0 0001010000000000001000000000000000000000000000000000000000000000
# .1.3.6.1.4.1.318.1.1.1.2.2.1.0 100
# .1.3.6.1.4.1.318.1.1.1.2.2.4.0 1
# .1.3.6.1.4.1.318.1.1.1.2.2.6.0 0
# .1.3.6.1.4.1.318.1.1.1.2.2.3.0 360000
# .1.3.6.1.4.1.318.1.1.1.7.2.6.0 2
# .1.3.6.1.4.1.318.1.1.1.7.2.4.0 0
# .1.3.6.1.4.1.318.1.1.1.2.2.2.0 25
# .1.3.6.1.4.1.318.1.1.1.2.2.9.0 0

# upsBasicStateOutputState:
# The flags are numbered 1 to 64, read from left to right. The flags are defined as follows:
# 1: Abnormal Condition Present, 2: On Battery, 3: Low Battery, 4: On Line
# 5: Replace Battery, 6: Serial Communication Established, 7: AVR Boost Active
# 8: AVR Trim Active, 9: Overload, 10: Runtime Calibration, 11: Batteries Discharged
# 12: Manual Bypass, 13: Software Bypass, 14: In Bypass due to Internal Fault
# 15: In Bypass due to Supply Failure, 16: In Bypass due to Fan Failure
# 17: Sleeping on a Timer, 18: Sleeping until Utility Power Returns
# 19: On, 20: Rebooting, 21: Battery Communication Lost, 22: Graceful Shutdown Initiated
# 23: Smart Boost or Smart Trim Fault, 24: Bad Output Voltage, 25: Battery Charger Failure
# 26: High Battery Temperature, 27: Warning Battery Temperature, 28: Critical Battery Temperature
# 29: Self Test In Progress, 30: Low Battery / On Battery, 31: Graceful Shutdown Issued by Upstream Device
# 32: Graceful Shutdown Issued by Downstream Device, 33: No Batteries Attached
# 34: Synchronized Command is in Progress, 35: Synchronized Sleeping Command is in Progress
# 36: Synchronized Rebooting Command is in Progress, 37: Inverter DC Imbalance
# 38: Transfer Relay Failure, 39: Shutdown or Unable to Transfer, 40: Low Battery Shutdown
# 41: Electronic Unit Fan Failure, 42: Main Relay Failure, 43: Bypass Relay Failure
# 44: Temporary Bypass, 45: High Internal Temperature, 46: Battery Temperature Sensor Fault
# 47: Input Out of Range for Bypass, 48: DC Bus Overvoltage, 49: PFC Failure
# 50: Critical Hardware Fault, 51: Green Mode/ECO Mode, 52: Hot Standby
# 53: Emergency Power Off (EPO) Activated, 54: Load Alarm Violation, 55: Bypass Phase Fault
# 56: UPS Internal Communication Failure, 57-64: <Not Used>


def parse_apc_symmetra(info):
    parsed = {}
    if not info:
        return parsed

    # some numeric fields may be empty
    battery_status,  output_status,           battery_capacity, \
    battery_replace, battery_num_batt_packs,  battery_time_remain,    calib_result, \
    last_diag_date,  battery_temp,            battery_current, state_output_state      = info[0]

    if state_output_state != '':
        # string contains a bitmask, convert to int
        output_state_bitmask = int(state_output_state, 2)
    else:
        output_state_bitmask = 0
    self_test_in_progress = output_state_bitmask & 1 << 35 != 0

    for key, val in [
        ("status", battery_status),
        ("output", output_status),
        ("self_test", self_test_in_progress),
        ("capacity", battery_capacity),
        ("replace", battery_replace),
        ("num_packs", battery_num_batt_packs),
        ("time_remain", battery_time_remain),
        ("calib", calib_result),
        ("diag_date", last_diag_date),
    ]:
        if val:
            parsed.setdefault("status", {})
            parsed["status"][key] = val

    if battery_temp:
        parsed["temp"] = float(battery_temp)

    if battery_current:
        parsed["elphase"] = {"Battery": {"current": float(battery_current)}}

    return parsed


#   .--battery status------------------------------------------------------.
#   |   _           _   _                        _        _                |
#   |  | |__   __ _| |_| |_ ___ _ __ _   _   ___| |_ __ _| |_ _   _ ___    |
#   |  | '_ \ / _` | __| __/ _ \ '__| | | | / __| __/ _` | __| | | / __|   |
#   |  | |_) | (_| | |_| ||  __/ |  | |_| | \__ \ || (_| | |_| |_| \__ \   |
#   |  |_.__/ \__,_|\__|\__\___|_|   \__, | |___/\__\__,_|\__|\__,_|___/   |
#   |                                |___/                                 |
#   '----------------------------------------------------------------------'

# old format:
# apc_default_levels = ( 95, 40, 1, 220 )  or  { "levels" : ( 95, 40, 1, 220 ) }
# crit_capacity, crit_sys_temp, crit_batt_curr, crit_voltage = levels
# Temperature default now 60C: regadring to a apc technician a temperature up tp 70C is possible
factory_settings["apc_default_levels"] = {
    "capacity": (95, 80),
    "calibration_state": 0,
    "battery_replace_state": 1,
}


def inventory_apc_symmetra(parsed):
    return [(None, {})]


def check_apc_symmetra(_no_item, params, parsed):
    data = parsed["status"]
    battery_status = data.get("status")
    output_status = data.get("output")
    self_test_in_progress = data.get("self_test")
    battery_capacity = data.get("capacity")
    battery_replace = data.get("replace")
    battery_num_batt_packs = data.get("num_packs")
    battery_time_remain = data.get("time_remain")
    calib_result = data.get("calib")
    last_diag_date = data.get("diag_date")

    # convert old format tuple to dict, new format with up to 6 params in dict
    if isinstance(params, tuple):
        params = {"levels": params}

    if "levels" in params:
        crit_cap = params["levels"][0]
        params["capacity"] = (crit_cap, crit_cap)

    alt_crit_capacity = None
    warn_cap, crit_cap = params['capacity']
    # the last_diag_date is reported as %m/%d/%Y or %y
    if params.get("post_calibration_levels") and \
       last_diag_date not in [ None, 'Unknown' ] and \
       len(last_diag_date) in [8, 10]:
        year_format = '%y' if len(last_diag_date) == 8 else '%Y'
        last_ts = time.mktime(time.strptime(last_diag_date, '%m/%d/' + year_format))
        diff_sec = time.time() - last_ts
        allowed_delay_sec = 86400 + params['post_calibration_levels']['additional_time_span']
        alt_crit_capacity = params['post_calibration_levels']['altcapacity']

    state, state_readable = {
        "1": (3, "unknown"),
        "2": (0, "normal"),
        "3": (2, "low"),
        "4": (2, "in fault condition"),
    }.get(battery_status, (3, "unexpected(%s)" % battery_status))
    yield state, "Battery status: %s" % state_readable

    if battery_replace:
        state, state_readable = {
            "1": (0, "no battery needs replacing"),
            "2": (params.get("battery_replace_state", 1), "battery needs replacing"),
        }.get(battery_replace, (3, "battery needs replacing: unknown"))
        if battery_num_batt_packs and int(battery_num_batt_packs) > 1:
            yield 2, "%i batteries need replacement" % int(battery_num_batt_packs)
        elif state:
            yield state, state_readable

    if output_status:
        output_status_txts = {
            "1": "unknown",
            "2": "on line",
            "3": "on battery",
            "4": "on smart boost",
            "5": "timed sleeping",
            "6": "software bypass",
            "7": "off",
            "8": "rebooting",
            "9": "switched bypass",
            "10": "hardware failure bypass",
            "11": "sleeping until power return",
            "12": "on smart trim",
            "13": "eco mode",
            "14": "hot standby",
            "15": "on battery test",
            "16": "emergency static bypass",
            "17": "static bypass standby",
            "18": "power saving mode",
            "19": "spot mode",
            "20": "e conversion",
        }
        state_readable = output_status_txts.get(output_status, "unexpected(%s)" % output_status)

        if output_status not in output_status_txts:
            state = 3
        elif output_status not in ["2", "4", "12"] and \
             calib_result != "3" and not self_test_in_progress:
            state = 2
        elif output_status in ["2", "4", "12"] and \
             calib_result == "2" and not self_test_in_progress:
            state = params.get("calibration_state")
        else:
            state = 0

        calib_text = {
            "1": "",
            "2": " (calibration invalid)",
            "3": " (calibration in progress)",
        }.get(calib_result, " (calibration unexpected(%s))" % calib_result)

        yield state, "Output status: %s%s%s" % (
            state_readable, calib_text, self_test_in_progress and " (self-test running)" or "")

    if battery_capacity:
        battery_capacity = int(battery_capacity)
        state = 0
        levelstxt = ""
        if alt_crit_capacity is not None and diff_sec < allowed_delay_sec:
            if battery_capacity < alt_crit_capacity:
                state = 2
                levelstxt = " (crit below %d%% in delay after calibration)" % alt_crit_capacity
        else:
            if battery_capacity < crit_cap:
                state = 2
                levelstxt = " (warn/crit below %.1f%%/%.1f%%)" % (warn_cap, crit_cap)
            elif battery_capacity < warn_cap:
                state = 1
                levelstxt = " (warn/crit below %.1f%%/%.1f%%)" % (warn_cap, crit_cap)

        yield state, "Capacity: %d%%%s" % (battery_capacity, levelstxt), \
              [("capacity", battery_capacity, warn_cap, crit_cap, 0, 100)]

    if battery_time_remain:
        battery_time_remain = float(battery_time_remain) / 100
        battery_time_remain_readable = get_age_human_readable(battery_time_remain)
        state = 0
        levelstxt = ""
        battery_time_warn, battery_time_crit = None, None
        if params.get('battime'):
            battery_time_warn, battery_time_crit = params['battime']
            if battery_time_remain < battery_time_crit:
                state = 2
            elif battery_time_remain < battery_time_warn:
                state = 1
            perfdata = [("runtime", battery_time_remain / 60, battery_time_warn / 60,
                         battery_time_crit / 60)]
        else:
            perfdata = [("runtime", battery_time_remain / 60)]

        if state:
            levelstxt = " (warn/crit below %s/%s)" % \
                          (get_age_human_readable(battery_time_warn),
                           get_age_human_readable(battery_time_crit))

        yield state, "Time remaining: %s%s" % (battery_time_remain_readable, levelstxt), perfdata


check_info['apc_symmetra'] = {
    "parse_function": parse_apc_symmetra,
    "inventory_function": inventory_apc_symmetra,
    "check_function": check_apc_symmetra,
    "service_description": "APC Symmetra status",
    # A Note on the order of OIDs: If the 11.1.1.0 is not the last to be polled,
    # this leads to bogus values for some other OIDs on some devices.
    "snmp_info": (
        ".1.3.6.1.4.1.318.1.1.1",
        [
            "2.1.1.0",  # PowerNet-MIB::upsBasicBatteryStatus,
            "4.1.1.0",  # PowerNet-MIB::upsBasicOutputStatus,
            "2.2.1.0",  # PowerNet-MIB::upsAdvBatteryCapacity,
            "2.2.4.0",  # PowerNet-MIB::upsAdvBatteryReplaceIndicator,
            "2.2.6.0",  # PowerNet-MIB::upsAdvBatteryNumOfBadBattPacks,
            "2.2.3.0",  # PowerNet-MIB::upsAdvBatteryRunTimeRemaining,
            "7.2.6.0",  # PowerNet-MIB::upsAdvTestCalibrationResults
            "7.2.4.0",  # PowerNet-MIB::upsLastDiagnosticsDate
            "2.2.2.0",  # PowerNet-MIB::upsAdvBatteryTemperature,
            "2.2.9.0",  # PowerNet-MIB::upsAdvBatteryCurrent,
            "11.1.1.0",  # PowerNet-MIB::upsBasicStateOutputState
        ]),
    "snmp_scan_function": lambda oid: oid(".1.3.6.1.2.1.1.2.0").startswith(".1.3.6.1.4.1.318.1.3"),
    "has_perfdata": True,
    "group": "apc_symentra",
    "default_levels_variable": "apc_default_levels",
}

#.
#   .--temperature---------------------------------------------------------.
#   |      _                                      _                        |
#   |     | |_ ___ _ __ ___  _ __   ___ _ __ __ _| |_ _   _ _ __ ___       |
#   |     | __/ _ \ '_ ` _ \| '_ \ / _ \ '__/ _` | __| | | | '__/ _ \      |
#   |     | ||  __/ | | | | | |_) |  __/ | | (_| | |_| |_| | | |  __/      |
#   |      \__\___|_| |_| |_| .__/ \___|_|  \__,_|\__|\__,_|_|  \___|      |
#   |                       |_|                                            |
#   '----------------------------------------------------------------------'

# Temperature default now 60C: regadring to a apc technician a temperature up tp 70C is possible
factory_settings["apc_symmetra_temp_default_levels"] = {"levels": (50, 60)}


def inventory_apc_symmetra_temp(parsed):
    if "temp" in parsed:
        return [("Battery", {})]


def check_apc_symmetra_temp(item, params, parsed):
    return check_temperature(parsed["temp"], params, "check_apc_symmetra_temp.%s" % item)


check_info['apc_symmetra.temp'] = {
    'inventory_function': inventory_apc_symmetra_temp,
    'check_function': check_apc_symmetra_temp,
    'service_description': "Temperature %s",
    'default_levels_variable': 'apc_symmetra_temp_default_levels',
    'has_perfdata': True,
    'group': 'temperature',
    'includes': ['temperature.include'],
}

#.
#   .--el phase------------------------------------------------------------.
#   |                      _         _                                     |
#   |                  ___| |  _ __ | |__   __ _ ___  ___                  |
#   |                 / _ \ | | '_ \| '_ \ / _` / __|/ _ \                 |
#   |                |  __/ | | |_) | | | | (_| \__ \  __/                 |
#   |                 \___|_| | .__/|_| |_|\__,_|___/\___|                 |
#   |                         |_|                                          |
#   '----------------------------------------------------------------------'

factory_settings["apc_symmetra_elphase_default_levels"] = {"current": (1, 1)}


def inventory_apc_symmetra_elphase(parsed):
    for phase in parsed.get("elphase", {}):
        yield phase, {}


def check_apc_symmetra_elphase(item, params, parsed):
    return check_elphase(item, params, parsed.get("elphase", {}))


check_info['apc_symmetra.elphase'] = {
    'inventory_function': inventory_apc_symmetra_elphase,
    'check_function': check_apc_symmetra_elphase,
    'service_description': "Phase %s",
    'has_perfdata': True,
    'default_levels_variable': 'apc_symmetra_elphase_default_levels',
    'group': 'ups_outphase',
    'includes': ['elphase.include'],
}
