diff --git a/mkp/fritzbox_smarthome-0.8.3-20231230.mkp b/mkp/fritzbox_smarthome-0.8.3-20231230.mkp index 3861f7e52d2a579d8db61d0d1c8e5f2b03cd398c..fc445537db31dbe328abb9dbd0a7a0155cd678ae 100644 Binary files a/mkp/fritzbox_smarthome-0.8.3-20231230.mkp and b/mkp/fritzbox_smarthome-0.8.3-20231230.mkp differ diff --git a/source/agent_based/fritzbox_smarthome.py b/source/agent_based/fritzbox_smarthome.py new file mode 100644 index 0000000000000000000000000000000000000000..95a42b578dee78829556392410c088541e4f070e --- /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 0000000000000000000000000000000000000000..b52314e868c9feb5c542d517f7c46be96c46b179 --- /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 0000000000000000000000000000000000000000..61d597b61e08a8b772b93bc69a952ca0ef4cd051 --- /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 0000000000000000000000000000000000000000..6c088fd7a7b41578eb49c70221a78d1b59725bd1 --- /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 0000000000000000000000000000000000000000..6e1c0968b4c9cf0b14806af2775b0695eecd1311 --- /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 0000000000000000000000000000000000000000..d702cb982da21eb64265129736411316c179fc0d --- /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 0000000000000000000000000000000000000000..7bfeac10cc0d0b5123851487a22971cc57557b52 --- /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 0000000000000000000000000000000000000000..42875b7c17593e59a6768cede99a60a3133aa6b4 --- /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 0000000000000000000000000000000000000000..2579f61cc82f0cece617d8049186e270016959b9 --- /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 0000000000000000000000000000000000000000..0f77bf5b73d0c1de58c04821adcecfeb1595adf5 --- /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 0000000000000000000000000000000000000000..b6a9137d4fd0e3e03c0ef29975b1cec5f622fc9c --- /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 0000000000000000000000000000000000000000..48c63127519dda4b07ce0bc284eab2517e47dee4 --- /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 0000000000000000000000000000000000000000..2c6df2a45ae403004c2582d87869b5b437cbeebf --- /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 0000000000000000000000000000000000000000..26a785476309e6f6aae7e43c7d47bd8579f0f397 --- /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 0000000000000000000000000000000000000000..7a8fc1255de4954df05fb55b76f73ae7034464d6 --- /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 0000000000000000000000000000000000000000..737f69ae25d7f62b7be6054d2fd7d101babd8f2f --- /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 0000000000000000000000000000000000000000..d5958163afc4378f683fad50d580c97262e4931b --- /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 0000000000000000000000000000000000000000..a102e0e302422008fe6265402ee038d19651bb58 --- /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 0000000000000000000000000000000000000000..0c9d88797128a4367acbb287ca580b4c5f8bf5f3 --- /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 0000000000000000000000000000000000000000..7fd6761a10438b84ac57cd446909475e456d6cc8 --- /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 0000000000000000000000000000000000000000..b6696503e4188220a6e1d5ce6a2e53ca2ee23581 --- /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 0000000000000000000000000000000000000000..89a5584d6efd6638ca68908f10753cb91b674a6c --- /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 0000000000000000000000000000000000000000..76733ecaa3209fc5d02fec3438979d4717bb926a --- /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 0000000000000000000000000000000000000000..cc2f86736981940299dbac01368d26e0920ba7d8 --- /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, + ))