check_home_assistant_state/check_home_assistant_state.py

225 lines
6.3 KiB
Python
Raw Normal View History

#!/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."""
import argparse
2022-11-26 11:11:03 -05:00
import dataclasses
import logging
import sys
from urllib.parse import urljoin
from typing import Optional
from pathlib import Path
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
device_class: str
min: float
max: float
filters: list[str]
attribute: Optional[str]
friendly_name: bool
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:
if state["attributes"].get("device_class") == self.device_class and all(
state["attributes"].get(k) == v for k, v in self.filters
):
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")
if self.friendly_name:
name = (
state["attributes"]
.get("friendly_name", state["entity_id"])
.translate({ord(c): "_" for c in "'-"})
)
else:
name = state["entity_id"]
if state["state"] == "unavailable":
_log.info(f"{state['entity_id']} unavailable")
yield nagiosplugin.Metric(
name,
value,
uom,
context=self.device_class,
min=self.min,
max=self.max,
)
def key_value(arg: str):
k, _, v = arg.partition("=")
return k, v
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()
@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(
"-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"
)
argp.add_argument(
"-d",
"--device-class",
type=str,
required=True,
help="device class of entities to monitor",
)
argp.add_argument(
"-w",
"--warning",
metavar="RANGE",
2022-11-26 19:34:06 -05:00
required=True,
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,
help="return critical if battery percentage is outside RANGE",
)
argp.add_argument(
"--min",
type=float,
help="min for performance data",
)
argp.add_argument(
"--max",
type=float,
help="max for performance data",
)
argp.add_argument(
"-a", "--attribute", type=str, help="check attribute instead of value"
)
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)",
)
argp.add_argument(
"--friendly",
action="store_true",
help="use friendly name, when available",
)
2022-11-26 19:34:37 -05:00
args = argp.parse_args()
check = nagiosplugin.Check(
Entities(
args.url,
args.token,
args.device_class,
args.min,
args.max,
args.filter,
args.attribute,
args.friendly,
),
nagiosplugin.ScalarContext(args.device_class, args.warning, args.critical),
)
check.main(args.verbose)
if __name__ == "__main__":
main()