2022-09-29 17:38:59 -04:00
|
|
|
#!/usr/bin/env python3
|
|
|
|
|
2022-11-29 18:26:02 -05:00
|
|
|
"""A check plugin for Nagios/Icinga/etc. that queries Home Assistant for the current state of entities."""
|
|
|
|
|
2022-09-29 17:38:59 -04:00
|
|
|
import argparse
|
2022-11-26 11:11:03 -05:00
|
|
|
import dataclasses
|
2022-09-29 17:38:59 -04:00
|
|
|
import logging
|
|
|
|
import sys
|
|
|
|
from urllib.parse import urljoin
|
2022-11-26 19:44:26 -05:00
|
|
|
from typing import Optional
|
2022-11-29 17:54:54 -05:00
|
|
|
from pathlib import Path
|
2022-09-29 17:38:59 -04:00
|
|
|
|
|
|
|
import requests
|
|
|
|
import nagiosplugin
|
|
|
|
|
|
|
|
_log = logging.getLogger("nagiosplugin")
|
|
|
|
|
|
|
|
|
2022-11-26 11:11:03 -05:00
|
|
|
@dataclasses.dataclass
|
|
|
|
class Entities(nagiosplugin.Resource):
|
|
|
|
url: str
|
|
|
|
token: str
|
2022-11-26 19:42:49 -05:00
|
|
|
device_class: str
|
2022-11-26 19:40:57 -05:00
|
|
|
min: float
|
|
|
|
max: float
|
2022-11-26 19:44:08 -05:00
|
|
|
filters: list[str]
|
2022-11-26 19:44:26 -05:00
|
|
|
attribute: Optional[str]
|
2022-11-29 15:42:08 -05:00
|
|
|
friendly_name: bool
|
2022-09-29 17:38:59 -04:00
|
|
|
|
|
|
|
def hass_get(self, endpoint: str) -> requests.Response:
|
|
|
|
headers = {
|
|
|
|
"Authorization": "Bearer " + self.token,
|
|
|
|
"Content-Type": "application/json",
|
|
|
|
}
|
|
|
|
|
|
|
|
r = requests.get(urljoin(self.url, endpoint), headers=headers)
|
|
|
|
if not r.ok:
|
|
|
|
raise Exception("Failed to query Home Assistant API: " + r.text)
|
|
|
|
|
|
|
|
return r.json()
|
|
|
|
|
|
|
|
def check_api(self):
|
|
|
|
message = self.hass_get("/api/").get("message")
|
|
|
|
if message != "API running.":
|
|
|
|
print("ERROR: " + message)
|
|
|
|
sys.exit(1)
|
|
|
|
else:
|
|
|
|
print("OK: " + message)
|
|
|
|
|
|
|
|
def probe(self):
|
|
|
|
response = self.hass_get("/api/states")
|
|
|
|
for state in response:
|
2022-11-26 19:44:08 -05:00
|
|
|
if state["attributes"].get("device_class") == self.device_class and all(
|
|
|
|
state["attributes"].get(k) == v for k, v in self.filters
|
|
|
|
):
|
2022-11-26 19:44:26 -05:00
|
|
|
if self.attribute is not None:
|
|
|
|
value = state["attributes"].get(self.attribute, -1)
|
|
|
|
uom = None
|
|
|
|
else:
|
|
|
|
value = float(state["state"]) if state["state"].isnumeric() else -1
|
|
|
|
uom = state["attributes"].get("unit_of_measurement")
|
|
|
|
|
2022-11-29 15:42:08 -05:00
|
|
|
if self.friendly_name:
|
|
|
|
name = (
|
|
|
|
state["attributes"]
|
|
|
|
.get("friendly_name", state["entity_id"])
|
2022-11-29 19:07:15 -05:00
|
|
|
.translate({ord(c): "_" for c in "'="})
|
2022-11-29 15:42:08 -05:00
|
|
|
)
|
|
|
|
else:
|
|
|
|
name = state["entity_id"]
|
|
|
|
|
2022-09-29 17:38:59 -04:00
|
|
|
if state["state"] == "unavailable":
|
|
|
|
_log.info(f"{state['entity_id']} unavailable")
|
|
|
|
yield nagiosplugin.Metric(
|
2022-11-29 15:42:08 -05:00
|
|
|
name,
|
2022-11-26 19:44:26 -05:00
|
|
|
value,
|
|
|
|
uom,
|
2022-11-26 19:42:49 -05:00
|
|
|
context=self.device_class,
|
2022-11-26 19:40:57 -05:00
|
|
|
min=self.min,
|
|
|
|
max=self.max,
|
2022-09-29 17:38:59 -04:00
|
|
|
)
|
|
|
|
|
|
|
|
|
2022-11-26 19:44:08 -05:00
|
|
|
def key_value(arg: str):
|
|
|
|
k, _, v = arg.partition("=")
|
|
|
|
return k, v
|
|
|
|
|
|
|
|
|
2022-11-29 17:54:54 -05:00
|
|
|
class Icinga2ConfAction(argparse.Action):
|
|
|
|
def __init__(
|
|
|
|
self,
|
|
|
|
option_strings,
|
|
|
|
dest=argparse.SUPPRESS,
|
|
|
|
default=argparse.SUPPRESS,
|
|
|
|
help="Generate conf file for icinga2 CheckCommand",
|
|
|
|
):
|
|
|
|
super().__init__(
|
|
|
|
option_strings=option_strings,
|
|
|
|
dest=dest,
|
|
|
|
default=default,
|
|
|
|
nargs=0,
|
|
|
|
help=help,
|
|
|
|
)
|
|
|
|
|
|
|
|
def __call__(
|
|
|
|
self, parser: argparse.ArgumentParser, namespace, values, option_string=None
|
|
|
|
):
|
|
|
|
filename = Path(__file__).absolute()
|
|
|
|
command_name = filename.stem.removeprefix("check_")
|
|
|
|
# FIXME: assumes that script will be in a subdirectory of PluginContribDir
|
|
|
|
command_path = filename.relative_to(filename.parents[1])
|
|
|
|
|
|
|
|
print(f'object CheckCommand "{command_name}" {{')
|
|
|
|
print(f' command = [ PluginContribDir + "/{command_path}" ]')
|
|
|
|
print(" arguments = {")
|
|
|
|
for action in parser._actions:
|
|
|
|
if action.dest in ["verbose", "help", self.dest]:
|
|
|
|
continue
|
|
|
|
|
|
|
|
arg_str = action.option_strings[-1]
|
|
|
|
icinga2_var = arg_str.lstrip("-").replace("-", "_")
|
|
|
|
print(f' "{arg_str}" = {{')
|
|
|
|
|
|
|
|
if action.required:
|
|
|
|
print(" required = true")
|
|
|
|
|
|
|
|
if isinstance(action, argparse._StoreConstAction):
|
|
|
|
print(f' set_if = "${command_name}_{icinga2_var}$"')
|
|
|
|
else:
|
|
|
|
print(f' value = "${command_name}_{icinga2_var}$"')
|
|
|
|
|
|
|
|
print(f' description = "{action.help}"')
|
|
|
|
print(" }")
|
|
|
|
|
|
|
|
print(" }\n}")
|
|
|
|
parser.exit()
|
|
|
|
|
|
|
|
|
2022-09-29 17:38:59 -04:00
|
|
|
@nagiosplugin.guarded
|
|
|
|
def main():
|
|
|
|
argp = argparse.ArgumentParser(description=__doc__)
|
2022-11-29 18:00:21 -05:00
|
|
|
|
|
|
|
argp.add_argument("-v", "--verbose", action="count", default=0)
|
|
|
|
argp.add_argument("--make-icinga-conf", action=Icinga2ConfAction)
|
|
|
|
|
2022-11-26 19:34:37 -05:00
|
|
|
argp.add_argument(
|
2022-11-29 17:56:13 -05:00
|
|
|
"-t",
|
|
|
|
"--token",
|
|
|
|
required=True,
|
|
|
|
type=str,
|
|
|
|
help="token for Home Assistant REST API (see https://developers.home-assistant.io/docs/api/rest/)",
|
2022-11-26 19:34:37 -05:00
|
|
|
)
|
|
|
|
argp.add_argument(
|
|
|
|
"-u", "--url", required=True, type=str, help="URL for Home Assistant"
|
|
|
|
)
|
|
|
|
|
2022-11-26 19:42:49 -05:00
|
|
|
argp.add_argument(
|
|
|
|
"-d",
|
|
|
|
"--device-class",
|
|
|
|
type=str,
|
|
|
|
required=True,
|
2022-11-29 17:55:46 -05:00
|
|
|
help="device class of entities to monitor",
|
2022-11-26 19:42:49 -05:00
|
|
|
)
|
2022-09-29 17:38:59 -04:00
|
|
|
argp.add_argument(
|
|
|
|
"-w",
|
|
|
|
"--warning",
|
|
|
|
metavar="RANGE",
|
2022-11-26 19:34:06 -05:00
|
|
|
required=True,
|
2022-09-29 17:38:59 -04:00
|
|
|
help="return warning if battery percentage is outside RANGE",
|
|
|
|
)
|
|
|
|
argp.add_argument(
|
|
|
|
"-c",
|
|
|
|
"--critical",
|
|
|
|
metavar="RANGE",
|
2022-11-26 19:34:06 -05:00
|
|
|
required=True,
|
2022-09-29 17:38:59 -04:00
|
|
|
help="return critical if battery percentage is outside RANGE",
|
|
|
|
)
|
2022-11-26 19:40:57 -05:00
|
|
|
argp.add_argument(
|
|
|
|
"--min",
|
|
|
|
type=float,
|
2022-11-29 17:55:46 -05:00
|
|
|
help="min for performance data",
|
2022-11-26 19:40:57 -05:00
|
|
|
)
|
|
|
|
argp.add_argument(
|
|
|
|
"--max",
|
|
|
|
type=float,
|
2022-11-29 17:55:46 -05:00
|
|
|
help="max for performance data",
|
2022-11-26 19:40:57 -05:00
|
|
|
)
|
2022-11-26 19:44:26 -05:00
|
|
|
argp.add_argument(
|
2022-11-29 17:55:46 -05:00
|
|
|
"-a", "--attribute", type=str, help="check attribute instead of value"
|
2022-11-26 19:44:26 -05:00
|
|
|
)
|
2022-11-26 19:44:08 -05:00
|
|
|
argp.add_argument(
|
|
|
|
"-f",
|
|
|
|
"--filter",
|
|
|
|
type=key_value,
|
|
|
|
default=[],
|
|
|
|
nargs="*",
|
2022-11-29 18:00:21 -05:00
|
|
|
help="filter by 'attribute=value' (may be specified multiple times)",
|
2022-11-26 19:44:08 -05:00
|
|
|
)
|
2022-11-29 15:42:08 -05:00
|
|
|
argp.add_argument(
|
|
|
|
"--friendly",
|
|
|
|
action="store_true",
|
2022-11-29 17:55:46 -05:00
|
|
|
help="use friendly name, when available",
|
2022-11-29 15:42:08 -05:00
|
|
|
)
|
2022-11-26 19:34:37 -05:00
|
|
|
|
2022-09-29 17:38:59 -04:00
|
|
|
args = argp.parse_args()
|
|
|
|
|
|
|
|
check = nagiosplugin.Check(
|
2022-11-26 19:40:57 -05:00
|
|
|
Entities(
|
|
|
|
args.url,
|
|
|
|
args.token,
|
2022-11-26 19:42:49 -05:00
|
|
|
args.device_class,
|
2022-11-26 19:40:57 -05:00
|
|
|
args.min,
|
|
|
|
args.max,
|
2022-11-26 19:44:08 -05:00
|
|
|
args.filter,
|
2022-11-26 19:44:26 -05:00
|
|
|
args.attribute,
|
2022-11-29 15:42:08 -05:00
|
|
|
args.friendly,
|
2022-11-26 19:40:57 -05:00
|
|
|
),
|
|
|
|
nagiosplugin.ScalarContext(args.device_class, args.warning, args.critical),
|
2022-09-29 17:38:59 -04:00
|
|
|
)
|
|
|
|
check.main(args.verbose)
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
main()
|