From ec178060838f2cfa2d321a38c19f42b9dac236db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lorenz=20K=C3=A4stle?= Date: Wed, 29 Apr 2026 10:44:27 +0200 Subject: [PATCH 01/15] Autoformatting --- check_sensorProbe2plus.py | 160 ++++++++++++++++++++++++++------------ 1 file changed, 110 insertions(+), 50 deletions(-) diff --git a/check_sensorProbe2plus.py b/check_sensorProbe2plus.py index 45cd6f9..3806238 100755 --- a/check_sensorProbe2plus.py +++ b/check_sensorProbe2plus.py @@ -24,9 +24,10 @@ import argparse import sys -from pysnmp.entity.rfc3413.oneliner import cmdgen from enum import Enum, IntEnum +from pysnmp.entity.rfc3413.oneliner import cmdgen + # Translate the OID indexes to keywords class Types(IntEnum): @@ -67,7 +68,7 @@ class NagiosState(Enum): 22: "Power", 24: "Fuel", 26: "Tank sender", - 27: "Door" + 27: "Door", } @@ -106,21 +107,36 @@ def print_status_message(sensor_states, perf_data): result_message = "" if len(sensor_states["WARNING"]) > 0 and len(sensor_states["CRITICAL"]) > 0: - result_message = "CRITICAL sensorProbe2plus: Sensor reports state CRITICAL for %d sensor%s (%s) " \ - "and state WARNING for %d sensor%s (%s)" % ( - len(sensor_states["CRITICAL"]), - "s" if len(sensor_states["CRITICAL"]) > 1 else "", - critical_name_string, - len(sensor_states["WARNING"]), - "s" if len(sensor_states["WARNING"]) > 1 else "", - warning_name_string - ) + result_message = ( + "CRITICAL sensorProbe2plus: Sensor reports state CRITICAL for %d sensor%s (%s) " + "and state WARNING for %d sensor%s (%s)" + % ( + len(sensor_states["CRITICAL"]), + "s" if len(sensor_states["CRITICAL"]) > 1 else "", + critical_name_string, + len(sensor_states["WARNING"]), + "s" if len(sensor_states["WARNING"]) > 1 else "", + warning_name_string, + ) + ) elif len(sensor_states["WARNING"]) > 0: - result_message = "WARNING sensorProbe2plus: Sensor reports state WARNING for %d sensor%s (%s)" % ( - len(sensor_states["WARNING"]), "s" if len(sensor_states["WARNING"]) > 1 else "", warning_name_string) + result_message = ( + "WARNING sensorProbe2plus: Sensor reports state WARNING for %d sensor%s (%s)" + % ( + len(sensor_states["WARNING"]), + "s" if len(sensor_states["WARNING"]) > 1 else "", + warning_name_string, + ) + ) elif len(sensor_states["CRITICAL"]) > 0: - result_message = "CRITICAL sensorProbe2plus: Sensor reports state CRITICAL for %d sensor%s (%s)" % ( - len(sensor_states["CRITICAL"]), "s" if len(sensor_states["CRITICAL"]) > 1 else "", critical_name_string) + result_message = ( + "CRITICAL sensorProbe2plus: Sensor reports state CRITICAL for %d sensor%s (%s)" + % ( + len(sensor_states["CRITICAL"]), + "s" if len(sensor_states["CRITICAL"]) > 1 else "", + critical_name_string, + ) + ) else: result_message = "OK sensorProbe2plus: Sensor reports that everything is fine" @@ -143,13 +159,29 @@ def print_status_message(sensor_states, perf_data): port = 0 # Arguments for the CLI command -parser = argparse.ArgumentParser(description='Check plugin for AKCP SensorProbe2+') +parser = argparse.ArgumentParser(description="Check plugin for AKCP SensorProbe2+") parser.add_argument("-V", "--version", action="store_true") -parser.add_argument("-v", "--verbose", action="count", default=0, help="increase output verbosity (-v or -vv)") -parser.add_argument("-p", "--port", help="port of the sensors to check (shows all if not set)", type=int, default=0) -required = parser.add_argument_group('required arguments') -required.add_argument("-H", "--hostname", help="host of the sensor probe", required=True) -required.add_argument("-C", "--community", help="read community of the sensor probe", required=True) +parser.add_argument( + "-v", + "--verbose", + action="count", + default=0, + help="increase output verbosity (-v or -vv)", +) +parser.add_argument( + "-p", + "--port", + help="port of the sensors to check (shows all if not set)", + type=int, + default=0, +) +required = parser.add_argument_group("required arguments") +required.add_argument( + "-H", "--hostname", help="host of the sensor probe", required=True +) +required.add_argument( + "-C", "--community", help="read community of the sensor probe", required=True +) args = parser.parse_args() @@ -182,18 +214,27 @@ def print_status_message(sensor_states, perf_data): generator = cmdgen.CommandGenerator() communityData = cmdgen.CommunityData(community) transport = cmdgen.UdpTransportTarget((hostname, 161)) -command = getattr(generator, 'nextCmd') +command = getattr(generator, "nextCmd") -errorIndication, errorStatus, errorIndex, result = command(communityData, transport, sensorsOID) +errorIndication, errorStatus, errorIndex, result = command( + communityData, transport, sensorsOID +) # Check if an exception occurred if errorIndication: print("%s sensorProbe2plus: %s" % (NagiosState.UNKNOWN.name, errorIndication)) mostImportantState = NagiosState.UNKNOWN elif errorStatus: - print(('%s sensorProbe2plus: %s at %s' % (NagiosState.CRITICAL.name, - errorStatus.prettyPrint(), - errorIndex and result[int(errorIndex)-1] or '?'))) + print( + ( + "%s sensorProbe2plus: %s at %s" + % ( + NagiosState.CRITICAL.name, + errorStatus.prettyPrint(), + errorIndex and result[int(errorIndex) - 1] or "?", + ) + ) + ) mostImportantState = NagiosState.CRITICAL else: # Sort results @@ -233,7 +274,10 @@ def print_status_message(sensor_states, perf_data): # Check if there is no sensor on the given port if len(sensorPorts) < 1: - print("%s sensorProbe2plus: There is no sensor on the given port" % NagiosState.UNKNOWN.name) + print( + "%s sensorProbe2plus: There is no sensor on the given port" + % NagiosState.UNKNOWN.name + ) sys.exit(NagiosState.UNKNOWN.value) # Sensor names sorted by state @@ -246,7 +290,7 @@ def print_status_message(sensor_states, perf_data): state = convert_state_to_nagios(valueIndexes[Types.STATE]) # Redetermines most important state - if hasattr(state, 'value') and state.value > mostImportantState.value: + if hasattr(state, "value") and state.value > mostImportantState.value: mostImportantState = state else: mostImportantState = NagiosState.UNKNOWN @@ -260,8 +304,7 @@ def print_status_message(sensor_states, perf_data): value = 0 if state == NagiosState.OK else 1 # Status message for sensor - stateMessages.append( - "%s %s" % (state.name, valueIndexes[Types.NAME])) + stateMessages.append("%s %s" % (state.name, valueIndexes[Types.NAME])) # Add performance data to performance data array perfData.append("'%s'=%s;" % (valueIndexes[Types.NAME], value)) @@ -269,35 +312,52 @@ def print_status_message(sensor_states, perf_data): # Convert temperatures into right format if valueIndexes[Types.UNIT] == "C": valueIndexes[Types.VALUE] = float(valueIndexes[Types.VALUE]) / 10 - valueIndexes[Types.LOW_CRITICAL] = float(valueIndexes[Types.LOW_CRITICAL]) / 10 - valueIndexes[Types.LOW_WARNING] = float(valueIndexes[Types.LOW_WARNING]) / 10 - valueIndexes[Types.HIGH_WARNING] = float(valueIndexes[Types.HIGH_WARNING]) / 10 - valueIndexes[Types.HIGH_CRITICAL] = float(valueIndexes[Types.HIGH_CRITICAL]) / 10 + valueIndexes[Types.LOW_CRITICAL] = ( + float(valueIndexes[Types.LOW_CRITICAL]) / 10 + ) + valueIndexes[Types.LOW_WARNING] = ( + float(valueIndexes[Types.LOW_WARNING]) / 10 + ) + valueIndexes[Types.HIGH_WARNING] = ( + float(valueIndexes[Types.HIGH_WARNING]) / 10 + ) + valueIndexes[Types.HIGH_CRITICAL] = ( + float(valueIndexes[Types.HIGH_CRITICAL]) / 10 + ) # Status message for sensor - stateMessage = '%s %s sensor "%s": %s%s' % (state.name, - valueIndexes[Types.CATEGORY], - valueIndexes[Types.NAME], - valueIndexes[Types.VALUE], - valueIndexes[Types.UNIT]) + stateMessage = '%s %s sensor "%s": %s%s' % ( + state.name, + valueIndexes[Types.CATEGORY], + valueIndexes[Types.NAME], + valueIndexes[Types.VALUE], + valueIndexes[Types.UNIT], + ) # Add thresholds to verbose sensor messages if verbose > 1: - stateMessage += " (%s:%s/%s:%s)" % (valueIndexes[Types.LOW_WARNING], - valueIndexes[Types.HIGH_WARNING], - valueIndexes[Types.LOW_CRITICAL], - valueIndexes[Types.HIGH_CRITICAL]) + stateMessage += " (%s:%s/%s:%s)" % ( + valueIndexes[Types.LOW_WARNING], + valueIndexes[Types.HIGH_WARNING], + valueIndexes[Types.LOW_CRITICAL], + valueIndexes[Types.HIGH_CRITICAL], + ) stateMessages.append(stateMessage) # Add performance data to performance data array - perfData.append("'%s'=%s%s;%s:%s;%s:%s" % (valueIndexes[Types.NAME], - valueIndexes[Types.VALUE], - valueIndexes[Types.UNIT], - valueIndexes[Types.LOW_WARNING], - valueIndexes[Types.HIGH_WARNING], - valueIndexes[Types.LOW_CRITICAL], - valueIndexes[Types.HIGH_CRITICAL])) + perfData.append( + "'%s'=%s%s;%s:%s;%s:%s" + % ( + valueIndexes[Types.NAME], + valueIndexes[Types.VALUE], + valueIndexes[Types.UNIT], + valueIndexes[Types.LOW_WARNING], + valueIndexes[Types.HIGH_WARNING], + valueIndexes[Types.LOW_CRITICAL], + valueIndexes[Types.HIGH_CRITICAL], + ) + ) # Print first line of output print_status_message(namesByState, perfData) From cd6226b6b12c5a04ac05659f781d8d608f744558 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lorenz=20K=C3=A4stle?= Date: Wed, 29 Apr 2026 10:50:27 +0200 Subject: [PATCH 02/15] Remove version and authors, both are retrieved via git --- check_sensorProbe2plus.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/check_sensorProbe2plus.py b/check_sensorProbe2plus.py index 3806238..ff3d4de 100755 --- a/check_sensorProbe2plus.py +++ b/check_sensorProbe2plus.py @@ -2,10 +2,6 @@ # ------------------------------------------------------------------------------ # check_sensorProbe2plus.py - A check plugin for AKCP SensorProbe2+. # Copyright (C) 2017 NETWAYS GmbH, www.netways.de -# Authors: Noah Hilverling -# Jennifer Mourek -# -# Version: 1.0 # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License From 022ca7bf82fb6b80d96657a8de33b7e609463c84 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lorenz=20K=C3=A4stle?= Date: Wed, 29 Apr 2026 10:50:49 +0200 Subject: [PATCH 03/15] Add some type hints --- check_sensorProbe2plus.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/check_sensorProbe2plus.py b/check_sensorProbe2plus.py index ff3d4de..699ff73 100755 --- a/check_sensorProbe2plus.py +++ b/check_sensorProbe2plus.py @@ -20,6 +20,7 @@ import argparse import sys +import typing from enum import Enum, IntEnum from pysnmp.entity.rfc3413.oneliner import cmdgen @@ -202,7 +203,7 @@ def print_status_message(sensor_states, perf_data): perfData = [] # Root for sensor dictionary tree -sensorPorts = {} +sensorPorts: dict[int, typing.Any] = {} # Root for sensor OIDs sensorsOID = (1, 3, 6, 1, 4, 1, 3854, 3, 5) @@ -277,7 +278,12 @@ def print_status_message(sensor_states, perf_data): sys.exit(NagiosState.UNKNOWN.value) # Sensor names sorted by state - namesByState = {"OK": [], "WARNING": [], "CRITICAL": [], "UNKNOWN": []} + namesByState: dict[str, list] = { + "OK": [], + "WARNING": [], + "CRITICAL": [], + "UNKNOWN": [], + } # Iterate through sensors for sensorPort, sensorIndexes in sensorPorts.items(): From e5d244c12a856d2f74af44fde8caa328b4c02c03 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lorenz=20K=C3=A4stle?= Date: Wed, 29 Apr 2026 10:51:03 +0200 Subject: [PATCH 04/15] remove unused variable --- check_sensorProbe2plus.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/check_sensorProbe2plus.py b/check_sensorProbe2plus.py index 699ff73..27f2c82 100755 --- a/check_sensorProbe2plus.py +++ b/check_sensorProbe2plus.py @@ -243,7 +243,7 @@ def print_status_message(sensor_states, perf_data): valueIndex = int(oid[11]) try: Types(valueIndex) - except ValueError as err: + except ValueError: continue # Get sensor category From 0dc360492773cb8aa2bb23f6af1f9939f4ce4f73 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lorenz=20K=C3=A4stle?= Date: Wed, 29 Apr 2026 11:13:25 +0200 Subject: [PATCH 05/15] Use stdlib enums instead of enum34 --- requirements.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index a748911..84c1b3d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1 @@ -enum34==1.1.10 pysnmp==4.4.12 From 6ca404ba1d7c0e36fbd652f5e96e928fa9436db5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lorenz=20K=C3=A4stle?= Date: Wed, 29 Apr 2026 11:16:08 +0200 Subject: [PATCH 06/15] add basic CI stuff --- .github/ISSUE_TEMPLATE/bug_report.yaml | 27 +++++++++++++++++++++ .github/ISSUE_TEMPLATE/documentation.yaml | 10 ++++++++ .github/ISSUE_TEMPLATE/feature_request.yaml | 15 ++++++++++++ .github/ISSUE_TEMPLATE/question.yaml | 10 ++++++++ .github/dependabot.yml | 6 +++++ .github/workflows/unittest.yml | 20 +++++++++++++++ Makefile | 4 +++ 7 files changed, 92 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/bug_report.yaml create mode 100644 .github/ISSUE_TEMPLATE/documentation.yaml create mode 100644 .github/ISSUE_TEMPLATE/feature_request.yaml create mode 100644 .github/ISSUE_TEMPLATE/question.yaml create mode 100644 .github/dependabot.yml create mode 100644 .github/workflows/unittest.yml create mode 100644 Makefile diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml new file mode 100644 index 0000000..6adb01d --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yaml @@ -0,0 +1,27 @@ +name: Bug Report +description: File a bug report +title: "[Bug]: " +labels: ["bug", "needs-triage"] +body: + - type: checkboxes + id: terms + attributes: + label: Please try to fill out as much of the information below as you can. Thank you! + options: + - label: Yes, I've searched similar issues on GitHub and didn't find any. + required: true + - type: input + id: app_version + attributes: + label: Which version contains the bug? + placeholder: 1.0.0 + - type: textarea + id: description + attributes: + label: Describe the bug + description: Please provide a concise description of the bug, add any relevant output or error messages. You can use markdown. + - type: textarea + id: recreate + attributes: + label: How to recreate the bug? + description: Please provide the steps to recreate the issue. diff --git a/.github/ISSUE_TEMPLATE/documentation.yaml b/.github/ISSUE_TEMPLATE/documentation.yaml new file mode 100644 index 0000000..088b14f --- /dev/null +++ b/.github/ISSUE_TEMPLATE/documentation.yaml @@ -0,0 +1,10 @@ +name: Documentation +description: Suggest documentation improvements +title: "[Documentation]: " +labels: ["documentation"] +body: + - type: textarea + id: description + attributes: + label: Describe the improvements you'd like. + description: Please provide as much context as possible. You can use markdown. diff --git a/.github/ISSUE_TEMPLATE/feature_request.yaml b/.github/ISSUE_TEMPLATE/feature_request.yaml new file mode 100644 index 0000000..12c9e2e --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yaml @@ -0,0 +1,15 @@ +name: Feature Request +description: Request a feature or enhancement +title: "[Feature]: " +labels: ["feature", "needs-triage"] +body: + - type: markdown + attributes: + value: | + Please try to fill out as much of the information below as you can. Thank you! + **Note:** If you want to sponsor new features, contact us at info@netways.de + - type: textarea + id: description + attributes: + label: Describe the feature request + description: Please provide a concise description of the feature. You can use markdown. diff --git a/.github/ISSUE_TEMPLATE/question.yaml b/.github/ISSUE_TEMPLATE/question.yaml new file mode 100644 index 0000000..65183ea --- /dev/null +++ b/.github/ISSUE_TEMPLATE/question.yaml @@ -0,0 +1,10 @@ +name: Question +description: Ask a question +title: "[Question]: " +labels: ["question"] +body: + - type: textarea + id: description + attributes: + label: Ask a question + description: Please provide as much context as possible. You can use markdown. diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..ad45155 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,6 @@ +version: 2 +updates: +- package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: monthly diff --git a/.github/workflows/unittest.yml b/.github/workflows/unittest.yml new file mode 100644 index 0000000..20eb257 --- /dev/null +++ b/.github/workflows/unittest.yml @@ -0,0 +1,20 @@ +name: CI + +on: [push, pull_request] + +jobs: + gitHubActionForPytest: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: [3.10, 3.11, 3.12, 3.13, 3.14] + name: GitHub Action + steps: + - name: Checkout + uses: actions/checkout@v6 + - name: Install dependencies + run: | + python -m pip install -r requirements.txt -r requirements-dev.txt + - name: Lint + run: | + make lint diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..3032f34 --- /dev/null +++ b/Makefile @@ -0,0 +1,4 @@ +.PHONY: lint + +lint: + python -m pylint check_sensorProbe2plus.py From d8f33a7430fd3dd3415fd69f86aaa440a363e245 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lorenz=20K=C3=A4stle?= Date: Wed, 29 Apr 2026 14:42:25 +0200 Subject: [PATCH 07/15] Refactor, fix linter warnings, update requirements --- Makefile | 2 +- check_sensorProbe2plus.py | 505 +++++++++++++++++++++----------------- requirements.txt | 2 +- 3 files changed, 276 insertions(+), 233 deletions(-) diff --git a/Makefile b/Makefile index 3032f34..b399dfd 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ .PHONY: lint lint: - python -m pylint check_sensorProbe2plus.py + python -m pylint check_sensorProbe2plus.py --disable=invalid-name diff --git a/check_sensorProbe2plus.py b/check_sensorProbe2plus.py index 27f2c82..c7259b8 100755 --- a/check_sensorProbe2plus.py +++ b/check_sensorProbe2plus.py @@ -18,16 +18,35 @@ # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. # ------------------------------------------------------------------------------ +""" +check_sensorProbe2plus.py is a Check Plugin for Nagios similar monitoring systems +like Icinga or Nameon. +It retrieves data from a AKCP SensorProbe2+ devices and alerts on problematic +conditions +""" + import argparse +import asyncio as other_asyncio_name import sys import typing from enum import Enum, IntEnum -from pysnmp.entity.rfc3413.oneliner import cmdgen +import pysnmp +from pysnmp.hlapi.v3arch.asyncio import SnmpEngine as pySnmp_engine +from pysnmp.hlapi.v3arch.asyncio import next_cmd as pySnmp_next_cmd + +# Version number +VERSION = 1.1 + +# Root for sensor OIDs +SENSORS_OID = (1, 3, 6, 1, 4, 1, 3854, 3, 5) + +# pylint: disable=consider-using-f-string -# Translate the OID indexes to keywords class Types(IntEnum): + """Translate the OID indexes to keywords""" + CATEGORY = 0 NAME = 2 UNIT = 5 @@ -39,8 +58,9 @@ class Types(IntEnum): VALUE = 20 -# Translate the Nagios state IDs to keywords class NagiosState(Enum): + """Translate the Nagios state IDs to keywords""" + OK = 0 WARNING = 1 CRITICAL = 2 @@ -69,27 +89,31 @@ class NagiosState(Enum): } -# Convert a state from AKCP format to Nagios def convert_state_to_nagios(state_to_convert): + """ + Convert a state from AKCP format to Nagios + """ state_to_convert = int(state_to_convert) if state_to_convert == 1: return NagiosState.UNKNOWN - elif state_to_convert == 2: + if state_to_convert == 2: return NagiosState.OK - elif state_to_convert == 3 or state_to_convert == 5: + if state_to_convert in (3, 5): return NagiosState.WARNING - elif state_to_convert == 4 or state_to_convert == 6: + if state_to_convert in (4, 6): return NagiosState.CRITICAL - else: - print("State %s is out of range. That should not happen." % state_to_convert) - return NagiosState.UNKNOWN + + print("State %s is out of range. That should not happen." % state_to_convert) + return NagiosState.UNKNOWN -# Display a one line status message with: -# - most important Nagios state and short message -# - number and name of the sensors in any state but OK -# - thresholds of the sensors def print_status_message(sensor_states, perf_data): + """ + Display a one line status message with: + - most important Nagios state and short message + - number and name of the sensors in any state but OK + - thresholds of the sensors + """ warning_name_string = "" for name in sensor_states["WARNING"]: if warning_name_string: @@ -105,9 +129,8 @@ def print_status_message(sensor_states, perf_data): result_message = "" if len(sensor_states["WARNING"]) > 0 and len(sensor_states["CRITICAL"]) > 0: result_message = ( - "CRITICAL sensorProbe2plus: Sensor reports state CRITICAL for %d sensor%s (%s) " - "and state WARNING for %d sensor%s (%s)" - % ( + "CRITICAL sensorProbe2plus: Sensor reports state CRITICAL for {} sensor{} ({}) " + "and state WARNING for {} sensor{} ({})".format( len(sensor_states["CRITICAL"]), "s" if len(sensor_states["CRITICAL"]) > 1 else "", critical_name_string, @@ -139,235 +162,255 @@ def print_status_message(sensor_states, perf_data): # Add the performance data to the end of the first output line result_message += "|" - for singlePerfData in perf_data: - result_message += singlePerfData + " " + for single_perfdata in perf_data: + result_message += single_perfdata + " " # Print summary and performance data print(result_message) -# Version number -version = 1.0 - -# Initialise variables -verbose = 0 -hostname = "" -community = "" -port = 0 - -# Arguments for the CLI command -parser = argparse.ArgumentParser(description="Check plugin for AKCP SensorProbe2+") -parser.add_argument("-V", "--version", action="store_true") -parser.add_argument( - "-v", - "--verbose", - action="count", - default=0, - help="increase output verbosity (-v or -vv)", -) -parser.add_argument( - "-p", - "--port", - help="port of the sensors to check (shows all if not set)", - type=int, - default=0, -) -required = parser.add_argument_group("required arguments") -required.add_argument( - "-H", "--hostname", help="host of the sensor probe", required=True -) -required.add_argument( - "-C", "--community", help="read community of the sensor probe", required=True -) - -args = parser.parse_args() - -# Print version if version argument is given -if args.version: - print("AKCP SensorProbe2+ Version %s" % version) - sys.exit() -else: - # Assert arguments to their variables - verbose = args.verbose if args.verbose <= 2 else 2 - hostname = args.hostname - community = args.community - port = args.port - -# The state with the highest importance (CRITICAL -> WARNING -> OK) -mostImportantState = NagiosState.OK - -# Array of messages to print after first line if verbose -stateMessages = [] - -# Performance data for each sensor as string -perfData = [] - -# Root for sensor dictionary tree -sensorPorts: dict[int, typing.Any] = {} +def parse_args() -> tuple[int, str, str, int]: + """ + Parse CLI arguments + """ + parser = argparse.ArgumentParser(description="Check plugin for AKCP SensorProbe2+") + parser.add_argument("-V", "--version", action="store_true") + parser.add_argument( + "-v", + "--verbose", + action="count", + default=0, + help="increase output verbosity (-v or -vv)", + ) + parser.add_argument( + "-p", + "--port", + help="port of the sensors to check (shows all if not set)", + type=int, + default=0, + ) + required = parser.add_argument_group("required arguments") + required.add_argument( + "-H", "--hostname", help="host of the sensor probe", required=True + ) + required.add_argument( + "-C", "--community", help="read community of the sensor probe", required=True + ) -# Root for sensor OIDs -sensorsOID = (1, 3, 6, 1, 4, 1, 3854, 3, 5) - -generator = cmdgen.CommandGenerator() -communityData = cmdgen.CommunityData(community) -transport = cmdgen.UdpTransportTarget((hostname, 161)) -command = getattr(generator, "nextCmd") - -errorIndication, errorStatus, errorIndex, result = command( - communityData, transport, sensorsOID -) - -# Check if an exception occurred -if errorIndication: - print("%s sensorProbe2plus: %s" % (NagiosState.UNKNOWN.name, errorIndication)) - mostImportantState = NagiosState.UNKNOWN -elif errorStatus: - print( - ( - "%s sensorProbe2plus: %s at %s" - % ( - NagiosState.CRITICAL.name, - errorStatus.prettyPrint(), - errorIndex and result[int(errorIndex) - 1] or "?", - ) - ) + args = parser.parse_args() + + # Print version if version argument is given + if args.version: + print("check_sensorProbe2plus version %s" % VERSION) + sys.exit() + else: + # Assert arguments to their variables + verbose = args.verbose if args.verbose <= 2 else 2 + hostname = args.hostname + community = args.community + port = args.port + return (verbose, hostname, community, port) + + +def execute(hostname, sensor_port_input, community, verbose) -> None: + """ + Execute the actual test + """ + # Array of messages to print after first line if verbose + state_messages = [] + + # Performance data for each sensor as string + perfdata = [] + + # Root for sensor dictionary tree + sensor_ports: dict[int, typing.Any] = {} + + error_indication, error_status, error_index, result = other_asyncio_name.run( + snmp_query(hostname, 161, SENSORS_OID, community) ) - mostImportantState = NagiosState.CRITICAL -else: - # Sort results - for data in result: - oid = data[0][0] - value = data[0][1] - - # Filter relevant OIDs - valueIndex = int(oid[11]) - try: - Types(valueIndex) - except ValueError: - continue - - # Get sensor category - category = int(oid[9]) - if category == 1 or category > 27: - continue - - # Filter ports if port is given in arguments - sensorPort = int(oid[15]) - if args.port != 0 and args.port - 1 != sensorPort: - continue - - # Numeric index of sensor - sensorIndex = int(oid[16]) - - # Add needed dictionaries if not yet existing - if sensorPort not in sensorPorts: - sensorPorts[sensorPort] = {} - if sensorIndex not in sensorPorts[sensorPort]: - sensorPorts[sensorPort][sensorIndex] = {} - - # Store data in dictionary tree - sensorPorts[sensorPort][sensorIndex][valueIndex] = value - sensorPorts[sensorPort][sensorIndex][Types.CATEGORY] = categories[category] - - # Check if there is no sensor on the given port - if len(sensorPorts) < 1: + + # The state with the highest importance (CRITICAL -> WARNING -> OK) + most_important_state = NagiosState.OK + + if error_indication: + print("%s sensorProbe2plus: %s" % (NagiosState.UNKNOWN.name, error_indication)) + most_important_state = NagiosState.UNKNOWN + elif error_status: print( - "%s sensorProbe2plus: There is no sensor on the given port" - % NagiosState.UNKNOWN.name + ( + "%s sensorProbe2plus: %s at %s" + % ( + NagiosState.CRITICAL.name, + error_status.prettyPrint(), + error_index and result[int(error_index) - 1] or "?", + ) + ) ) - sys.exit(NagiosState.UNKNOWN.value) - - # Sensor names sorted by state - namesByState: dict[str, list] = { - "OK": [], - "WARNING": [], - "CRITICAL": [], - "UNKNOWN": [], - } - - # Iterate through sensors - for sensorPort, sensorIndexes in sensorPorts.items(): - for sensorIndex, valueIndexes in sensorIndexes.items(): - # Convert state to Nagios states - state = convert_state_to_nagios(valueIndexes[Types.STATE]) - - # Redetermines most important state - if hasattr(state, "value") and state.value > mostImportantState.value: - mostImportantState = state - else: - mostImportantState = NagiosState.UNKNOWN - - # Sort sensor name by state - namesByState[state.name].append(valueIndexes[Types.NAME]) - - # Check if sensor has no value - if Types.VALUE not in valueIndexes: - # Value replacement for sensors without value - value = 0 if state == NagiosState.OK else 1 - - # Status message for sensor - stateMessages.append("%s %s" % (state.name, valueIndexes[Types.NAME])) - - # Add performance data to performance data array - perfData.append("'%s'=%s;" % (valueIndexes[Types.NAME], value)) - else: - # Convert temperatures into right format - if valueIndexes[Types.UNIT] == "C": - valueIndexes[Types.VALUE] = float(valueIndexes[Types.VALUE]) / 10 - valueIndexes[Types.LOW_CRITICAL] = ( - float(valueIndexes[Types.LOW_CRITICAL]) / 10 - ) - valueIndexes[Types.LOW_WARNING] = ( - float(valueIndexes[Types.LOW_WARNING]) / 10 + most_important_state = NagiosState.CRITICAL + else: + # Sort results + for data in result: + oid = data[0][0] + value = data[0][1] + + # Filter relevant OIDs + value_index = int(oid[11]) + try: + Types(value_index) + except ValueError: + continue + + # Get sensor category + category = int(oid[9]) + if category == 1 or category > 27: + continue + + # Filter ports if port is given in arguments + sensor_port = int(oid[15]) + if sensor_port_input != 0 and sensor_port_input - 1 != sensor_port: + continue + + # Numeric index of sensor + sensor_index = int(oid[16]) + + # Add needed dictionaries if not yet existing + if sensor_port not in sensor_ports: + sensor_ports[sensor_port] = {} + if sensor_index not in sensor_ports[sensor_port]: + sensor_ports[sensor_port][sensor_index] = {} + + # Store data in dictionary tree + sensor_ports[sensor_port][sensor_index][value_index] = value + sensor_ports[sensor_port][sensor_index][Types.CATEGORY] = categories[ + category + ] + + # Check if there is no sensor on the given port + if len(sensor_ports) < 1: + print( + "%s sensorProbe2plus: There is no sensor on the given port" + % NagiosState.UNKNOWN.name + ) + sys.exit(NagiosState.UNKNOWN.value) + + # Sensor names sorted by state + names_by_state: dict[str, list] = { + "OK": [], + "WARNING": [], + "CRITICAL": [], + "UNKNOWN": [], + } + + # Iterate through sensors + for sensor_port, sensor_indexes in sensor_ports.items(): + for sensor_index, value_indexes in sensor_indexes.items(): + # Convert state to Nagios states + state = convert_state_to_nagios(value_indexes[Types.STATE]) + + # Redetermines most important state + if hasattr(state, "value") and state.value > most_important_state.value: + most_important_state = state + else: + most_important_state = NagiosState.UNKNOWN + + # Sort sensor name by state + names_by_state[state.name].append(value_indexes[Types.NAME]) + + # Check if sensor has no value + if Types.VALUE not in value_indexes: + # Value replacement for sensors without value + value = 0 if state == NagiosState.OK else 1 + + # Status message for sensor + state_messages.append( + "%s %s" % (state.name, value_indexes[Types.NAME]) ) - valueIndexes[Types.HIGH_WARNING] = ( - float(valueIndexes[Types.HIGH_WARNING]) / 10 + + # Add performance data to performance data array + perfdata.append("'%s'=%s;" % (value_indexes[Types.NAME], value)) + else: + # Convert temperatures into right format + if value_indexes[Types.UNIT] == "C": + value_indexes[Types.VALUE] = ( + float(value_indexes[Types.VALUE]) / 10 + ) + value_indexes[Types.LOW_CRITICAL] = ( + float(value_indexes[Types.LOW_CRITICAL]) / 10 + ) + value_indexes[Types.LOW_WARNING] = ( + float(value_indexes[Types.LOW_WARNING]) / 10 + ) + value_indexes[Types.HIGH_WARNING] = ( + float(value_indexes[Types.HIGH_WARNING]) / 10 + ) + value_indexes[Types.HIGH_CRITICAL] = ( + float(value_indexes[Types.HIGH_CRITICAL]) / 10 + ) + + # Status message for sensor + state_message = '%s %s sensor "%s": %s%s' % ( + state.name, + value_indexes[Types.CATEGORY], + value_indexes[Types.NAME], + value_indexes[Types.VALUE], + value_indexes[Types.UNIT], ) - valueIndexes[Types.HIGH_CRITICAL] = ( - float(valueIndexes[Types.HIGH_CRITICAL]) / 10 + + # Add thresholds to verbose sensor messages + if verbose > 1: + state_message += " (%s:%s/%s:%s)" % ( + value_indexes[Types.LOW_WARNING], + value_indexes[Types.HIGH_WARNING], + value_indexes[Types.LOW_CRITICAL], + value_indexes[Types.HIGH_CRITICAL], + ) + + state_messages.append(state_message) + + # Add performance data to performance data array + perfdata.append( + "'%s'=%s%s;%s:%s;%s:%s" + % ( + value_indexes[Types.NAME], + value_indexes[Types.VALUE], + value_indexes[Types.UNIT], + value_indexes[Types.LOW_WARNING], + value_indexes[Types.HIGH_WARNING], + value_indexes[Types.LOW_CRITICAL], + value_indexes[Types.HIGH_CRITICAL], + ) ) - # Status message for sensor - stateMessage = '%s %s sensor "%s": %s%s' % ( - state.name, - valueIndexes[Types.CATEGORY], - valueIndexes[Types.NAME], - valueIndexes[Types.VALUE], - valueIndexes[Types.UNIT], - ) + # Print first line of output + print_status_message(names_by_state, perfdata) - # Add thresholds to verbose sensor messages - if verbose > 1: - stateMessage += " (%s:%s/%s:%s)" % ( - valueIndexes[Types.LOW_WARNING], - valueIndexes[Types.HIGH_WARNING], - valueIndexes[Types.LOW_CRITICAL], - valueIndexes[Types.HIGH_CRITICAL], - ) + # Add additional information for each sensor to the output if verbose + if verbose > 0: + for message in state_messages: + print(message) - stateMessages.append(stateMessage) - - # Add performance data to performance data array - perfData.append( - "'%s'=%s%s;%s:%s;%s:%s" - % ( - valueIndexes[Types.NAME], - valueIndexes[Types.VALUE], - valueIndexes[Types.UNIT], - valueIndexes[Types.LOW_WARNING], - valueIndexes[Types.HIGH_WARNING], - valueIndexes[Types.LOW_CRITICAL], - valueIndexes[Types.HIGH_CRITICAL], - ) - ) + # Exit with most important state + sys.exit(most_important_state.value) + + +async def snmp_query(hostname, port, oid, community): + """ + snmp_query executes the actual query + """ + snmp_engine = pySnmp_engine() + + snmp_object = pysnmp.smi.rfc1902.ObjectType(pysnmp.smi.rfc1902.ObjectIdentity(oid)) + error_indication, error_status, error_index, result = await pySnmp_next_cmd( + snmp_engine, + pysnmp.hlapi.v3arch.asyncio.auth.CommunityData(community), + await pysnmp.hlapi.v3arch.asyncio.UdpTransportTarget.create((hostname, port)), + pysnmp.hlapi.v3arch.asyncio.ContextData(), + snmp_object, + ) - # Print first line of output - print_status_message(namesByState, perfData) + return (error_indication, error_status, error_index, result) - # Add additional information for each sensor to the output if verbose - if verbose > 0: - for message in stateMessages: - print(message) - # Exit with most important state - sys.exit(mostImportantState.value) +if __name__ == "__main__": + verbose, hostname, community, sensor_port = parse_args() + execute(hostname, sensor_port, community, verbose) diff --git a/requirements.txt b/requirements.txt index 84c1b3d..abe87b3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1 @@ -pysnmp==4.4.12 +pysnmp==7.0.0 From ffc936905341ae24a1b4ad803bf849e8b7f36533 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lorenz=20K=C3=A4stle?= Date: Wed, 29 Apr 2026 14:44:22 +0200 Subject: [PATCH 08/15] Fix c&p error in CI file --- .github/workflows/unittest.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/unittest.yml b/.github/workflows/unittest.yml index 20eb257..89e34fd 100644 --- a/.github/workflows/unittest.yml +++ b/.github/workflows/unittest.yml @@ -14,7 +14,7 @@ jobs: uses: actions/checkout@v6 - name: Install dependencies run: | - python -m pip install -r requirements.txt -r requirements-dev.txt + python -m pip install -r requirements.txt - name: Lint run: | make lint From b72b8806f1f391ca4cd4e1aed3f05281d6813d3f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lorenz=20K=C3=A4stle?= Date: Wed, 29 Apr 2026 14:49:20 +0200 Subject: [PATCH 09/15] Revert "Fix c&p error in CI file" This reverts commit ffc936905341ae24a1b4ad803bf849e8b7f36533. --- .github/workflows/unittest.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/unittest.yml b/.github/workflows/unittest.yml index 89e34fd..20eb257 100644 --- a/.github/workflows/unittest.yml +++ b/.github/workflows/unittest.yml @@ -14,7 +14,7 @@ jobs: uses: actions/checkout@v6 - name: Install dependencies run: | - python -m pip install -r requirements.txt + python -m pip install -r requirements.txt -r requirements-dev.txt - name: Lint run: | make lint From 3dee0942ae0120f45cdf6b0a0a3382abf831c3d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lorenz=20K=C3=A4stle?= Date: Wed, 29 Apr 2026 14:50:01 +0200 Subject: [PATCH 10/15] Add dev requirements --- requirements-dev.txt | 1 + 1 file changed, 1 insertion(+) create mode 100644 requirements-dev.txt diff --git a/requirements-dev.txt b/requirements-dev.txt new file mode 100644 index 0000000..64151a6 --- /dev/null +++ b/requirements-dev.txt @@ -0,0 +1 @@ +pylint==3.3.7 From ac26964c5bd7d53846a828dbfc6de4d7e43ad768 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lorenz=20K=C3=A4stle?= Date: Wed, 29 Apr 2026 15:06:02 +0200 Subject: [PATCH 11/15] Add lots of linter exceptions --- .pylintrc | 15 +++++++++++++++ Makefile | 2 +- 2 files changed, 16 insertions(+), 1 deletion(-) create mode 100644 .pylintrc diff --git a/.pylintrc b/.pylintrc new file mode 100644 index 0000000..04b6c62 --- /dev/null +++ b/.pylintrc @@ -0,0 +1,15 @@ +# pylint config +[FORMAT] +good-names=s,r,e +[MESSAGES CONTROL] +disable=fixme, + invalid-name, + consider-using-f-string, + missing-module-docstring, + too-many-instance-attributes, + too-many-arguments, + too-many-branches, + too-many-locals, + too-many-statements, + redefined-outer-name, + no-member, diff --git a/Makefile b/Makefile index b399dfd..3032f34 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ .PHONY: lint lint: - python -m pylint check_sensorProbe2plus.py --disable=invalid-name + python -m pylint check_sensorProbe2plus.py From c8a268de68c236c89f85f30fe2f1c7a861d8ce45 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lorenz=20K=C3=A4stle?= Date: Wed, 29 Apr 2026 15:14:20 +0200 Subject: [PATCH 12/15] Disable more linters --- check_sensorProbe2plus.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/check_sensorProbe2plus.py b/check_sensorProbe2plus.py index c7259b8..50bccb8 100755 --- a/check_sensorProbe2plus.py +++ b/check_sensorProbe2plus.py @@ -32,6 +32,7 @@ from enum import Enum, IntEnum import pysnmp +# pylint: disable=import-error,no-name-in-module from pysnmp.hlapi.v3arch.asyncio import SnmpEngine as pySnmp_engine from pysnmp.hlapi.v3arch.asyncio import next_cmd as pySnmp_next_cmd @@ -400,6 +401,7 @@ async def snmp_query(hostname, port, oid, community): snmp_engine = pySnmp_engine() snmp_object = pysnmp.smi.rfc1902.ObjectType(pysnmp.smi.rfc1902.ObjectIdentity(oid)) + # pylint: disable=c-extension-no-member error_indication, error_status, error_index, result = await pySnmp_next_cmd( snmp_engine, pysnmp.hlapi.v3arch.asyncio.auth.CommunityData(community), From 164927f959fdc897ab14615d63445405139e1c06 Mon Sep 17 00:00:00 2001 From: Markus Opolka Date: Wed, 29 Apr 2026 15:29:52 +0200 Subject: [PATCH 13/15] Update .gitignore --- .gitignore | 65 ++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 63 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index 7da0c3b..ece1395 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,65 @@ -.idea -.venv +# Byte-compiled / optimized / DLL files __pycache__/ +*.py[cod] + +# C extensions +*.so + +# Distribution / packaging +.Python +env/ +venv/ +.venv/ +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +*.egg-info/ +.installed.cfg +*.egg + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*,cover + +# Translations +*.mo +*.pot + +# Django stuff: +*.log + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Editors +\#* +.\#* +.idea *.bak From 6176637c82a83cbdea2c675c63a15e5ffedcdced Mon Sep 17 00:00:00 2001 From: Markus Opolka Date: Wed, 29 Apr 2026 15:30:11 +0200 Subject: [PATCH 14/15] Update README and requirements.txt --- README.md | 40 ++++++++++++++++------------------------ requirements.txt | 2 +- 2 files changed, 17 insertions(+), 25 deletions(-) diff --git a/README.md b/README.md index 86f288b..8e511e0 100644 --- a/README.md +++ b/README.md @@ -1,35 +1,27 @@ -## check_sensorProbe2plus ## - -### Description ### +# check_sensorProbe2plus This plugin serves the purpose of receiving data from the SensorProbe2+ and checking its state. - -### Dependencies ### +## Dependencies + [PySNMP](https://github.com/etingof/pysnmp) -+ [enum34](https://pypi.org/project/enum34) -### Usage ### +## Usage ``` -check_sensorProbe2plus.py -H -C [-p] [-V] [-v] [-h] -``` +usage: check_sensorProbe2plus.py [-h] [-V] [-v] [-p PORT] -H HOSTNAME -C COMMUNITY -#### required arguments: #### +Check plugin for AKCP SensorProbe2+ -+ **HOSTNAME:** host of the SensorProbe2+ - `` -H, --hostname `` -+ **COMMUNITY:** read community of the SensorProbe2+ - `` -C, --community `` +options: + -h, --help show this help message and exit + -V, --version + -v, --verbose increase output verbosity (-v or -vv) + -p, --port PORT port of the sensors to check (shows all if not set) -#### optional arguments: #### - -+ **HELP** show the help message and exit - `` -h, --help `` -+ **VERSION** shows the current version of the check plugin - `` -V, --version `` -+ **VERBOSE** increases output verbosity (-v or -vv) - `` -v, --verbose `` -+ **PORT** port of the sensor to check (shows all if not set) - `` -p, --port `` +required arguments: + -H, --hostname HOSTNAME + host of the sensor probe + -C, --community COMMUNITY + read community of the sensor probe +``` diff --git a/requirements.txt b/requirements.txt index abe87b3..d39f41e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1 @@ -pysnmp==7.0.0 +pysnmp==7.1.26 From 8bc4f9c62a20c75f8ebc8999fb427c6e9c59c3dc Mon Sep 17 00:00:00 2001 From: Markus Opolka Date: Wed, 29 Apr 2026 15:42:15 +0200 Subject: [PATCH 15/15] Add some unittests --- .github/workflows/unittest.yml | 3 +++ Makefile | 7 ++++++- requirements-dev.txt | 1 + test_check_sensorProbe2plus.py | 36 ++++++++++++++++++++++++++++++++++ 4 files changed, 46 insertions(+), 1 deletion(-) create mode 100644 test_check_sensorProbe2plus.py diff --git a/.github/workflows/unittest.yml b/.github/workflows/unittest.yml index 20eb257..9666768 100644 --- a/.github/workflows/unittest.yml +++ b/.github/workflows/unittest.yml @@ -18,3 +18,6 @@ jobs: - name: Lint run: | make lint + - name: Test + run: | + make coverage diff --git a/Makefile b/Makefile index 3032f34..8014a40 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,9 @@ -.PHONY: lint +.PHONY: lint test coverage lint: python -m pylint check_sensorProbe2plus.py +test: + python -m unittest -v test_check_sensorProbe2plus.py +coverage: + python -m coverage run -m unittest -b test_check_sensorProbe2plus.py + python -m coverage report -m --include check_sensorProbe2plus.py diff --git a/requirements-dev.txt b/requirements-dev.txt index 64151a6..368140c 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1 +1,2 @@ pylint==3.3.7 +coverage==7.9.0 diff --git a/test_check_sensorProbe2plus.py b/test_check_sensorProbe2plus.py new file mode 100644 index 0000000..796df4b --- /dev/null +++ b/test_check_sensorProbe2plus.py @@ -0,0 +1,36 @@ +#!/usr/bin/env python3 + +import unittest +import unittest.mock as mock +import sys + +sys.path.append('..') + +from check_sensorProbe2plus import convert_state_to_nagios +from check_sensorProbe2plus import print_status_message + +from check_sensorProbe2plus import NagiosState + +class UtilTesting(unittest.TestCase): + + def test_state(self): + actual = convert_state_to_nagios(1) + expected = NagiosState.UNKNOWN + + self.assertEqual(actual, expected) + + @mock.patch('builtins.print') + def test_status_message(self, mock_print): + sensor_states = { + "OK": [], + "WARNING": [], + "CRITICAL": [], + "UNKNOWN": [], + } + + print_status_message(sensor_states, "perfdata") + + calls = [mock.call('OK sensorProbe2plus: Sensor reports that everything is fine|p e r f d a t a ')] + + mock_print.assert_has_calls(calls) +