From 302cc9e4bce47586e6cd32447206d665269ea4a4 Mon Sep 17 00:00:00 2001 From: "th.l" <thl-cmk@outlook.com> Date: Sun, 31 Dec 2023 11:46:16 +0100 Subject: [PATCH] update project --- mkp/fritzbox_smarthome-0.8.3-20231230.mkp | Bin 14018 -> 14020 bytes source/agent_based/fritzbox_smarthome.py | 147 +++++++++ .../fritzbox_smarthome_app_lock.py | 89 ++++++ .../agent_based/fritzbox_smarthome_battery.py | 92 ++++++ .../fritzbox_smarthome_device_lock.py | 89 ++++++ .../fritzbox_smarthome_power_meter.py | 239 +++++++++++++++ .../fritzbox_smarthome_power_socket.py | 91 ++++++ .../agent_based/fritzbox_smarthome_switch.py | 91 ++++++ .../fritzbox_smarthome_temperature.py | 93 ++++++ .../fritzbox_smarthome_thermostat.py | 148 +++++++++ source/agent_based/inv_fritzbox_smarthome.py | 46 +++ .../agent_based/utils/fritzbox_smarthome.py | 238 +++++++++++++++ .../agents/special/agent_fritzbox_smarthome | 7 + source/checkman/fritzbox_smarthome | 11 + source/checks/agent_fritzbox_smarthome | 44 +++ source/gui/metrics/fritzbox_smarthome.py | 80 +++++ .../check_parameters/electrical_energy.py | 76 +++++ source/gui/wato/check_parameters/epower.py | 80 +++++ .../check_parameters/fritzbox_smarthome.py | 139 +++++++++ .../check_parameters/temperature_single.py | 28 ++ .../wato/check_parameters/voltage_single.py | 28 ++ .../agent_fritzbox_smarthome.py | 284 ++++++++++++++++++ source/packages/fritzbox_smarthome | 55 ++++ .../web/plugins/views/fritzbox_smarthome.py | 40 +++ .../plugins/wato/agent_fritzbox_smarthome.py | 91 ++++++ 25 files changed, 2326 insertions(+) create mode 100644 source/agent_based/fritzbox_smarthome.py create mode 100644 source/agent_based/fritzbox_smarthome_app_lock.py create mode 100644 source/agent_based/fritzbox_smarthome_battery.py create mode 100644 source/agent_based/fritzbox_smarthome_device_lock.py create mode 100644 source/agent_based/fritzbox_smarthome_power_meter.py create mode 100644 source/agent_based/fritzbox_smarthome_power_socket.py create mode 100644 source/agent_based/fritzbox_smarthome_switch.py create mode 100644 source/agent_based/fritzbox_smarthome_temperature.py create mode 100644 source/agent_based/fritzbox_smarthome_thermostat.py create mode 100644 source/agent_based/inv_fritzbox_smarthome.py create mode 100644 source/agent_based/utils/fritzbox_smarthome.py create mode 100755 source/agents/special/agent_fritzbox_smarthome create mode 100644 source/checkman/fritzbox_smarthome create mode 100644 source/checks/agent_fritzbox_smarthome create mode 100644 source/gui/metrics/fritzbox_smarthome.py create mode 100644 source/gui/wato/check_parameters/electrical_energy.py create mode 100644 source/gui/wato/check_parameters/epower.py create mode 100644 source/gui/wato/check_parameters/fritzbox_smarthome.py create mode 100644 source/gui/wato/check_parameters/temperature_single.py create mode 100644 source/gui/wato/check_parameters/voltage_single.py create mode 100644 source/lib/python3/cmk/special_agents/agent_fritzbox_smarthome.py create mode 100644 source/packages/fritzbox_smarthome create mode 100644 source/web/plugins/views/fritzbox_smarthome.py create mode 100644 source/web/plugins/wato/agent_fritzbox_smarthome.py diff --git a/mkp/fritzbox_smarthome-0.8.3-20231230.mkp b/mkp/fritzbox_smarthome-0.8.3-20231230.mkp index 3861f7e52d2a579d8db61d0d1c8e5f2b03cd398c..fc445537db31dbe328abb9dbd0a7a0155cd678ae 100644 GIT binary patch delta 12748 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 SG7=2ih5rxLW5m4xfC2!i7jUlt delta 12728 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 diff --git a/source/agent_based/fritzbox_smarthome.py b/source/agent_based/fritzbox_smarthome.py new file mode 100644 index 0000000..95a42b5 --- /dev/null +++ b/source/agent_based/fritzbox_smarthome.py @@ -0,0 +1,147 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# +# License: GNU General Public License v2 +# +# Author: thl-cmk[at]outlook[dot]com +# URL : https://thl-cmk.hopto.org +# Date : 2023-12-28 +# File : fritzbox_smarthome.py (check plugin) +# +# Based on the work of Maximilian Clemens, see https://github.com/MaximilianClemens/checkmk_fritzbox +# +# + +import json +from typing import Dict + +from cmk.base.plugins.agent_based.agent_based_api.v1 import ( + Result, + Service, + State, + register, + HostLabel, +) +from cmk.base.plugins.agent_based.agent_based_api.v1.type_defs import ( + CheckResult, + DiscoveryResult, + HostLabelGenerator, + StringTable, +) + +from cmk.base.plugins.agent_based.utils.fritzbox_smarthome import ( + AvmSmartHomeDevice, + parse_avm_smarthome_device, +) + + +def host_label_fritzbox_smarthome(section: AvmSmartHomeDevice | Dict[str, AvmSmartHomeDevice]) -> HostLabelGenerator: + yield HostLabel(name="fritz/smarthome/device", value="yes") + if isinstance(section, AvmSmartHomeDevice): # piggyback + yield HostLabel( + name="fritz/smarthome/device_type", + value=section.product_name.replace(' ', '_').lower() + ) + + +def parse_fritzbox_smarthome(string_table: StringTable) -> AvmSmartHomeDevice | Dict[str, AvmSmartHomeDevice] | None: + try: + raw_devices = json.loads(str(string_table[0][0])) + except json.JSONDecodeError: + return + + if isinstance(raw_devices, Dict): + return parse_avm_smarthome_device(raw_devices) + else: + return {raw_device['id']: parse_avm_smarthome_device(raw_device) for raw_device in raw_devices} + + +register.agent_section( + name="fritzbox_smarthome", + parse_function=parse_fritzbox_smarthome, + host_label_function=host_label_fritzbox_smarthome, +) + + +def discovery_fritzbox_smarthome_single( + section: AvmSmartHomeDevice | Dict[str, AvmSmartHomeDevice] +) -> DiscoveryResult: + if isinstance(section, AvmSmartHomeDevice): + yield Service() + + +def discovery_fritzbox_smarthome_multiple( + section: AvmSmartHomeDevice | Dict[str, AvmSmartHomeDevice] +) -> DiscoveryResult: + if not isinstance(section, AvmSmartHomeDevice): + for device_id, device in section.items(): + yield Service(item=str(device_id)) + + +def check_fritzbox_smarthome_single( + params, section: AvmSmartHomeDevice | Dict[str, AvmSmartHomeDevice] +) -> CheckResult: + if not isinstance(section, AvmSmartHomeDevice): + return + + _tx_busy = { + 0: 'no', + 1: 'yes', + } + + yield Result( + state=State.OK, + notice=f'Device: {section.manufacturer} {section.product_name}, FW: {section.fw_version}' + ) + + if section.present == 0: + yield Result(state=State(params['present']), summary='Device is offline') + # stop if device is not present + return + + yield Result(state=State.OK, summary='Device is online') + + if section.tx_busy is not None: + yield Result( + state=State.OK, + notice=f'Sending command: {_tx_busy.get(section.tx_busy, f"unknown ({section.tx_busy})")}' + ) + + +def check_fritzbox_smarthome_multiple( + item, params, section: AvmSmartHomeDevice | Dict[str, AvmSmartHomeDevice] +) -> CheckResult: + if isinstance(section, Dict): + try: + device = section[item] + except KeyError: + return + + yield from check_fritzbox_smarthome_single(params, device) + yield Result(state=State.OK, summary=f'[{device.name}]') + + +_fritzbox_smarthome_parameters = { + 'present': 1, +} + +register.check_plugin( + name="fritzbox_smarthome_single", + service_name="Device status", + sections=['fritzbox_smarthome'], + discovery_function=discovery_fritzbox_smarthome_single, + check_function=check_fritzbox_smarthome_single, + check_ruleset_name="fritzbox_smarthome_single", + check_default_parameters=_fritzbox_smarthome_parameters, +) + + +register.check_plugin( + name="fritzbox_smarthome_multiple", + service_name="Smarthome Device status %s", + sections=['fritzbox_smarthome'], + discovery_function=discovery_fritzbox_smarthome_multiple, + check_function=check_fritzbox_smarthome_multiple, + check_ruleset_name="fritzbox_smarthome_multiple", + check_default_parameters=_fritzbox_smarthome_parameters, +) diff --git a/source/agent_based/fritzbox_smarthome_app_lock.py b/source/agent_based/fritzbox_smarthome_app_lock.py new file mode 100644 index 0000000..b52314e --- /dev/null +++ b/source/agent_based/fritzbox_smarthome_app_lock.py @@ -0,0 +1,89 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# +# License: GNU General Public License v2 +# +# Author: thl-cmk[at]outlook[dot]com +# URL : https://thl-cmk.hopto.org +# Date : 2023-12-28 +# File : fritzbox_smarthome_app_lock.py (check plugin) +# +# + +from typing import Dict + +from cmk.base.plugins.agent_based.agent_based_api.v1 import ( + Service, + register, + Result, + State, +) +from cmk.base.plugins.agent_based.agent_based_api.v1.type_defs import CheckResult, DiscoveryResult +from cmk.base.plugins.agent_based.utils.fritzbox_smarthome import AvmSmartHomeDevice + + +def discovery_fritzbox_smarthome_app_lock_single( + section: AvmSmartHomeDevice | Dict[str, AvmSmartHomeDevice] +) -> DiscoveryResult: + if isinstance(section, AvmSmartHomeDevice): + if section.lock is not None: + yield Service() + + +def discovery_fritzbox_smarthome_app_lock_multiple( + section: AvmSmartHomeDevice | Dict[str, AvmSmartHomeDevice] +) -> DiscoveryResult: + if not isinstance(section, AvmSmartHomeDevice): + for device_id, device in section.items(): + if device.lock is not None: + yield Service(item=str(device_id)) + + +def check_fritzbox_smarthome_app_lock_single( + params, section: AvmSmartHomeDevice | Dict[str, AvmSmartHomeDevice] +) -> CheckResult: + if not isinstance(section, AvmSmartHomeDevice): + return + if section.lock is None: + return + + def _get_status(status: int): + _lock = { + 0: 'is not deactivated', + 1: 'is deactivated', + } + return _lock.get(status, f'unknown ({status})') + + yield Result(state=State.OK, summary=f'Manual access for phone, app or user interface {_get_status(section.lock)}') + + +def check_fritzbox_smarthome_app_lock_multiple( + item, params, section: AvmSmartHomeDevice | Dict[str, AvmSmartHomeDevice] +) -> CheckResult: + if isinstance(section, Dict): + try: + yield from check_fritzbox_smarthome_app_lock_single(params, section[item]) + except KeyError: + return + + +register.check_plugin( + name='fritzbox_smarthome_app_lock_single', + service_name='APP lock', + sections=['fritzbox_smarthome'], + discovery_function=discovery_fritzbox_smarthome_app_lock_single, + check_function=check_fritzbox_smarthome_app_lock_single, + # check_ruleset_name='fritzbox_smarthome_app_lock', + check_default_parameters={} +) + + +register.check_plugin( + name='fritzbox_smarthome_app_lock_multiple', + service_name='Smarthome APP lock %s', + sections=['fritzbox_smarthome'], + discovery_function=discovery_fritzbox_smarthome_app_lock_multiple, + check_function=check_fritzbox_smarthome_app_lock_multiple, + # check_ruleset_name='fritzbox_smarthome_app_lock', + check_default_parameters={} +) diff --git a/source/agent_based/fritzbox_smarthome_battery.py b/source/agent_based/fritzbox_smarthome_battery.py new file mode 100644 index 0000000..61d597b --- /dev/null +++ b/source/agent_based/fritzbox_smarthome_battery.py @@ -0,0 +1,92 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# +# License: GNU General Public License v2 +# +# Author: thl-cmk[at]outlook[dot]com +# URL : https://thl-cmk.hopto.org +# Date : 2023-12-29 +# File : fritzbox_smarthome_battery.py (check plugin) +# +# + +from typing import Dict + +from cmk.base.plugins.agent_based.agent_based_api.v1 import ( + Result, + Service, + State, + register, +) +from cmk.base.plugins.agent_based.agent_based_api.v1.type_defs import CheckResult, DiscoveryResult +from cmk.base.plugins.agent_based.utils.fritzbox_smarthome import AvmSmartHomeDevice + + +def discovery_fritzbox_smarthome_battery_single( + section: AvmSmartHomeDevice | Dict[str, AvmSmartHomeDevice] +) -> DiscoveryResult: + if isinstance(section, AvmSmartHomeDevice): + if section.battery_low is not None: + yield Service() + + +def discovery_fritzbox_smarthome_battery_multiple( + section: AvmSmartHomeDevice | Dict[str, AvmSmartHomeDevice] +) -> DiscoveryResult: + if not isinstance(section, AvmSmartHomeDevice): + for device_id, device in section.items(): + if device.battery_low is not None: + yield Service(item=str(device_id)) + + +def check_fritzbox_smarthome_battery_single( + params, section: AvmSmartHomeDevice | Dict[str, AvmSmartHomeDevice] +) -> CheckResult: + if not isinstance(section, AvmSmartHomeDevice): + return + + if section.battery_low is None: + return + + _battery_low = { + 0: 'no', + 1: 'yes', + } + + _message = f'Battery low: {_battery_low.get(section.battery_low, f"unknown ({section.battery_low})")}' + if section.battery_low == 0: + yield Result(state=State.OK, summary=_message) + else: + yield Result(state=State(params.get('battery_low', 2)), summary=_message) + + +def check_fritzbox_smarthome_battery_multiple( + item, params, section: AvmSmartHomeDevice | Dict[str, AvmSmartHomeDevice] +) -> CheckResult: + if isinstance(section, Dict): + try: + yield from check_fritzbox_smarthome_battery_single(params, section[item]) + except KeyError: + return + + +register.check_plugin( + name='fritzbox_smarthome_battery_single', + service_name='Battery', + sections=['fritzbox_smarthome'], + discovery_function=discovery_fritzbox_smarthome_battery_single, + check_function=check_fritzbox_smarthome_battery_single, + check_ruleset_name='fritzbox_smarthome_battery_single', + check_default_parameters={} +) + + +register.check_plugin( + name='fritzbox_smarthome_battery_multiple', + service_name='Smarthome Battery %s', + sections=['fritzbox_smarthome'], + discovery_function=discovery_fritzbox_smarthome_battery_multiple, + check_function=check_fritzbox_smarthome_battery_multiple, + check_ruleset_name='fritzbox_smarthome_battery_multiple', + check_default_parameters={} +) diff --git a/source/agent_based/fritzbox_smarthome_device_lock.py b/source/agent_based/fritzbox_smarthome_device_lock.py new file mode 100644 index 0000000..6c088fd --- /dev/null +++ b/source/agent_based/fritzbox_smarthome_device_lock.py @@ -0,0 +1,89 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# +# License: GNU General Public License v2 +# +# Author: thl-cmk[at]outlook[dot]com +# URL : https://thl-cmk.hopto.org +# Date : 2023-12-30 +# File : fritzbox_smarthome_device_lock.py (check plugin) +# +# + +from typing import Dict + +from cmk.base.plugins.agent_based.agent_based_api.v1 import ( + Service, + register, + Result, + State, +) +from cmk.base.plugins.agent_based.agent_based_api.v1.type_defs import CheckResult, DiscoveryResult +from cmk.base.plugins.agent_based.utils.fritzbox_smarthome import AvmSmartHomeDevice + + +def discovery_fritzbox_smarthome_device_lock_single( + section: AvmSmartHomeDevice | Dict[str, AvmSmartHomeDevice] +) -> DiscoveryResult: + if isinstance(section, AvmSmartHomeDevice): + if section.lock is not None: + yield Service() + + +def discovery_fritzbox_smarthome_device_lock_multiple( + section: AvmSmartHomeDevice | Dict[str, AvmSmartHomeDevice] +) -> DiscoveryResult: + if not isinstance(section, AvmSmartHomeDevice): + for device_id, device in section.items(): + if device.lock is not None: + yield Service(item=str(device_id)) + + +def check_fritzbox_smarthome_device_lock_single( + params, section: AvmSmartHomeDevice | Dict[str, AvmSmartHomeDevice] +) -> CheckResult: + if not isinstance(section, AvmSmartHomeDevice): + return + + if section.device_lock is None: + return + + def _get_status(status: int): + _dev_lock = { + 0: 'is not active', + 1: 'is active', + } + return _dev_lock.get(status, f'unknown ({status})') + + yield Result(state=State.OK, summary=f'Button lock on the device {_get_status(section.device_lock)}') + + +def check_fritzbox_smarthome_device_lock_multiple( + item, params, section: AvmSmartHomeDevice | Dict[str, AvmSmartHomeDevice] +) -> CheckResult: + if isinstance(section, Dict): + try: + yield from check_fritzbox_smarthome_device_lock_single(params, section[item]) + except KeyError: + return + + +register.check_plugin( + name='fritzbox_smarthome_device_lock_single', + service_name='Device lock', + sections=['fritzbox_smarthome'], + discovery_function=discovery_fritzbox_smarthome_device_lock_single, + check_function=check_fritzbox_smarthome_device_lock_single, + # check_ruleset_name='fritzbox_smarthome_device_lock_single', + check_default_parameters={} +) + +register.check_plugin( + name='fritzbox_smarthome_device_lock_multiple', + service_name='Smarthome Device lock %s', + sections=['fritzbox_smarthome'], + discovery_function=discovery_fritzbox_smarthome_device_lock_multiple, + check_function=check_fritzbox_smarthome_device_lock_multiple, + # check_ruleset_name='fritzbox_smarthome_device_lock_multiple', + check_default_parameters={} +) diff --git a/source/agent_based/fritzbox_smarthome_power_meter.py b/source/agent_based/fritzbox_smarthome_power_meter.py new file mode 100644 index 0000000..6e1c096 --- /dev/null +++ b/source/agent_based/fritzbox_smarthome_power_meter.py @@ -0,0 +1,239 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# +# License: GNU General Public License v2 +# +# Author: thl-cmk[at]outlook[dot]com +# URL : https://thl-cmk.hopto.org +# Date : 2023-12-28 +# File : fritzbox_smarthome_power_meter.py (check plugin) +# +# + +from time import time as time_now +from typing import Dict + +from cmk.base.plugins.agent_based.agent_based_api.v1 import ( + GetRateError, + Result, + Service, + State, + check_levels, + get_rate, + get_value_store, + register, +) +from cmk.base.plugins.agent_based.agent_based_api.v1.type_defs import CheckResult, DiscoveryResult +from cmk.utils.render import physical_precision +from cmk.base.plugins.agent_based.utils.fritzbox_smarthome import AvmSmartHomeDevice + + +def discovery_fritzbox_smarthome_voltage_single( + section: AvmSmartHomeDevice | Dict[str, AvmSmartHomeDevice] +) -> DiscoveryResult: + if isinstance(section, AvmSmartHomeDevice): + if section.power_meter and section.power_meter.voltage: + yield Service() + + +def discovery_fritzbox_smarthome_voltage_multiple( + section: AvmSmartHomeDevice | Dict[str, AvmSmartHomeDevice] +) -> DiscoveryResult: + if not isinstance(section, AvmSmartHomeDevice): + for device_id, device in section.items(): + if device.power_meter and device.power_meter.voltage: + yield Service(item=str(device_id)) + + +def check_fritzbox_smarthome_voltage_single( + params, section: AvmSmartHomeDevice | Dict[str, AvmSmartHomeDevice] +) -> CheckResult: + if not isinstance(section, AvmSmartHomeDevice): + return + if section.power_meter and section.power_meter.voltage: + yield from check_levels( + label='Voltage', + levels_lower=params.get('levels_lower'), + levels_upper=params.get('levels'), + metric_name='voltage', + render_func=lambda x: f'{x}V', + value=section.power_meter.voltage, + ) + + +def check_fritzbox_smarthome_voltage_multiple( + item, params, section: AvmSmartHomeDevice | Dict[str, AvmSmartHomeDevice] +) -> CheckResult: + if isinstance(section, Dict): + try: + yield from check_fritzbox_smarthome_voltage_single(params, section[item]) + except KeyError: + return + + +register.check_plugin( + name='fritzbox_smarthome_voltage_single', + service_name='Voltage', + sections=['fritzbox_smarthome'], + discovery_function=discovery_fritzbox_smarthome_voltage_single, + check_function=check_fritzbox_smarthome_voltage_single, + check_ruleset_name='voltage_single', + check_default_parameters={} +) + +register.check_plugin( + name='fritzbox_smarthome_voltage_multiple', + service_name='Smarthome Voltage %s', + sections=['fritzbox_smarthome'], + discovery_function=discovery_fritzbox_smarthome_voltage_multiple, + check_function=check_fritzbox_smarthome_voltage_multiple, + check_ruleset_name='voltage', + check_default_parameters={} +) + + +def discovery_fritzbox_smarthome_power_single( + section: AvmSmartHomeDevice | Dict[str, AvmSmartHomeDevice] +) -> DiscoveryResult: + if isinstance(section, AvmSmartHomeDevice): + if section.power_meter and section.power_meter.power: + yield Service() + + +def discovery_fritzbox_smarthome_power_multiple( + section: AvmSmartHomeDevice | Dict[str, AvmSmartHomeDevice] +) -> DiscoveryResult: + if not isinstance(section, AvmSmartHomeDevice): + for device_id, device in section.items(): + if device.power_meter and device.power_meter.power: + yield Service(item=str(device_id)) + + +def check_fritzbox_smarthome_power_single( + params, section: AvmSmartHomeDevice | Dict[str, AvmSmartHomeDevice] +) -> CheckResult: + if not isinstance(section, AvmSmartHomeDevice): + return + + if section.power_meter and section.power_meter.power: + yield from check_levels( + value=section.power_meter.power, + metric_name='power', + label='Power', + render_func=lambda x: f'{x}W', + levels_upper=params.get('levels_upper'), + levels_lower=params.get('levels_lower'), + ) + + +def check_fritzbox_smarthome_power_multiple( + item, params, section: AvmSmartHomeDevice | Dict[str, AvmSmartHomeDevice] +) -> CheckResult: + if isinstance(section, Dict): + try: + yield from check_fritzbox_smarthome_power_single(params, section[item]) + except KeyError: + return + + +register.check_plugin( + name='fritzbox_smarthome_power_single', + service_name='Power', + sections=['fritzbox_smarthome'], + discovery_function=discovery_fritzbox_smarthome_power_single, + check_function=check_fritzbox_smarthome_power_single, + check_ruleset_name='epower_single', + check_default_parameters={} +) + +register.check_plugin( + name='fritzbox_smarthome_power_multiple', + service_name='Smarthome Power %s', + sections=['fritzbox_smarthome'], + discovery_function=discovery_fritzbox_smarthome_power_multiple, + check_function=check_fritzbox_smarthome_power_multiple, + check_ruleset_name='epower', + check_default_parameters={} +) + + +def discovery_fritzbox_smarthome_energy_single( + section: AvmSmartHomeDevice | Dict[str, AvmSmartHomeDevice] +) -> DiscoveryResult: + if isinstance(section, AvmSmartHomeDevice): + if section.power_meter and section.power_meter.energy: + yield Service() + + +def discovery_fritzbox_smarthome_energy_multiple( + section: AvmSmartHomeDevice | Dict[str, AvmSmartHomeDevice] +) -> DiscoveryResult: + if not isinstance(section, AvmSmartHomeDevice): + for device_id, device in section.items(): + if device.power_meter and device.power_meter.energy: + yield Service(item=str(device_id)) + + +def check_fritzbox_smarthome_energy_single( + params, section: AvmSmartHomeDevice | Dict[str, AvmSmartHomeDevice] +) -> CheckResult: + if not isinstance(section, AvmSmartHomeDevice): + return + + if section.power_meter and section.power_meter.energy: + try: + energy = get_rate( + value_store=get_value_store(), + key='energy', + time=time_now(), + value=section.power_meter.energy, + raise_overflow=True + ) + except GetRateError as e: + yield Result(state=State.OK, notice=str(e)) + else: + yield from check_levels( + value=energy, + metric_name='energy', + label='Consumption current', + render_func=lambda x: physical_precision(v=x, precision=3, unit_symbol="Wh"), + levels_lower=params.get('levels_lower'), + levels_upper=params.get('levels_upper'), + ) + + yield Result( + state=State.OK, + summary=f'Consumption total: ' + f'{physical_precision(v=section.power_meter.energy, precision=3, unit_symbol="Wh")}' + ) + + +def check_fritzbox_smarthome_energy_multiple( + item, params, section: AvmSmartHomeDevice | Dict[str, AvmSmartHomeDevice] +) -> CheckResult: + if isinstance(section, Dict): + try: + yield from check_fritzbox_smarthome_energy_single(params, section[item]) + except KeyError: + return + + +register.check_plugin( + name='fritzbox_smarthome_energy_single', + service_name='Energy', + sections=['fritzbox_smarthome'], + discovery_function=discovery_fritzbox_smarthome_energy_single, + check_function=check_fritzbox_smarthome_energy_single, + check_ruleset_name='energy_single', + check_default_parameters={} +) + +register.check_plugin( + name='fritzbox_smarthome_energy_multiple', + service_name='Smarthome Energy %s', + sections=['fritzbox_smarthome'], + discovery_function=discovery_fritzbox_smarthome_energy_multiple, + check_function=check_fritzbox_smarthome_energy_multiple, + check_ruleset_name='energy_multiple', + check_default_parameters={} +) diff --git a/source/agent_based/fritzbox_smarthome_power_socket.py b/source/agent_based/fritzbox_smarthome_power_socket.py new file mode 100644 index 0000000..d702cb9 --- /dev/null +++ b/source/agent_based/fritzbox_smarthome_power_socket.py @@ -0,0 +1,91 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# +# License: GNU General Public License v2 +# +# Author: thl-cmk[at]outlook[dot]com +# URL : https://thl-cmk.hopto.org +# Date : 2023-12-30 +# File : fritzbox_smarthome_power_socket.py (check plugin) +# +# + +from typing import Dict + +from cmk.base.plugins.agent_based.agent_based_api.v1 import ( + Service, + register, + Result, + State, +) +from cmk.base.plugins.agent_based.agent_based_api.v1.type_defs import CheckResult, DiscoveryResult +from cmk.base.plugins.agent_based.utils.fritzbox_smarthome import AvmSmartHomeDevice + + +def discovery_fritzbox_smarthome_power_socket_single( + section: AvmSmartHomeDevice | Dict[str, AvmSmartHomeDevice] +) -> DiscoveryResult: + if isinstance(section, AvmSmartHomeDevice): + if section.switch is not None: + yield Service() + + +def discovery_fritzbox_smarthome_power_socket_multiple( + section: AvmSmartHomeDevice | Dict[str, AvmSmartHomeDevice] +) -> DiscoveryResult: + if not isinstance(section, AvmSmartHomeDevice): + for device_id, device in section.items(): + if device.switch is not None: + yield Service(item=str(device_id)) + + +def check_fritzbox_smarthome_power_socket_single( + params, section: AvmSmartHomeDevice | Dict[str, AvmSmartHomeDevice] +) -> CheckResult: + if not isinstance(section, AvmSmartHomeDevice): + return + + if not section.switch: + return + + def _get_status(status: int): + _switch_state = { + 0: 'off', + 1: 'on', + } + return _switch_state.get(status, f'unknown ({status})') + + yield Result(state=State.OK, summary=f'State: {_get_status(section.switch.state)}') + yield Result(state=State.OK, summary=f'Mode: {section.switch.mode}') + + +def check_fritzbox_smarthome_power_socket_multiple( + item, params, section: AvmSmartHomeDevice | Dict[str, AvmSmartHomeDevice] +) -> CheckResult: + if isinstance(section, Dict): + try: + yield from check_fritzbox_smarthome_power_socket_single(params, section[item]) + except KeyError: + return + + +register.check_plugin( + name='fritzbox_smarthome_power_socket_single', + service_name='Power socket', + sections=['fritzbox_smarthome'], + discovery_function=discovery_fritzbox_smarthome_power_socket_single, + check_function=check_fritzbox_smarthome_power_socket_single, + # check_ruleset_name='fritzbox_smarthome_power_socket', + check_default_parameters={} +) + + +register.check_plugin( + name='fritzbox_smarthome_power_socket_multiple', + service_name='Smarthome Power socket %s', + sections=['fritzbox_smarthome'], + discovery_function=discovery_fritzbox_smarthome_power_socket_multiple, + check_function=check_fritzbox_smarthome_power_socket_multiple, + # check_ruleset_name='fritzbox_smarthome_power_socket', + check_default_parameters={} +) diff --git a/source/agent_based/fritzbox_smarthome_switch.py b/source/agent_based/fritzbox_smarthome_switch.py new file mode 100644 index 0000000..7bfeac1 --- /dev/null +++ b/source/agent_based/fritzbox_smarthome_switch.py @@ -0,0 +1,91 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# +# License: GNU General Public License v2 +# +# Author: thl-cmk[at]outlook[dot]com +# URL : https://thl-cmk.hopto.org +# Date : 2023-12-30 +# File : fritzbox_smarthome_power_socket.py (check plugin) +# +# + +from typing import Dict + +from cmk.base.plugins.agent_based.agent_based_api.v1 import ( + Service, + register, + Result, + State, +) +from cmk.base.plugins.agent_based.agent_based_api.v1.type_defs import CheckResult, DiscoveryResult +from cmk.base.plugins.agent_based.utils.fritzbox_smarthome import AvmSmartHomeDevice + + +def discovery_fritzbox_smarthome_switch_single( + section: AvmSmartHomeDevice | Dict[str, AvmSmartHomeDevice] +) -> DiscoveryResult: + if isinstance(section, AvmSmartHomeDevice): + if section.switch is not None: + yield Service() + + +def discovery_fritzbox_smarthome_switch_multiple( + section: AvmSmartHomeDevice | Dict[str, AvmSmartHomeDevice] +) -> DiscoveryResult: + if not isinstance(section, AvmSmartHomeDevice): + for device_id, device in section.items(): + if device.simple_on_off is not None: + yield Service(item=str(device_id)) + + +def check_fritzbox_smarthome_switch_single( + params, section: AvmSmartHomeDevice | Dict[str, AvmSmartHomeDevice] +) -> CheckResult: + if not isinstance(section, AvmSmartHomeDevice): + return + + if not section.simple_on_off: + return + + if section.simple_on_off: + def _get_status(status: int): + _simple_onf_off_state = { + 0: 'off', + 1: 'on', + } + return _simple_onf_off_state.get(status, f'unknown ({status})') + + yield Result(state=State.OK, summary=f'State: {_get_status(section.switch.state)}') + + +def check_fritzbox_smarthome_switch_multiple( + item, params, section: AvmSmartHomeDevice | Dict[str, AvmSmartHomeDevice] +) -> CheckResult: + if isinstance(section, Dict): + try: + yield from check_fritzbox_smarthome_switch_single(params, section[item]) + except KeyError: + return + + +register.check_plugin( + name='fritzbox_smarthome_switch_single', + service_name='Switch', + sections=['fritzbox_smarthome'], + discovery_function=discovery_fritzbox_smarthome_switch_single, + check_function=check_fritzbox_smarthome_switch_single, + # check_ruleset_name='fritzbox_smarthome_switch', + check_default_parameters={} +) + + +register.check_plugin( + name='fritzbox_smarthome_switch_multiple', + service_name='Smarthome Switch %s', + sections=['fritzbox_smarthome'], + discovery_function=discovery_fritzbox_smarthome_switch_multiple, + check_function=check_fritzbox_smarthome_switch_multiple, + # check_ruleset_name='fritzbox_smarthome_switch', + check_default_parameters={} +) diff --git a/source/agent_based/fritzbox_smarthome_temperature.py b/source/agent_based/fritzbox_smarthome_temperature.py new file mode 100644 index 0000000..42875b7 --- /dev/null +++ b/source/agent_based/fritzbox_smarthome_temperature.py @@ -0,0 +1,93 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# +# License: GNU General Public License v2 +# +# Author: thl-cmk[at]outlook[dot]com +# URL : https://thl-cmk.hopto.org +# Date : 2023-12-28 +# File : fritzbox_smarthome_temperature.py (check plugin) +# +# + +from typing import Dict + +from cmk.base.plugins.agent_based.agent_based_api.v1 import Result, Service, State, register +from cmk.base.plugins.agent_based.agent_based_api.v1.type_defs import CheckResult, DiscoveryResult +from cmk.base.plugins.agent_based.utils.temperature import check_temperature, _render_temp_with_unit +from cmk.base.plugins.agent_based.utils.fritzbox_smarthome import AvmSmartHomeDevice + + +def discovery_fritzbox_smarthome_temperature_single( + section: AvmSmartHomeDevice | Dict[str, AvmSmartHomeDevice] +) -> DiscoveryResult: + if isinstance(section, AvmSmartHomeDevice): + if section.temperature: + yield Service() + + +def discovery_fritzbox_smarthome_temperature_multiple( + section: AvmSmartHomeDevice | Dict[str, AvmSmartHomeDevice] +) -> DiscoveryResult: + if not isinstance(section, AvmSmartHomeDevice): + for device_id, device in section.items(): + if device.temperature: + yield Service(item=str(device_id)) + + +def check_fritzbox_smarthome_temperature_single( + params, section: AvmSmartHomeDevice | Dict[str, AvmSmartHomeDevice] +) -> CheckResult: + if not isinstance(section, AvmSmartHomeDevice): + return + + if not section.temperature: + return + + yield from check_temperature( + reading=section.temperature.celsius, + params=params, + ) + if section.temperature.offset != 0: + _status = section.temperature.celsius + section.temperature.offset * -1 + _message = ( + f'Temperature measured at the thermostat: ' + f'{_render_temp_with_unit(_status, params.get("output_unit", "c"))}' + ) + yield Result(state=State.OK, notice=_message) + yield Result( + state=State.OK, + notice=f'Temperature offset: ' + f'{_render_temp_with_unit(section.temperature.offset, params.get("output_unit", "c"))}' + ) + + +def check_fritzbox_smarthome_temperature_multiple( + item, params, section: AvmSmartHomeDevice | Dict[str, AvmSmartHomeDevice] +) -> CheckResult: + if isinstance(section, Dict): + try: + yield from check_fritzbox_smarthome_temperature_single(params, section[item]) + except KeyError: + return + + +register.check_plugin( + name='fritzbox_smarthome_temperature_single', + service_name='Temperature', + sections=['fritzbox_smarthome'], + discovery_function=discovery_fritzbox_smarthome_temperature_single, + check_function=check_fritzbox_smarthome_temperature_single, + check_ruleset_name='temperature_single', + check_default_parameters={} +) + +register.check_plugin( + name='fritzbox_smarthome_temperature', + service_name='Smarthome Temperature %s', + sections=['fritzbox_smarthome'], + discovery_function=discovery_fritzbox_smarthome_temperature_multiple, + check_function=check_fritzbox_smarthome_temperature_multiple, + check_ruleset_name='temperature', + check_default_parameters={} +) diff --git a/source/agent_based/fritzbox_smarthome_thermostat.py b/source/agent_based/fritzbox_smarthome_thermostat.py new file mode 100644 index 0000000..2579f61 --- /dev/null +++ b/source/agent_based/fritzbox_smarthome_thermostat.py @@ -0,0 +1,148 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# +# License: GNU General Public License v2 +# +# Author: thl-cmk[at]outlook[dot]com +# URL : https://thl-cmk.hopto.org +# Date : 2023-12-28 +# File : fritzbox_smarthome_thermostat.py (check plugin) +# +# + +from time import strftime, localtime +from typing import Dict + +from cmk.base.plugins.agent_based.agent_based_api.v1 import ( + Metric, + Result, + Service, + State, + register, +) +from cmk.base.plugins.agent_based.agent_based_api.v1.type_defs import CheckResult, DiscoveryResult +from cmk.base.plugins.agent_based.utils.fritzbox_smarthome import AvmSmartHomeDevice + +_TIME_FORMAT = '%Y-%m-%dT%H:%M:%S' + + +def discovery_fritzbox_smarthome_thermostat_single( + section: AvmSmartHomeDevice | Dict[str, AvmSmartHomeDevice] +) -> DiscoveryResult: + if isinstance(section, AvmSmartHomeDevice): + if section.thermostat: + yield Service() + + +def discovery_fritzbox_smarthome_thermostat_multiple( + section: AvmSmartHomeDevice | Dict[str, AvmSmartHomeDevice] +) -> DiscoveryResult: + if not isinstance(section, AvmSmartHomeDevice): + for device_id, device in section.items(): + if device.thermostat: + yield Service(item=str(device_id)) + + +def check_fritzbox_smarthome_thermostat_single( + params, section: AvmSmartHomeDevice | Dict[str, AvmSmartHomeDevice] +) -> CheckResult: + if not isinstance(section, AvmSmartHomeDevice): + return + + if not section.thermostat: + return + + if thermostat := section.thermostat: + _error_codes = { + 0: 'no error', + 1: 'No adaptation possible. Is the thermostat correctly mounted on the radiator?', + 2: 'Valve stroke too short or battery power too low. Open and close the ' + 'valve tappet several times by hand or insert new batteries.', + 3: 'No valve movement possible. Valve tappet free?', + 4: 'The installation is currently being prepared.', + 5: 'The thermostat is in installation mode and can be mounted on the heating valve.', + 6: 'The thermostat now adapts to the stroke of the heating valve', + } + + if thermostat.temp_target == 126.5: # == radiator off + yield Result(state=State.OK, summary=f'Temperature current: {thermostat.temp_current}°C') + yield Result(state=State.OK, summary=f'Temperature target: radiator off') + else: + deviation = thermostat.temp_current - thermostat.temp_target + if deviation == 0: + yield Result(state=State.OK, summary=f'Temperature current: {thermostat.temp_target}°C') + else: + _message = f'Temperature current: {thermostat.temp_current}°C (deviation from target {deviation}°C)' + _state = State.OK + if params.get('deviation'): + warn, crit = params['deviation'] + if abs(deviation) >= crit: + _state = State.CRIT + elif abs(deviation) >= warn: + _state = State.WARN + yield Result(state=_state, summary=_message) + + yield Result( + state=State.OK, + summary=f'Target: {thermostat.temp_target}°C', + details=f'Temperature target: {thermostat.temp_target}°C' + ) + yield Metric(name='temp_target', value=thermostat.temp_target) + + yield Result(state=State.OK, notice=f'Temperature economic: {thermostat.temp_economic}°C') + yield Result(state=State.OK, notice=f'Temperature comfort: {thermostat.temp_comfort}°C') + + yield Metric(name='temp_current', value=thermostat.temp_current) + yield Metric(name='temp_comfort', value=thermostat.temp_comfort) + yield Metric(name='temp_economic', value=thermostat.temp_economic) + + if thermostat.next_change: + yield Result( + state=State.OK, + notice=f'End of period: {strftime(_TIME_FORMAT, localtime(thermostat.next_change.end_period))}' + ) + yield Result( + state=State.OK, + notice=f'Temperature target after end of period: {thermostat.next_change.temp_change_to}°C' + ) + + _message = f'Error code: {_error_codes.get(thermostat.error_code, f"unknown error {thermostat.error_code}")}' + if thermostat.error_code == 0: + yield Result(state=State.OK, notice=_message) + else: + yield Result( + state=State(params.get('state_on_error', 1)), + summary=f'Error Code: {thermostat.error_code} (see details)', + details=_message) + + +def check_fritzbox_smarthome_thermostat_multiple( + item, params, section: AvmSmartHomeDevice | Dict[str, AvmSmartHomeDevice] +) -> CheckResult: + if isinstance(section, Dict): + try: + yield from check_fritzbox_smarthome_thermostat_single(params, section[item]) + except KeyError: + return + + +register.check_plugin( + name='fritzbox_smarthome_thermostat_single', + service_name='Thermostat', + sections=['fritzbox_smarthome'], + discovery_function=discovery_fritzbox_smarthome_thermostat_single, + check_function=check_fritzbox_smarthome_thermostat_single, + check_ruleset_name='fritzbox_smarthome_thermostat_single', + check_default_parameters={} +) + + +register.check_plugin( + name='fritzbox_smarthome_thermostat_multiple', + service_name='Smarthome Thermostat %s', + sections=['fritzbox_smarthome'], + discovery_function=discovery_fritzbox_smarthome_thermostat_multiple, + check_function=check_fritzbox_smarthome_thermostat_multiple, + check_ruleset_name='fritzbox_smarthome_thermostat_multiple', + check_default_parameters={} +) diff --git a/source/agent_based/inv_fritzbox_smarthome.py b/source/agent_based/inv_fritzbox_smarthome.py new file mode 100644 index 0000000..0f77bf5 --- /dev/null +++ b/source/agent_based/inv_fritzbox_smarthome.py @@ -0,0 +1,46 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# +# License: GNU General Public License v2 +# +# Author: thl-cmk[at]outlook[dot]com +# URL : https://thl-cmk.hopto.org +# Date : 2023-12-29 +# File : inv_fritzbox_smarthome.py (inventory plugin) +# + +from typing import Dict +from cmk.base.plugins.agent_based.agent_based_api.v1 import register, TableRow +from cmk.base.plugins.agent_based.agent_based_api.v1.type_defs import InventoryResult +from cmk.base.plugins.agent_based.utils.fritzbox_smarthome import AvmSmartHomeDevice + + +def _add_avm_smarthome_device(device: AvmSmartHomeDevice): + path = ['hardware', 'avm', 'smart_home_devices'] + yield TableRow( + path=path, + key_columns={'id': device.id}, + inventory_columns={ + 'identifier': device.identifier, + 'name': device.name, + 'fw_version': device.fw_version, + 'manufacturer': device.manufacturer, + 'product_name': device.product_name, + 'functions': ', '.join(device.functions) + } + ) + + +def inventory_fritzbox_smarthome(section: AvmSmartHomeDevice | Dict[str, AvmSmartHomeDevice]) -> InventoryResult: + if isinstance(section, AvmSmartHomeDevice): + yield from _add_avm_smarthome_device(device=section) + else: + for device in section.values(): + yield from _add_avm_smarthome_device(device) + + +register.inventory_plugin( + name="inv_fritzbox_smarthome", + sections=['fritzbox_smarthome'], + inventory_function=inventory_fritzbox_smarthome, +) diff --git a/source/agent_based/utils/fritzbox_smarthome.py b/source/agent_based/utils/fritzbox_smarthome.py new file mode 100644 index 0000000..b6a9137 --- /dev/null +++ b/source/agent_based/utils/fritzbox_smarthome.py @@ -0,0 +1,238 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# +# License: GNU General Public License v2 +# +# Author: thl-cmk[at]outlook[dot]com +# URL : https://thl-cmk.hopto.org +# Date : 2023-12-29 +# File : fritzbox_smarthome.py (check plugin utils) +# +# Based on the work of Maximilian Clemens, see https://github.com/MaximilianClemens/checkmk_fritzbox +# +# + +from dataclasses import dataclass +from typing import Any, List, Dict + + +@dataclass(frozen=True) +class AvmTemperature: + celsius: float + offset: float + + +@dataclass(frozen=True) +class AvmAlert: + last_alert_chg_timestamp: int + state: int + + +@dataclass(frozen=True) +class AvmButton: + last_pressed_timestamp: int + id: int | None = None + identifier: int | None = None + name: str | None = None + + +@dataclass(frozen=True) +class AvmPowerMeter: + energy: float + power: float + voltage: float + + +@dataclass(frozen=True) +class AvmSimpleOnOff: + state: int + + +@dataclass(frozen=True) +class AvmNextChange: + end_period: int + temp_change_to: float + + +@dataclass(frozen=True) +class AvmThermostat: + error_code: int + temp_comfort: float + temp_current: float + temp_economic: float + temp_target: float + adaptive_heating_active: int | None = None + adaptive_heating_running: int | None = None + battery: float | None = None + boost_active: int | None = None + boost_active_end_time: int | None = None + holiday_active: int | None = None + next_change: AvmNextChange | None = None + summer_active: int | None = None + window_open_activ: int | None = None + + +@dataclass(frozen=True) +class AvmSwitch: + mode: str + state: int + + +@dataclass(frozen=True) +class AvmSmartHomeDevice: + fbm: int + functions: List[str] + fw_version: str + id: str + identifier: str + manufacturer: str + name: str + present: int + product_name: str + battery_low: int | None = None + device_lock: int | None = None + lock: int | None = None + power_meter: AvmPowerMeter | None = None + simple_on_off: AvmSimpleOnOff | None = None + switch: AvmSwitch | None = None + temperature: AvmTemperature | None = None + thermostat: AvmThermostat | None = None + tx_busy: int | None = None + + +_AVM_THERMOSTAT = 'hkr' +_AVM_SWITCH = 'switch' +_AVM_POWER_METER = 'powermeter' +_AVM_TEMPERATURE = 'temperature' +_AVM_SIMPLE_ON_OFF = 'simpleonoff' +_AVM_NEXT_CHANGE = 'nextchange' + + +def _get_battery_low(device: Dict[str, Any]) -> int | None: + try: + return int(device[_AVM_THERMOSTAT]['batterylow']) + except KeyError: + pass + + return None + + +def _get_lock(device: Dict[str, Any]) -> int | None: + try: + return int(device[_AVM_THERMOSTAT]['lock']) + except KeyError: + pass + + try: + return int(device[_AVM_SWITCH]['lock']) + except KeyError: + pass + + return None + + +def _get_device_lock(device: Dict[str, Any]) -> int | None: + try: + return int(device[_AVM_THERMOSTAT]['devicelock']) + except KeyError: + pass + + try: + return int(device[_AVM_SWITCH]['devicelock']) + except KeyError: + pass + + return None + + +def parse_avm_smarthome_device(raw_device: Dict[str, Any]) -> AvmSmartHomeDevice: + return AvmSmartHomeDevice( + battery_low=_get_battery_low(raw_device), + device_lock=_get_device_lock(raw_device), + fbm=int(raw_device['functionbitmask']), + functions=get_avm_device_functions_from_fbm(int(raw_device['functionbitmask'])), + fw_version=str(raw_device['fwversion']), + id=str(raw_device['id']), + identifier=str(raw_device['identifier']), + lock=_get_lock(raw_device), + manufacturer=str(raw_device['manufacturer']), + name=str(raw_device['name']), + present=int(raw_device['present']), + product_name=str(raw_device['productname']), + tx_busy=int(raw_device['txbusy']) if raw_device.get('txbusy') else None, + temperature=AvmTemperature( + celsius=float(raw_device[_AVM_TEMPERATURE]['celsius']) / 10.0, + offset=float(raw_device[_AVM_TEMPERATURE]['offset']) / 10.0, + ) if raw_device.get(_AVM_TEMPERATURE) else None, + thermostat=AvmThermostat( + temp_current=float(raw_device[_AVM_THERMOSTAT]['tist']) / 2.0, + temp_target=float(raw_device[_AVM_THERMOSTAT]['tsoll']) / 2.0, + temp_economic=float(raw_device[_AVM_THERMOSTAT]['absenk']) / 2.0, + temp_comfort=float(raw_device[_AVM_THERMOSTAT]['komfort']) / 2.0, + error_code=int(raw_device[_AVM_THERMOSTAT]['errorcode']), + next_change=AvmNextChange( + end_period=int(raw_device[_AVM_THERMOSTAT][_AVM_NEXT_CHANGE]['endperiod']), + temp_change_to=float(raw_device[_AVM_THERMOSTAT][_AVM_NEXT_CHANGE]['tchange']) / 2.0, + ) if raw_device[_AVM_THERMOSTAT].get(_AVM_NEXT_CHANGE) else None, + ) if raw_device.get(_AVM_THERMOSTAT) else None, + switch=AvmSwitch( + state=int(raw_device[_AVM_SWITCH]['state']), + mode=str(raw_device[_AVM_SWITCH]['mode']), + ) if raw_device.get(_AVM_SWITCH) else None, + power_meter=AvmPowerMeter( + voltage=float(raw_device[_AVM_POWER_METER]['voltage']) / 1000, + power=float(raw_device[_AVM_POWER_METER]['power']) / 1000, + energy=float(raw_device[_AVM_POWER_METER]['energy']), # / 1000, + ) if raw_device.get(_AVM_POWER_METER) else None, + simple_on_off=AvmSimpleOnOff( + state=int(raw_device[_AVM_SIMPLE_ON_OFF]['state']), + ) if raw_device.get(_AVM_SIMPLE_ON_OFF) else None, + ) + + +def get_avm_device_functions_from_fbm(fbm: int) -> List[str]: + functions = [] + if fbm >> 0 & 1: + functions.append('HAN-FUN Device') + if fbm >> 2 & 1: + functions.append('Light') + if fbm >> 3 & 1: + functions.append('unknown (Bit 3)') + if fbm >> 4 & 1: + functions.append('Alarm Sensor') + if fbm >> 5 & 1: + functions.append('AVM Button') + if fbm >> 6 & 1: + functions.append('AVM Thermostat') + if fbm >> 7 & 1: + functions.append('AVM Powermeter') + if fbm >> 8 & 1: + functions.append('Temperature Sensor') + if fbm >> 9 & 1: + functions.append('AVM Switching socket') + if fbm >> 10 & 1: + functions.append('AVM DECT Repeater') + if fbm >> 11 & 1: + functions.append('AVM Microphone') + if fbm >> 12 & 1: + functions.append('unknown (Bit 12)') + if fbm >> 13 & 1: + functions.append('HAN-FUN Unit') + if fbm >> 14 & 1: + functions.append('unknown (Bit 14)') + if fbm >> 15 & 1: + functions.append('on/off switchable device') + if fbm >> 16 & 1: + functions.append('Device with adjustable dimming, height and level') + if fbm >> 17 & 1: + functions.append('Light') + if fbm >> 18 & 1: + functions.append('Roller shutter') + if fbm >> 19 & 1: + functions.append('unknown (Bit 19)') + if fbm >> 20 & 1: + functions.append('Humidity sensor') + + functions.sort() + + return functions diff --git a/source/agents/special/agent_fritzbox_smarthome b/source/agents/special/agent_fritzbox_smarthome new file mode 100755 index 0000000..48c6312 --- /dev/null +++ b/source/agents/special/agent_fritzbox_smarthome @@ -0,0 +1,7 @@ +#!/usr/bin/env python3 +import sys + +from cmk.special_agents.agent_fritzbox_smarthome import main + +if __name__ == "__main__": + sys.exit(main()) diff --git a/source/checkman/fritzbox_smarthome b/source/checkman/fritzbox_smarthome new file mode 100644 index 0000000..2c6df2a --- /dev/null +++ b/source/checkman/fritzbox_smarthome @@ -0,0 +1,11 @@ +title: Fritz!Box Smarthome +agents: fritzbox_smarthome +catalog: hw/network/avm +license: GPL +distribution: check_mk +description: + Comes with a new agent for Fritz!Box smarthome devices. Some values are configurable. + Feel free to report bugs at https://github.com/MaximilianClemens/checkmk_fritzbox/issues/ + +inventory: + fritzbox smarthome devices diff --git a/source/checks/agent_fritzbox_smarthome b/source/checks/agent_fritzbox_smarthome new file mode 100644 index 0000000..26a7854 --- /dev/null +++ b/source/checks/agent_fritzbox_smarthome @@ -0,0 +1,44 @@ +#!/usr/bin/env python3 +# -*- encoding: utf-8; py-indent-offset: 4 -*- +# +# modifications by thl-cmk[at]outlook[dot]com +# 2023-12-18: modified to work with cmk 2.2.x +# changed password to use password store +# + +def agent_fritzbox_smarthome_arguments(params, hostname, ipaddress): + args = [ + ipaddress + ] + + if (password := params.get("password")) is not None: + args.extend(["--password"] + [passwordstore_get_cmdline("%s", password)]) + + if (username := params.get("username")) is not None: + args.extend(["--user"] + [username]) + + if (port := params.get("port")) is not None: + args.extend(["--port"] + [port]) + + if (protocol := params.get("protocol")) is not None: + args.extend(["--protocol"] + [protocol]) + + if (ssl := params.get("ssl")) is not None: + args.append("--ignore_ssl") + + # if (piggyback := params.get("piggyback")) is not None: + # args.append("--piggyback") + + if (prefix := params.get("prefix")) is not None: + args.extend(["--prefix"] + [hostname]) + + if (testing := params.get("testing")) is not None: + args.append("--testing") + + if (no_piggyback := params.get("no_piggyback")) is not None: + args.append("--no-piggyback") + + return args + + +special_agent_info['fritzbox_smarthome'] = agent_fritzbox_smarthome_arguments diff --git a/source/gui/metrics/fritzbox_smarthome.py b/source/gui/metrics/fritzbox_smarthome.py new file mode 100644 index 0000000..7a8fc12 --- /dev/null +++ b/source/gui/metrics/fritzbox_smarthome.py @@ -0,0 +1,80 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# +# License: GNU General Public License v2 +# +# Author: thl-cmk[at]outlook[dot]com +# URL : https://thl-cmk.hopto.org +# Date : 2023-12-28 +# File : fritzbox_smarthome.py (metrics) + +from cmk.gui.i18n import _ +from cmk.gui.plugins.metrics.utils import ( + metric_info, + graph_info, + check_metrics, + perfometer_info, +) + +check_metrics["check_mk-fritzbox_smarthome_thermostat_single"] = { + "temp_current": {"auto_graph": False}, + "temp_target": {"auto_graph": False}, + "temp_economic": {"auto_graph": False}, + "temp_comfort": {"auto_graph": False}, +} + +check_metrics["check_mk-fritzbox_smarthome_thermostat_multiple"] = { + "temp_current": {"auto_graph": False}, + "temp_target": {"auto_graph": False}, + "temp_economic": {"auto_graph": False}, + "temp_comfort": {"auto_graph": False}, +} + +metric_info["temp_current"] = { + "title": _("Temperature current"), + "color": "26/a", + "unit": "c", +} +metric_info["temp_target"] = { + "title": _("Temperature target"), + "color": "21/a", + "unit": "c", +} +metric_info["temp_economic"] = { + "title": _("Temperature economic"), + "color": "31/a", + "unit": "c", +} +metric_info["temp_comfort"] = { + "title": _("Temperature comfort"), + "color": "11/a", + "unit": "c", +} + +graph_info["fritzbox_smart_home_temp_control"] = { + "title": _("Thermostat temperature control"), + "metrics": [ + ("temp_current", "area"), + ("temp_target", "line"), + ], + "scalars": [ + ("temp_comfort", "Temperature comfort"), + ("temp_economic", "Temperature economic"), + ], + "optional_metrics": [ + "temp_target", + ], +} + +perfometer_info.append(('stacked', [ + { + 'type': 'linear', + 'segments': ['temp_current'], + 'total': 50, + }, + { + 'type': 'linear', + 'segments': ['temp_target'], + 'total': 50, + } +])) diff --git a/source/gui/wato/check_parameters/electrical_energy.py b/source/gui/wato/check_parameters/electrical_energy.py new file mode 100644 index 0000000..737f69a --- /dev/null +++ b/source/gui/wato/check_parameters/electrical_energy.py @@ -0,0 +1,76 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# +# License: GNU General Public License v2 +# +# Author: thl-cmk[at]outlook[dot]com +# URL : https://thl-cmk.hopto.org +# Date : 2023-12-29 +# File : electrical_energy.py (WATO) + +from cmk.gui.i18n import _ +from cmk.gui.plugins.wato.utils import ( + CheckParameterRulespecWithItem, + CheckParameterRulespecWithoutItem, + rulespec_registry, + RulespecGroupCheckParametersEnvironment, +) +from cmk.gui.valuespec import Dictionary, Integer, TextInput, Tuple + + +def _item_spec_energy(): + return TextInput( + title=_("Phase"), help=_("The identifier of the phase the power is related to.") + ) + + +def _parameter_valuespec_energy(): + return Dictionary( + title=_('Parameters'), + elements=[ + ( + "levels_upper", + Tuple( + title=_("Upper levels for electrical energy"), + elements=[ + Integer(title=_("warning at"), unit="Wh"), + Integer(title=_("critical at"), unit="Wh"), + ], + ), + ), + ( + "levels_lower", + Tuple( + title=_("Lower levels for electrical energy"), + elements=[ + Integer(title=_("warning if below"), unit="Wh"), + Integer(title=_("critical if below"), unit="Wh"), + ], + ), + ), + ], + help=_( + "Levels for the electrical energy consumption of a device " + "like a UPS or a PDU. Several phases may be addressed independently." + ), + ) + + +rulespec_registry.register( + CheckParameterRulespecWithItem( + check_group_name="energy_multiple", + group=RulespecGroupCheckParametersEnvironment, + item_spec=_item_spec_energy, + parameter_valuespec=_parameter_valuespec_energy, + title=lambda: TextInput(title=_("Electrical Energy (several phases)")), + ) +) + +rulespec_registry.register( + CheckParameterRulespecWithoutItem( + check_group_name="energy_single", + group=RulespecGroupCheckParametersEnvironment, + parameter_valuespec=_parameter_valuespec_energy, + title=lambda: _("Electrical Energy (single phase)"), + ) +) \ No newline at end of file diff --git a/source/gui/wato/check_parameters/epower.py b/source/gui/wato/check_parameters/epower.py new file mode 100644 index 0000000..d595816 --- /dev/null +++ b/source/gui/wato/check_parameters/epower.py @@ -0,0 +1,80 @@ +#!/usr/bin/env python3 +# Copyright (C) 2019 Checkmk GmbH - License: GNU General Public License v2 +# This file is part of Checkmk (https://checkmk.com). It is subject to the terms and +# conditions defined in the file COPYING, which is part of this source code package. + +from cmk.gui.i18n import _ +from cmk.gui.plugins.wato.utils import ( + CheckParameterRulespecWithItem, + CheckParameterRulespecWithoutItem, + rulespec_registry, + RulespecGroupCheckParametersEnvironment, +) +from cmk.gui.valuespec import Dictionary, Integer, Migrate, TextInput, Tuple + + +def _item_spec_epower(): + return TextInput( + title=_("Phase"), help=_("The identifier of the phase the power is related to.") + ) + + +def _migrate(value: tuple | dict) -> dict: + if isinstance(value, tuple): + return {"levels_lower": value} + return value + + +def _parameter_valuespec_epower(): + return Migrate( + Dictionary( + title=_("Parameters"), + elements=[ + ( + "levels_lower", + Tuple( + title=_("Lower levels for electrical power"), + elements=[ + Integer(title=_("warning if below"), unit="Watt"), + Integer(title=_("critical if below"), unit="Watt"), + ], + ), + ), + ( + "levels_upper", + Tuple( + title=_("Upper levels for electrical power"), + elements=[ + Integer(title=_("warning at"), unit="Watt"), + Integer(title=_("critical at"), unit="Watt"), + ], + ), + ), + ], + help=_( + "Levels for the electrical power consumption of a device " + "like a UPS or a PDU. Several phases may be addressed independently." + ), + ), + migrate=_migrate, + ) + + +rulespec_registry.register( + CheckParameterRulespecWithItem( + check_group_name="epower", + group=RulespecGroupCheckParametersEnvironment, + item_spec=_item_spec_epower, + parameter_valuespec=_parameter_valuespec_epower, + title=lambda: _("Electrical Power"), + ) +) + +rulespec_registry.register( + CheckParameterRulespecWithoutItem( + check_group_name="epower_single", + group=RulespecGroupCheckParametersEnvironment, + parameter_valuespec=_parameter_valuespec_epower, + title=lambda: TextInput(title=_("Electrical Power (single phase)")), + ) +) diff --git a/source/gui/wato/check_parameters/fritzbox_smarthome.py b/source/gui/wato/check_parameters/fritzbox_smarthome.py new file mode 100644 index 0000000..a102e0e --- /dev/null +++ b/source/gui/wato/check_parameters/fritzbox_smarthome.py @@ -0,0 +1,139 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# +# License: GNU General Public License v2 +# +# Author: thl-cmk[at]outlook[dot]com +# URL : https://thl-cmk.hopto.org +# Date : 2023-12-28 +# File : fritzbox_smarthome.py (WATO check plugin) +# + + +from cmk.gui.i18n import _ +from cmk.gui.valuespec import ( + Dictionary, + Integer, + MonitoringState, + Tuple, + TextInput, +) +from cmk.gui.plugins.wato.utils import ( + CheckParameterRulespecWithItem, + CheckParameterRulespecWithoutItem, + RulespecGroupCheckParametersApplications, + rulespec_registry, +) + + +def _parameter_valuespec_fritzbox_smarthome(): + return Dictionary( + title=_('Parameter'), + elements=[ + ('present', + MonitoringState( + title=_('Monitoring state for offline devices'), + default_value=1, + )), + ], + ) + + +rulespec_registry.register( + CheckParameterRulespecWithoutItem( + check_group_name="fritzbox_smarthome_single", + group=RulespecGroupCheckParametersApplications, + match_type="dict", + parameter_valuespec=_parameter_valuespec_fritzbox_smarthome, + title=lambda: _('Fritz!Box Smarthome Devices') + ) +) + +rulespec_registry.register( + CheckParameterRulespecWithItem( + check_group_name="fritzbox_smarthome_multiple", + group=RulespecGroupCheckParametersApplications, + match_type="dict", + parameter_valuespec=_parameter_valuespec_fritzbox_smarthome, + title=lambda: _('Fritz!Box Smarthome Devices (with Device-ID)'), + item_spec=lambda: TextInput(title=_('Device-ID')), + ) +) + + +def _parameter_valuespec_fritzbox_smarthome_thermostat(): + return Dictionary( + title=_('Parameter'), + elements=[ + ('deviation', + Tuple( + title=_('Deviation from target temperature'), + help=_('Deviation form target temperature in °C'), + elements=[ + Integer(title=_('Warning'), default_value=3, unit=_('°C')), + Integer(title=_('Critical'), default_value=5, unit=_('°C')), + ])), + ('state_on_error', + MonitoringState( + title=_('Monitoring state on error'), + default_value=1, + )), + ], + ) + + +rulespec_registry.register( + CheckParameterRulespecWithoutItem( + check_group_name="fritzbox_smarthome_thermostat_single", + group=RulespecGroupCheckParametersApplications, + match_type="dict", + parameter_valuespec=_parameter_valuespec_fritzbox_smarthome_thermostat, + title=lambda: _('Fritz!Box Smarthome Thermostat'), + ) +) + +rulespec_registry.register( + CheckParameterRulespecWithItem( + check_group_name="fritzbox_smarthome_thermostat_multiple", + group=RulespecGroupCheckParametersApplications, + match_type="dict", + parameter_valuespec=_parameter_valuespec_fritzbox_smarthome_thermostat, + title=lambda: _('Fritz!Box Smarthome Thermostat (with Device-ID)'), + item_spec=lambda: TextInput(title=_('Device-ID')), + ) +) + + +def _parameter_valuespec_fritzbox_smarthome_battery(): + return Dictionary( + title=_('Parameter'), + elements=[ + ('battery_low', + MonitoringState( + title=_('Monitoring state on low battery'), + default_value=2, + )), + ], + ) + + +rulespec_registry.register( + CheckParameterRulespecWithoutItem( + check_group_name="fritzbox_smarthome_battery_single", + group=RulespecGroupCheckParametersApplications, + match_type="dict", + parameter_valuespec=_parameter_valuespec_fritzbox_smarthome_battery, + title=lambda: _('Fritz!Box Smarthome battery') + ) +) + +rulespec_registry.register( + CheckParameterRulespecWithItem( + check_group_name="fritzbox_smarthome_battery_multiple", + group=RulespecGroupCheckParametersApplications, + match_type="dict", + parameter_valuespec=_parameter_valuespec_fritzbox_smarthome_battery, + title=lambda: _('Fritz!Box Smarthome battery (with Device-ID)'), + item_spec=lambda: TextInput(title=_('Device-ID')), + ) +) diff --git a/source/gui/wato/check_parameters/temperature_single.py b/source/gui/wato/check_parameters/temperature_single.py new file mode 100644 index 0000000..0c9d887 --- /dev/null +++ b/source/gui/wato/check_parameters/temperature_single.py @@ -0,0 +1,28 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# +# License: GNU General Public License v2 +# +# Author: thl-cmk[at]outlook[dot]com +# URL : https://thl-cmk.hopto.org +# Date : 2023-12-29 +# File : temperature_single.py (WATO) + +from cmk.gui.i18n import _ +from cmk.gui.plugins.wato.utils import ( + CheckParameterRulespecWithoutItem, + rulespec_registry, + RulespecGroupCheckParametersEnvironment, +) + +from cmk.gui.plugins.wato.check_parameters.temperature import _parameter_valuespec_temperature + +rulespec_registry.register( + CheckParameterRulespecWithoutItem( + check_group_name="temperature_single", + group=RulespecGroupCheckParametersEnvironment, + match_type="dict", + parameter_valuespec=_parameter_valuespec_temperature, + title=lambda: _("Temperature (without Sensor-ID)"), + ) +) \ No newline at end of file diff --git a/source/gui/wato/check_parameters/voltage_single.py b/source/gui/wato/check_parameters/voltage_single.py new file mode 100644 index 0000000..7fd6761 --- /dev/null +++ b/source/gui/wato/check_parameters/voltage_single.py @@ -0,0 +1,28 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# +# License: GNU General Public License v2 +# +# Author: thl-cmk[at]outlook[dot]com +# URL : https://thl-cmk.hopto.org +# Date : 2023-12-29 +# File : voltage_single.py (WATO) + +from cmk.gui.i18n import _ +from cmk.gui.plugins.wato.utils import ( + CheckParameterRulespecWithoutItem, + rulespec_registry, + RulespecGroupCheckParametersEnvironment, +) + +from cmk.gui.plugins.wato.check_parameters.voltage import _parameter_valuespec_voltage + +rulespec_registry.register( + CheckParameterRulespecWithoutItem( + check_group_name="voltage_single", + group=RulespecGroupCheckParametersEnvironment, + match_type="dict", + parameter_valuespec=_parameter_valuespec_voltage, + title=lambda: _("Voltage Sensor (without Sensor-ID)"), + ) +) diff --git a/source/lib/python3/cmk/special_agents/agent_fritzbox_smarthome.py b/source/lib/python3/cmk/special_agents/agent_fritzbox_smarthome.py new file mode 100644 index 0000000..b669650 --- /dev/null +++ b/source/lib/python3/cmk/special_agents/agent_fritzbox_smarthome.py @@ -0,0 +1,284 @@ +#!/usr/bin/env python3 +# -*- encoding: utf-8; py-indent-offset: 4 -*- +# +# modifications by thl-cmk[at]outlook[dot]com +# 2023-12-18: modified to work with cmk 2.2.x +# changed to return the complete XML response back as json +# 2023-12-28: added data/option for testing + +import sys +import traceback +import ssl +import json +import time + +from urllib.request import urlopen +import argparse +import xml.etree.ElementTree as ET +import hashlib +from re import sub as re_sub +from cmk.utils.password_store import replace_passwords + + +# based on: https://stackoverflow.com/a/47081240 +def parse_xml_to_json(xml): + response = {} + for key in xml.keys(): + response[key] = xml.get(key) + for child in list(xml): + for key in child.keys(): + response[key] = child.get(key) + + if len(list(child)) > 0: + response[child.tag] = parse_xml_to_json(child) + else: + response[child.tag] = child.text or '' + + return response + + +def parse_args(): + parser = argparse.ArgumentParser( + description='Check_MK Fritz!Box Smarthome Agent\n' + 'This is an additional check_MK Fritz!Box Agent which can gather information\'s over the \n' + 'AVM AHA HTTP Interface about SmartHome Devices connected to an Fritz!Box.', + formatter_class=argparse.RawTextHelpFormatter, + ) + parser.add_argument( + 'host', + help='Host name or IP address of your Fritz!Box', + ) + parser.add_argument( + '--debug', action='store_true', default=False, + help='Debug mode: let Python exceptions come through', + ) + parser.add_argument( + '--ignore_ssl', action='store_true', default=False, + help='Tha agent will ignores SSL errors', + ) + parser.add_argument( + '--no-piggyback', action='store_true', default=False, + help='By default the agent generates the output as piggyback data for each\n' + 'Samrthome device. If you want to attach all your Smarthome devices directly\n' + ' to your Fritz!Box use this option.', + ) + parser.add_argument( + '--password', nargs='?', + help='The password to logon the Fritz!Box', + ) + parser.add_argument( + '--username', nargs='?', + help='The username to logon to the Fritz!Box', + ) + parser.add_argument( + '--port', nargs='?', type=int, default=443, + help='The TCP port on witch to access the Fritz!Box', + ) + parser.add_argument( + '--prefix', nargs='?', + help='The prefix is used to group all the Smarthome devices from one Fritz!Box in CMK.' + ) + parser.add_argument( + '--protocol', nargs='?', choices=['http', 'https'], default='https', + help='The protocol used to access the Fritz!Box', + ) + + parser.add_argument( + '--testing', action='store_true', default=False, + help='Development usage only (might be ignored)' + ) + args = parser.parse_args() + + return args + + +def check_fritzbox_smarthome(args): + base_address = '%s://%s:%d' % (args.protocol, args.host, args.port) + + ctx = ssl.create_default_context() + if args.ignore_ssl: + ctx.check_hostname = False + ctx.verify_mode = ssl.CERT_NONE + + # CALL /login_sid.lua + # and grab challenge + response = urlopen(base_address + '/login_sid.lua', context=ctx) + if args.password: + xml_login = ET.fromstring(response.read()) + challenge = xml_login.find('Challenge').text + blocktime = int(xml_login.find('BlockTime').text) + if blocktime > 0: + sys.stdout.write('<<<fritzbox_smarthome:sep(0)>>>') + sys.stdout.write(json.dumps({'block_time': blocktime})) + exit() + + # create challenge_response (hash with md5: '<challenge>-<password>') + # TODO: check if challenge is PBKDF2 (startswith $2) + digest = hashlib.md5() + digest.update(challenge.encode('utf-16le')) + digest.update('-'.encode('utf-16le')) + digest.update(args.password.encode('utf-16le')) + + challenge_response = challenge + '-' + digest.hexdigest() + + # CALL /login_sid.lua?username=<username>&response=<challenge_response> + # and grab sessionid + if args.username: + response = urlopen( + base_address + '/login_sid.lua?username=%s&response=%s' % (args.username, challenge_response), + context=ctx) + else: + response = urlopen(base_address + '/login_sid.lua?response=%s' % challenge_response, context=ctx) + + xml_login_solve = ET.fromstring(response.read()) + sessionid = xml_login_solve.find('SID').text + + blocktime = int(xml_login_solve.find('BlockTime').text) + if blocktime > 0: + sys.stdout.write('<<<fritzbox_smarthome:sep(0)>>>') + sys.stdout.write(json.dumps({'block_time': blocktime})) + exit() + + if args.password and sessionid == '0000000000000000': + raise Exception('Check credentials\n') + + # Write section header + response = urlopen( + base_address + '/webservices/homeautoswitch.lua?switchcmd=getdevicelistinfos&sid=%s' % sessionid, context=ctx) + response_read = response.read() + if args.debug: + sys.stdout.write('Raw XML:\n') + sys.stdout.write(str(response_read)) + sys.stdout.write('\n') + + xml_devicelist = ET.fromstring(response_read) + devices = [] + + if args.testing: + __switch_01 = { + "identifier": "08761 0116372", + "id": "99", + "functionbitmask": "35712", + "fwversion": "04.26", + "manufacturer": "AVM", + "productname": "FRITZ!DECT 200", + "present": "1", + "txbusy": "0", + "name": "TV-living_room", + "switch": { + "state": "1", + "mode": "manuell", + "lock": "0", + "devicelock": "0" + }, + "simpleonoff": { + "state": "1" + }, + "powermeter": { + "voltage": "235814", + "power": "4220", + "energy": "145427" + }, + "temperature": { + "celsius": "190", + "offset": "0" + } + } + __repeater_01 = { + "identifier": "11657 0057950", + "id": "98", + "functionbitmask": "1024", + "fwversion": "04.16", + "manufacturer": "AVM", + "productname": "FRITZ!DECT Repeater 100", + "present": "0", + "txbusy": "0", + "name": "FRITZ!DECT Rep 100 #1" + } + __repeater_02 = { + "identifier": "11657 0170905", + "id": "97", + "functionbitmask": "1280", + "fwversion": "04.25", + "manufacturer": "AVM", + "productname": "FRITZ!DECT Repeater 100", + "present": "1", + "txbusy": "0", + "name": "FRITZ!DECT Repeater 100 #2", + "temperature": { + "celsius": "245", + "offset": "0" + } + } + __thermostat_01 = { + "identifier": "13979 0878454", + "id": "96", + "functionbitmask": "320", + "fwversion": "05.16", + "manufacturer": "AVM", + "productname": "Comet DECT", + "present": "1", + "name": "Temp02", + "temperature": { + "celsius": "210", + "offset": "-10" + }, + "hkr": { + "tist": "42", + "tsoll": "32", + "absenk": "32", + "komfort": "38", + "lock": "1", + "devicelock": "1", + "errorcode": "0", + "batterylow": "0", + "nextchange": { + "endperiod": "1704888000", + "tchange": "32" + } + } + } + + energy = int(__switch_01["powermeter"]["energy"]) + power = int(__switch_01["powermeter"]["power"]) + start_time = 1703883617 + energy_up = int(time.time() - start_time) / 3600 * (int(power) / 1000) + __switch_01["powermeter"]["energy"] = str(int(energy + energy_up)) + + devices.append(__switch_01) + devices.append(__repeater_01) + devices.append(__repeater_02) + devices.append(__thermostat_01) + + for xml_device in xml_devicelist.findall('device'): + devices.append(parse_xml_to_json(xml_device)) + + if args.no_piggyback: + sys.stdout.write('<<<fritzbox_smarthome:sep(0)>>>\n') + # if len(devices) == 1: + # sys.stdout.write(json.dumps(devices[0])) # single device + # else: + sys.stdout.write(json.dumps(devices)) + sys.stdout.write('\n') + else: + for json_device in devices: + name = json_device["name"].replace(' ', '_') + name = re_sub(r'[^.\-_a-zA-Z0-9]', '', name) + if args.prefix: + name = f'{args.prefix}-{name}' + sys.stdout.write(f'<<<<{name}>>>>\n') + sys.stdout.write('<<<fritzbox_smarthome:sep(0)>>>\n') + sys.stdout.write(json.dumps(json_device)) + sys.stdout.write('\n') + + +def main(): + replace_passwords() + args = parse_args() + try: + check_fritzbox_smarthome(args) + except: + if args.debug: + raise + sys.stderr.write('fritzbox_smarthome\n %s\n' % traceback.format_exc()) + sys.exit(2) diff --git a/source/packages/fritzbox_smarthome b/source/packages/fritzbox_smarthome new file mode 100644 index 0000000..89a5584 --- /dev/null +++ b/source/packages/fritzbox_smarthome @@ -0,0 +1,55 @@ +{'author': 'Th.L. (thl-cmk[at]outlook[dot]com)', + 'description': 'Agent and checks/inventory for Fritz!Box smart home devices\n' + '\n' + 'This package is based on the work of Maximilian Clemens, ' + 'see \n' + 'https://github.com/MaximilianClemens/checkmk_fritzbox\n' + '\n' + 'I have rewritten this package for use with CMK 2.2.0x. As I ' + 'do not have access to all smart home \n' + 'devices, I have only implemented the checks for the following ' + 'devices:\n' + '\n' + 'FRITZ!DECT Repeater 100\n' + 'FRITZ!DECT 200\n' + 'FRITZ!DECT 302\n' + '\n' + 'So if you want the package to be extended to support your ' + 'sensors as well, see\n' + 'https://thl-cmk.hopto.org/gitlab/checkmk/various/fritzbox_smarthome/-/blob/master/CONTRIBUTING.md\n' + '\n' + 'Also, my FRIT!BOX is not brand new, so it may not include all ' + 'the features that the smart home \n' + 'devices support. E.g. window open/close for the FRITZ!DECT ' + '302.\n' + '\n', + 'download_url': 'https://github.com/MaximilianClemens/checkmk_fritzbox', + 'files': {'agent_based': ['fritzbox_smarthome.py', + 'fritzbox_smarthome_power_meter.py', + 'fritzbox_smarthome_temperature.py', + 'fritzbox_smarthome_thermostat.py', + 'inv_fritzbox_smarthome.py', + 'utils/fritzbox_smarthome.py', + 'fritzbox_smarthome_battery.py', + 'fritzbox_smarthome_app_lock.py', + 'fritzbox_smarthome_device_lock.py', + 'fritzbox_smarthome_power_socket.py', + 'fritzbox_smarthome_switch.py'], + 'agents': ['special/agent_fritzbox_smarthome'], + 'checkman': ['fritzbox_smarthome'], + 'checks': ['agent_fritzbox_smarthome'], + 'gui': ['wato/check_parameters/electrical_energy.py', + 'wato/check_parameters/epower.py', + 'wato/check_parameters/fritzbox_smarthome.py', + 'metrics/fritzbox_smarthome.py', + 'wato/check_parameters/temperature_single.py', + 'wato/check_parameters/voltage_single.py'], + 'lib': ['python3/cmk/special_agents/agent_fritzbox_smarthome.py'], + 'web': ['plugins/wato/agent_fritzbox_smarthome.py', + 'plugins/views/fritzbox_smarthome.py']}, + 'name': 'fritzbox_smarthome', + 'title': 'Fritz!Box SmartHome', + 'version': '0.8.3-20231230', + 'version.min_required': '2.2.0b1', + 'version.packaged': '2.2.0p14', + 'version.usable_until': None} diff --git a/source/web/plugins/views/fritzbox_smarthome.py b/source/web/plugins/views/fritzbox_smarthome.py new file mode 100644 index 0000000..76733ec --- /dev/null +++ b/source/web/plugins/views/fritzbox_smarthome.py @@ -0,0 +1,40 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# +# License: GNU General Public License v2 +# +# Author: thl-cmk[at]outlook[dot]com +# URL : https://thl-cmk.hopto.org +# Date : 2023-12-28 +# File : fritzbox_smarthome.py (views) + + +from cmk.gui.views.inventory.registry import inventory_displayhints + +from cmk.gui.i18n import _l + +inventory_displayhints.update({ + '.hardware.avm.': { + 'title': _l('AVM'), + }, + '.hardware.avm.smart_home_devices:': { + 'title': _l('Smart home devices'), + 'view': 'invavmsmarthomedevices_of_host', + 'keyorder': [ + 'id', + 'name', + 'manufacturer', + 'product_name', + 'fw_version', + 'identifier', + 'functions', + ] + }, + '.hardware.avm.smart_home_devices:*.id': {'title': _l('ID')}, + '.hardware.avm.smart_home_devices:*.name': {'title': _l('Name')}, + '.hardware.avm.smart_home_devices:*.manufacturer': {'title': _l('Manufacturer')}, + '.hardware.avm.smart_home_devices:*.product_name': {'title': _l('Product name')}, + '.hardware.avm.smart_home_devices:*.fw_version': {'title': _l('Firmware version')}, + '.hardware.avm.smart_home_devices:*.identifier': {'title': _l('Identifier')}, + '.hardware.avm.smart_home_devices:*.functions': {'title': _l('Functions')}, +}) diff --git a/source/web/plugins/wato/agent_fritzbox_smarthome.py b/source/web/plugins/wato/agent_fritzbox_smarthome.py new file mode 100644 index 0000000..cc2f867 --- /dev/null +++ b/source/web/plugins/wato/agent_fritzbox_smarthome.py @@ -0,0 +1,91 @@ +#!/usr/bin/env python3 +# -*- encoding: utf-8; py-indent-offset: 4 -*- + +# +# modifications by thl-cmk[at]outlook[dot]com +# 2023-12-18: modified to work with cmk 2.2.x +# changed password to use password store +# + +from cmk.gui.i18n import _ +from cmk.gui.plugins.wato.special_agents.common import RulespecGroupDatasourceProgramsHardware +from cmk.gui.plugins.wato.utils import ( + HostRulespec, + rulespec_registry, + IndividualOrStoredPassword, +) +from cmk.gui.valuespec import ( + Dictionary, + FixedValue, + TextAscii, + Integer, + ValueSpec, + DropdownChoice, +) + + +def _valuespec_special_agents_fritzbox_smarthome() -> ValueSpec: + return Dictionary( + title=_("Fritz!Box Smarthome Devices"), + help=_("This rule selects the Fritz!Box agent, which uses HTTP to gather information " + "about configuration and connection status information."), + elements=[ + ('username', + TextAscii( + title=_('Username'), + help=_('Username for the Fritz!Box') + )), + ("password", IndividualOrStoredPassword( + title=_("Password"), + allow_empty=False, + help=_('Password for the Fritz!Box.') + )), + ('port', + Integer( + title=_('Port'), + default_value=443, + )), + ('protocol', + DropdownChoice( + title=_('Protocol'), + choices=[ + ('http', 'HTTP'), + ('https', 'HTTPS'), + ], + default='https', + )), + ('ssl', FixedValue( + value=0, + totext='', + title=_('Ignore SSL errors'), + )), + ('prefix', FixedValue( + value=True, + help='Uses the hostname of the Fritz!Box as prefix for the hostnames generated for piggyback', + totext='', + title=_('Add Prefix'), + )), + ('no_piggyback', FixedValue( + value=True, + help='The agent will not generate piggyback data. ' + 'The Smarthome devices will be attached to the this host.', + totext='', + title=_('Disable piggyback'), + )), + ('testing', FixedValue( + value=True, + help='Development only, will be (most likely) ignored in production :-)', + totext='Add test data to the agent output', + title=_('Add test data'), + )), + ], + required_keys=['username', 'password'] + ) + + +rulespec_registry.register( + HostRulespec( + group=RulespecGroupDatasourceProgramsHardware, + name="special_agents:fritzbox_smarthome", + valuespec=_valuespec_special_agents_fritzbox_smarthome, + )) -- GitLab