From bd167be0f94765d5523dc34888d1d63b57bbd970 Mon Sep 17 00:00:00 2001 From: "th.l" <thl-cmk@outlook.com> Date: Sat, 30 Dec 2023 19:54:20 +0100 Subject: [PATCH] update project --- README.md | 2 +- agent_based/fritzbox_smarthome.py | 517 +++--------------- agent_based/fritzbox_smarthome_app_lock.py | 89 +++ agent_based/fritzbox_smarthome_battery.py | 92 ++++ agent_based/fritzbox_smarthome_device_lock.py | 89 +++ agent_based/fritzbox_smarthome_power_meter.py | 239 ++++++++ .../fritzbox_smarthome_power_socket.py | 91 +++ agent_based/fritzbox_smarthome_switch.py | 91 +++ agent_based/fritzbox_smarthome_temperature.py | 93 ++++ agent_based/fritzbox_smarthome_thermostat.py | 148 +++++ agent_based/inv_fritzbox_smarthome.py | 46 ++ agent_based/utils/fritzbox_smarthome.py | 238 ++++++++ checks/agent_fritzbox_smarthome | 12 +- gui/metrics/fritzbox_smarthome.py | 80 +++ .../check_parameters/electrical_energy.py | 76 +++ gui/wato/check_parameters/epower.py | 80 +++ .../check_parameters/fritzbox_smarthome.py | 139 +++++ .../check_parameters/temperature_single.py | 28 + gui/wato/check_parameters/voltage_single.py | 28 + .../agent_fritzbox_smarthome.py | 104 +++- mkp/fritzbox_smarthome-0.8.2-20231230.mkp | Bin 0 -> 13425 bytes packages/fritzbox_smarthome | 22 +- web/plugins/wato/agent_fritzbox_smarthome.py | 16 +- 23 files changed, 1853 insertions(+), 467 deletions(-) create mode 100644 agent_based/fritzbox_smarthome_app_lock.py create mode 100644 agent_based/fritzbox_smarthome_battery.py create mode 100644 agent_based/fritzbox_smarthome_device_lock.py create mode 100644 agent_based/fritzbox_smarthome_power_meter.py create mode 100644 agent_based/fritzbox_smarthome_power_socket.py create mode 100644 agent_based/fritzbox_smarthome_switch.py create mode 100644 agent_based/fritzbox_smarthome_temperature.py create mode 100644 agent_based/fritzbox_smarthome_thermostat.py create mode 100644 agent_based/inv_fritzbox_smarthome.py create mode 100644 agent_based/utils/fritzbox_smarthome.py create mode 100644 gui/metrics/fritzbox_smarthome.py create mode 100644 gui/wato/check_parameters/electrical_energy.py create mode 100644 gui/wato/check_parameters/epower.py create mode 100644 gui/wato/check_parameters/fritzbox_smarthome.py create mode 100644 gui/wato/check_parameters/temperature_single.py create mode 100644 gui/wato/check_parameters/voltage_single.py create mode 100644 mkp/fritzbox_smarthome-0.8.2-20231230.mkp diff --git a/README.md b/README.md index 3f09eb9..8a58ce7 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -[PACKAGE]: ../../raw/master/mkp/fritzbox_smarthome-0.7.0-20231228.mkp "fritzbox_smarthome-0.7.0-20231228.mkp" +[PACKAGE]: ../../raw/master/mkp/fritzbox_smarthome-0.8.2-20231230.mkp "fritzbox_smarthome-0.8.2-20231230.mkp" # Title A short description about the plugin diff --git a/agent_based/fritzbox_smarthome.py b/agent_based/fritzbox_smarthome.py index fabe357..1159ac6 100644 --- a/agent_based/fritzbox_smarthome.py +++ b/agent_based/fritzbox_smarthome.py @@ -13,255 +13,36 @@ # import json -from dataclasses import dataclass -from typing import Dict, List -from time import strftime, gmtime -from time import time as time_now +from typing import Dict from cmk.base.plugins.agent_based.agent_based_api.v1 import ( - Metric, Result, Service, State, - TableRow, - check_levels, - get_rate, - GetRateError, - get_value_store, register, ) from cmk.base.plugins.agent_based.agent_based_api.v1.type_defs import ( CheckResult, DiscoveryResult, - InventoryResult, StringTable, ) -from cmk.base.plugins.agent_based.utils.temperature import check_temperature -from cmk.utils.render import physical_precision - -@dataclass(frozen=True) -class AvmTemperature: - celsius: float - offset: float - - -@dataclass(frozen=True) -class AvmPowerMeter: - voltage: float - power: float - energy: float - - -@dataclass(frozen=True) -class AvmSimpleOnOff: - state: int - - -@dataclass(frozen=True) -class AvmNextChange: - end_period: int - temp_change_to: float - - -@dataclass(frozen=True) -class AvmHkr: - temp_current: float - temp_target: float - temp_economic: float - temp_comfort: float - lock: int - device_lock: int - error_code: int - battery_low: int - next_change: AvmNextChange | None = None - # summer_active: int | None = None - # window_open_activ: int | None = None - # battery: float | None = None - - -@dataclass(frozen=True) -class AvmSwitch: - state: int - mode: str - lock: int - device_lock: int - - -@dataclass(frozen=True) -class AvmSmartHomeDevice: - functions: List[str] - identifier: str - name: str - id: str - fbm: int - fw_version: str - manufacturer: str - product_name: str - present: int - tx_busy: int | None = None - temperature: AvmTemperature | None = None - hkr: AvmHkr | None = None - switch: AvmSwitch | None = None - power_meter: AvmPowerMeter | None = None - simple_on_off: AvmSimpleOnOff | None = None - - -_TIME_FORMAT = '%Y-%m-%dT%H:%M:%S' -_AVM_HKR = 'hkr' -_AVM_SWITCH = 'switch' -_AVM_POWER_METER = 'powermeter' -_AVM_TEMPERATURE = 'temperature' -_AVM_SIMPLE_ON_OFF = 'simpleonoff' -_AVM_NEXT_CHANGE = 'nextchange' - -__thermostat = [ - { - "identifier": "13979 0878454", - "id": "16", - "functionbitmask": "320", - "fwversion": "05.16", - "manufacturer": "AVM", - "productname": "Comet DECT", - "present": "1", - "name": "Temp01", - "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" - } - } - } -] - -__switch = [ - { - "identifier": "08761 0116372", - "id": "17", - "functionbitmask": "35712", - "fwversion": "04.26", - "manufacturer": "AVM", - "productname": "FRITZ!DECT 200", - "present": "1", - "txbusy": "0", - "name": "TV-Wohnzimmer", - "switch": { - "state": "1", - "mode": "manuell", - "lock": "0", - "devicelock": "0" - }, - "simpleonoff": { - "state": "1" - }, - "powermeter": { - "voltage": "235814", - "power": "4220", - "energy": "145427" - }, - "temperature": { - "celsius": "190", - "offset": "0" - } - } -] - - -def _get_avm_device_functions_from_fbm(fbm: int) -> List[str]: - functions = [] - if fbm >> 0 & 1: - functions.append('HAN-FUN Device') - if fbm >> 13 & 1: - functions.append('HAN-FUN Unit') - if fbm >> 4 & 1: - functions.append('Alarm Sensor') - if fbm >> 5 & 1: - functions.append('Button') - if fbm >> 6 & 1: - functions.append('Thermostat') - if fbm >> 9 & 1: - functions.append('Switch') - if fbm >> 7 & 1: - functions.append('Powermeter') - if fbm >> 10 & 1: - functions.append('DECT Repeater') - if fbm >> 8 & 1: - functions.append('Temperature Sensor') - if fbm >> 11 & 1: - functions.append('Microphone') - if fbm >> 17 & 1: - functions.append('Light') - if fbm >> 2 & 1: - functions.append('Light') - - return functions +from cmk.base.plugins.agent_based.utils.fritzbox_smarthome import ( + AvmSmartHomeDevice, + parse_avm_smarthome_device, +) -def parse_fritzbox_smarthome(string_table: StringTable) -> Dict[str, AvmSmartHomeDevice] | None: +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 - devices: Dict[str, AvmSmartHomeDevice] = { - raw_device['id']: AvmSmartHomeDevice( - functions=_get_avm_device_functions_from_fbm(int(raw_device['functionbitmask'])), - identifier=str(raw_device['identifier']), - name=str(raw_device['name']), - id=str(raw_device['id']), - fbm=int(raw_device['functionbitmask']), - fw_version=str(raw_device['fwversion']), - manufacturer=str(raw_device['manufacturer']), - product_name=str(raw_device['productname']), - present=int(raw_device['present']), - 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, - hkr=AvmHkr( - temp_current=float(raw_device[_AVM_HKR]['tist']) / 2.0, - temp_target=float(raw_device[_AVM_HKR]['tsoll']) / 2.0, - temp_economic=float(raw_device[_AVM_HKR]['absenk']) / 2.0, - temp_comfort=float(raw_device[_AVM_HKR]['komfort']) / 2.0, - lock=int(raw_device[_AVM_HKR]['lock']), - device_lock=int(raw_device[_AVM_HKR]['devicelock']), - error_code=int(raw_device[_AVM_HKR]['errorcode']), - battery_low=int(raw_device[_AVM_HKR]['batterylow']), - next_change=AvmNextChange( - end_period=int(raw_device[_AVM_HKR][_AVM_NEXT_CHANGE]['endperiod']), - temp_change_to=float(raw_device[_AVM_HKR][_AVM_NEXT_CHANGE]['tchange']) / 2.0, - ) if raw_device.get(_AVM_NEXT_CHANGE) else None, - ) if raw_device.get(_AVM_HKR) else None, - switch=AvmSwitch( - state=int(raw_device[_AVM_SWITCH]['state']), - mode=str(raw_device[_AVM_SWITCH]['mode']), - lock=int(raw_device[_AVM_SWITCH]['lock']), - device_lock=int(raw_device[_AVM_SWITCH]['devicelock']) - ) 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, - ) for raw_device in raw_devices - } - return devices + 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( @@ -270,253 +51,87 @@ register.agent_section( ) -def _check_fritzbox_smarthome_hkr(device: AvmSmartHomeDevice, params): - _lock = { - 0: 'no', - 1: 'yes', - } - _device_lock = { - 0: 'no', - 1: 'yes', - } - _battery_low = { - 0: 'no', - 1: 'yes', - } - - _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', - } - - deviation = device.hkr.temp_current - device.hkr.temp_target - _message = f'Temperature current: {device.hkr.temp_current}°C (deviation from target {deviation}°C)' - if params['hkr'].get('deviation'): - warn, crit = params['hkr']['deviation'] - if abs(deviation) >= crit: - yield Result(state=State.CRIT, notice=_message) - elif abs(deviation) >= warn: - yield Result(state=State.WARN, notice=_message) - else: - yield Result(state=State.OK, notice=_message) - else: - yield Result(state=State.OK, notice=_message) - - if deviation == 0: - yield Result(state=State.OK, notice=f'Temperature target: {device.hkr.temp_target}°C') - else: - yield Result( - state=State.OK, - summary=f'Target: {device.hkr.temp_target}°C', - details=f'Temperature target: {device.hkr.temp_target}°C', - ) - yield Result(state=State.OK, notice=f'Temperature economic: {device.hkr.temp_economic}°C') - yield Result(state=State.OK, notice=f'Temperature comfort: {device.hkr.temp_comfort}°C') - - yield Metric(name='temp_current', value=device.hkr.temp_current) - yield Metric(name='temp_target', value=device.hkr.temp_target) - yield Metric(name='temp_comfort', value=device.hkr.temp_comfort) - yield Metric(name='temp_economic', value=device.hkr.temp_economic) - - _message = f'Battery low: {_battery_low.get(device.hkr.battery_low, f"unknown ({device.hkr.battery_low})")}' - if device.hkr.battery_low == 0: - yield Result(state=State.OK, notice=_message) - else: - yield Result(state=State(params['hkr'].get('battery_low', 1)), notice=_message) - - _status = _device_lock.get(device.hkr.device_lock, f'unknown ({device.hkr.device_lock})') - _message = f'Button lock on the thermostat active: {_status}' - yield Result(state=State.OK, notice=_message) - - _status = _lock.get(device.hkr.lock, f'unknown {device.hkr.lock}') - _message = f'Deactivate manual access for phone, app or user interface: {_status}' - yield Result(state=State.OK, notice=_message) - - if device.hkr.next_change: - yield Result( - state=State.OK, - notice=f'End of period: {strftime(_TIME_FORMAT,gmtime(device.hkr.next_change.end_period))}' - ) - yield Result(state=State.OK, notice=f'Change Temperature to: {device.hkr.next_change.temp_change_to}°C') - - _message = f'Error code: {_error_codes.get(device.hkr.error_code, f"unknown {device.hkr.error_code}")}' - if device.hkr.error_code == 0: - yield Result(state=State.OK, notice=_message) - else: - yield Result(state=State(params['hkr'].get('state_on_error', 1)), notice=_message) - - -def _check_fritzbox_smarthome_powermeter(device: AvmSmartHomeDevice, params): - try: - energy = get_rate( - value_store=get_value_store(), - key='energy', - time=time_now(), - value=device.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") - ) - - yield Result( - state=State.OK, - notice=f'Consumption total: {physical_precision(v=device.power_meter.energy, precision=3, unit_symbol="Wh")}' - ) - - yield from check_levels( - value=device.power_meter.voltage, - metric_name='voltage', - label='Voltage', - render_func=lambda x: f'{x}V' - ) - yield from check_levels( - value=device.power_meter.power, - metric_name='power', - label='Power', - render_func=lambda x: f'{x}W' - ) - +def discovery_fritzbox_smarthome_single( + section: AvmSmartHomeDevice | Dict[str, AvmSmartHomeDevice] +) -> DiscoveryResult: + if isinstance(section, AvmSmartHomeDevice): + yield Service() -def _check_fritzbox_smarthome_temperature(device: AvmSmartHomeDevice, params): - _params = { - 'levels': (30, 35), - 'levels_lower': (-30, -35), - 'device_levels_handling': 'usr', - } - yield from check_temperature( - reading=device.temperature.celsius, - params=None, - unique_name=None, - value_store=None, - dev_levels=None, - dev_levels_lower=None, - dev_status=None, - dev_status_name=None - ) - if device.temperature.offset != 0: - _status = device.temperature.celsius + device.temperature.offset * -1 - _message = f'Temperature measured at the thermostat : {_status}' - yield Result(state=State.OK, notice=_message) - yield Result(state=State.OK, notice=f'Temperature offset: {device.temperature.offset}') +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_simple_on_off(device: AvmSmartHomeDevice, params): - _simple_onf_off_state = { - 0: 'off', - 1: 'on', - } - _state = device.simple_on_off.state - yield Result( - state=State.OK, - summary=f'Circuit state: {_simple_onf_off_state.get(_state, f"unknown ({_state}")}' - ) +def check_fritzbox_smarthome_single( + params, section: AvmSmartHomeDevice | Dict[str, AvmSmartHomeDevice] +) -> CheckResult: + if not isinstance(section, AvmSmartHomeDevice): + return -def _check_fritzbox_smarthome_switch(device: AvmSmartHomeDevice, params): - _switch_state = { - 0: 'off', - 1: 'on', - } - _switch_locked = { + _tx_busy = { 0: 'no', 1: 'yes', } - _switch_device_lock = { - 0: 'no', - 1: 'yes', - } - _state = device.switch.state - _lock = device.switch.lock - _dev_lock = device.switch.device_lock - yield Result(state=State.OK, summary=f'Switch state: {_switch_state.get(_state, f"unknown ({_state})")}') - yield Result(state=State.OK, notice=f'Switch mode: {device.switch.mode}') - yield Result(state=State.OK, notice=f'Switch lock: {_switch_state.get(_lock, f"unknown ({_lock})")}') - yield Result(state=State.OK, notice=f'Switch device lock: {_switch_state.get(_dev_lock, f"unknown ({_dev_lock})")}') - - -def discovery_fritzbox_smarthome(section: Dict[str, AvmSmartHomeDevice]) -> DiscoveryResult: - for device in section.values(): - yield Service(item=f'{device.id} {device.name}') - -def check_fritzbox_smarthome(item, params, section: Dict[str, AvmSmartHomeDevice]) -> CheckResult: - try: - device = section[item.split(' ')[0]] - except KeyError: - return - - # move to inventory yield Result( state=State.OK, - notice=f'Device: {device.manufacturer} {device.product_name}, FW: {device.fw_version}' + notice=f'Device: {section.manufacturer} {section.product_name}, FW: {section.fw_version}' ) - if device.present == 0: - yield Result(state=State(params['present']), notice='Device offline') + if section.present == 0: + yield Result(state=State(params['present']), summary='Device is offline') # stop if device is not present return - if device.temperature: - yield from _check_fritzbox_smarthome_temperature(device=device, params=params) - if device.power_meter: - yield from _check_fritzbox_smarthome_powermeter(device=device, params=params) - if device.simple_on_off: - yield from _check_fritzbox_smarthome_simple_on_off(device=device, params=params) - if device.switch: - yield from _check_fritzbox_smarthome_switch(device=device, params=params) - if device.hkr: - yield from _check_fritzbox_smarthome_hkr(device, params) + 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}]') register.check_plugin( - name="fritzbox_smarthome", - service_name="SmartDevice %s", - discovery_function=discovery_fritzbox_smarthome, - check_function=check_fritzbox_smarthome, - check_ruleset_name="fritzbox_smarthome", + name="fritzbox_smarthome_single", + service_name="Smarthome", + sections=['fritzbox_smarthome'], + discovery_function=discovery_fritzbox_smarthome_single, + check_function=check_fritzbox_smarthome_single, + check_ruleset_name="fritzbox_smarthome_single", check_default_parameters={ 'present': 1, - 'hkr': {}, 'switch': {}, - 'power_meter': {}, - 'temperature': {}, - 'humidity': {} } ) -def inventory_fritzbox_smarthome(section: Dict[str, AvmSmartHomeDevice]) -> InventoryResult: - path = ['hardware', 'avm', 'smart_home_devices'] - for device in section.values(): - 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) - } - ) - - -register.inventory_plugin( - name="inv_fritzbox_smarthome", +register.check_plugin( + name="fritzbox_smarthome_multiple", + service_name="Smarthome %s", sections=['fritzbox_smarthome'], - inventory_function=inventory_fritzbox_smarthome, + discovery_function=discovery_fritzbox_smarthome_multiple, + check_function=check_fritzbox_smarthome_multiple, + check_ruleset_name="fritzbox_smarthome_multiple", + check_default_parameters={ + 'present': 1, + 'switch': {}, + } ) diff --git a/agent_based/fritzbox_smarthome_app_lock.py b/agent_based/fritzbox_smarthome_app_lock.py new file mode 100644 index 0000000..b52314e --- /dev/null +++ b/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/agent_based/fritzbox_smarthome_battery.py b/agent_based/fritzbox_smarthome_battery.py new file mode 100644 index 0000000..61d597b --- /dev/null +++ b/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/agent_based/fritzbox_smarthome_device_lock.py b/agent_based/fritzbox_smarthome_device_lock.py new file mode 100644 index 0000000..6c088fd --- /dev/null +++ b/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/agent_based/fritzbox_smarthome_power_meter.py b/agent_based/fritzbox_smarthome_power_meter.py new file mode 100644 index 0000000..6e1c096 --- /dev/null +++ b/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/agent_based/fritzbox_smarthome_power_socket.py b/agent_based/fritzbox_smarthome_power_socket.py new file mode 100644 index 0000000..d702cb9 --- /dev/null +++ b/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/agent_based/fritzbox_smarthome_switch.py b/agent_based/fritzbox_smarthome_switch.py new file mode 100644 index 0000000..7bfeac1 --- /dev/null +++ b/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/agent_based/fritzbox_smarthome_temperature.py b/agent_based/fritzbox_smarthome_temperature.py new file mode 100644 index 0000000..42875b7 --- /dev/null +++ b/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/agent_based/fritzbox_smarthome_thermostat.py b/agent_based/fritzbox_smarthome_thermostat.py new file mode 100644 index 0000000..f2c748d --- /dev/null +++ b/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_target}°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/agent_based/inv_fritzbox_smarthome.py b/agent_based/inv_fritzbox_smarthome.py new file mode 100644 index 0000000..0f77bf5 --- /dev/null +++ b/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/agent_based/utils/fritzbox_smarthome.py b/agent_based/utils/fritzbox_smarthome.py new file mode 100644 index 0000000..b6a9137 --- /dev/null +++ b/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/checks/agent_fritzbox_smarthome b/checks/agent_fritzbox_smarthome index a3918e3..26a7854 100644 --- a/checks/agent_fritzbox_smarthome +++ b/checks/agent_fritzbox_smarthome @@ -25,9 +25,19 @@ def agent_fritzbox_smarthome_arguments(params, hostname, ipaddress): if (ssl := params.get("ssl")) is not None: args.append("--ignore_ssl") - if (testing := params.get("ssl")) is not None: + + # 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 diff --git a/gui/metrics/fritzbox_smarthome.py b/gui/metrics/fritzbox_smarthome.py new file mode 100644 index 0000000..7a8fc12 --- /dev/null +++ b/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/gui/wato/check_parameters/electrical_energy.py b/gui/wato/check_parameters/electrical_energy.py new file mode 100644 index 0000000..737f69a --- /dev/null +++ b/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/gui/wato/check_parameters/epower.py b/gui/wato/check_parameters/epower.py new file mode 100644 index 0000000..d595816 --- /dev/null +++ b/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/gui/wato/check_parameters/fritzbox_smarthome.py b/gui/wato/check_parameters/fritzbox_smarthome.py new file mode 100644 index 0000000..a102e0e --- /dev/null +++ b/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/gui/wato/check_parameters/temperature_single.py b/gui/wato/check_parameters/temperature_single.py new file mode 100644 index 0000000..0c9d887 --- /dev/null +++ b/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/gui/wato/check_parameters/voltage_single.py b/gui/wato/check_parameters/voltage_single.py new file mode 100644 index 0000000..7fd6761 --- /dev/null +++ b/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/lib/python3/cmk/special_agents/agent_fritzbox_smarthome.py b/lib/python3/cmk/special_agents/agent_fritzbox_smarthome.py index c0c2bad..7e6efef 100644 --- a/lib/python3/cmk/special_agents/agent_fritzbox_smarthome.py +++ b/lib/python3/cmk/special_agents/agent_fritzbox_smarthome.py @@ -6,11 +6,17 @@ # changed to return the complete XML response back as json # 2023-12-28: added data/option for testing -import sys, traceback, ssl, json +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 @@ -34,14 +40,15 @@ def parse_xml_to_json(xml): def parse_args(): parser = argparse.ArgumentParser(description='Check_MK Fritz!Box Smarthome Agent') parser.add_argument('host', help='Host name or IP address of your Fritz!Box') - parser.add_argument('--username', nargs='?') - parser.add_argument('--password', nargs='?') parser.add_argument('--debug', action='store_true', default=False, help='Debug mode: let Python exceptions come through') - - parser.add_argument('--port', nargs='?', type=int, default=443) parser.add_argument('--ignore_ssl', action='store_true', default=False) + parser.add_argument('--no-piggyback', action='store_true', default=False) + parser.add_argument('--password', nargs='?') + parser.add_argument('--port', nargs='?', type=int, default=443) + parser.add_argument('--prefix', nargs='?') parser.add_argument('--protocol', nargs='?', choices=['http', 'https'], default='https') + parser.add_argument('--username', nargs='?') parser.add_argument('--testing', action='store_true', default=False) args = parser.parse_args() @@ -99,7 +106,6 @@ def check_fritzbox_smarthome(args): raise Exception('Check credentials\n') # Write section header - sys.stdout.write('<<<fritzbox_smarthome:sep(0)>>>\n') response = urlopen( base_address + '/webservices/homeautoswitch.lua?switchcmd=getdevicelistinfos&sid=%s' % sessionid, context=ctx) response_read = response.read() @@ -112,7 +118,7 @@ def check_fritzbox_smarthome(args): devices = [] if args.testing: - __switch = { + __switch_01 = { "identifier": "08761 0116372", "id": "99", "functionbitmask": "35712", @@ -141,12 +147,92 @@ def check_fritzbox_smarthome(args): "offset": "0" } } - devices.append(__switch) + __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)) - sys.stdout.write(json.dumps(devices)) + 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(): diff --git a/mkp/fritzbox_smarthome-0.8.2-20231230.mkp b/mkp/fritzbox_smarthome-0.8.2-20231230.mkp new file mode 100644 index 0000000000000000000000000000000000000000..d32b72848621de623bfda86d54f1f3a43018ddfa GIT binary patch literal 13425 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ð7MvAzbtzY($uAA&dsgF9Db?Mn@% z1AAa@Vh_^`4)R7yFH-XMfO3=in=JxJ&?K(iL;c@*O2D(*e$^pS*z2dbDI?-KUKbQD zS01?;j0`Zw@LyzGkbtP@_W{8+m$A@)drX4lK9{*$WmWptKF$G$9atX$dk^Y9NT@1? zuG(=zq_N_oOEno2)_GEmR_d#=&@IgIcL22`tBtdg?{|DNjS|_D#G}G2y4*)q@idLu ze^uXWiYfr)^KQg9lci0*J{s6t1fCS%Q*XX=Eky>f`jNits{QOY8Qi3*X%$5E0IWII zm=-5XeJ@umr(0Jyr$k{4-A_{Rv~gfbXqHOmixEXq#WKq*-lnq}STYZiGRtM^7#Om} z>PfrdJ8@tFYM(aHBMjMKE9Bi}zzU|Q6z<-k!yfit7ZZ|Utas*ASOlw4m-ElHZ$_6e zjQmxm;lcj4tIIsDwh@xoB*palsS!X2i|jlck$J!&u>YyQ5$s#|IBVDs=;3rkw(map zwuXFAK$CC2#+fs$8cR8pT=7Xy{CjW6nI@u$C&$eqG>{wLQ|!K>g92c@^<R&?++poc zJxt{QKBt~hzz;unL&B&NLB9C*OrC8ix>rQ`lX7LBSJp063baF)8G_5@Ob-HSh%?LS zgshQBIPIo4N<$hk*CQ)Xk|vaKSi>&`EH(=imC~|!eAnT3+q31@#uZJ24A=5?B}JP9 zUx&r#Npfd^++SVw_iP>%(qxt6t&Q3^P@iJ1u_F`mD-sw0T}L(tUilBp&x4(By!9Qg zz=59Nn~z!Wj8tdA=RTSLX7i3>R*`XyCMb7YB?g#fA(SPwI<p*6**%@<!HDBoMh}r! zm_{^HsVem2!fllsuJW<5LU%9uEX3*aihZ$ylAl6DYf^$Ze-y{U0+}XRM?uesXdLao zc5)GjWe#X*wWZuF(03hm=w?28yK^EpUX$lhQtJ#9$-NGIH&yxPeBEv(t)7W=hJCF@ z=ai^yLlO)Gs;&IL7p~82KTdI7_(n8Q>W!hHFO+e|{ExG0f6<JzGq*~<9SQ1cFwdEb z?6XC8vDPk{TDkT%d=hBWs{MM`x4=O6bW%#uu~#FEl}ng^McwaOp^N)JD3Jdjnq-|7 P6pSiuIt8H%1@V6Y2SmhT literal 0 HcmV?d00001 diff --git a/packages/fritzbox_smarthome b/packages/fritzbox_smarthome index 2c17bf3..740e6c6 100644 --- a/packages/fritzbox_smarthome +++ b/packages/fritzbox_smarthome @@ -19,18 +19,32 @@ '....\n' '\n', 'download_url': 'https://github.com/MaximilianClemens/checkmk_fritzbox', - 'files': {'agent_based': ['fritzbox_smarthome.py'], + '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/wato/fritzbox_smarthome.py', - 'plugins/metrics/fritzbox_smarthome.py', 'plugins/views/fritzbox_smarthome.py']}, 'name': 'fritzbox_smarthome', 'title': 'Fritz!Box SmartHome', - 'version': '0.7.0-20231228', + 'version': '0.8.2-20231230', 'version.min_required': '2.2.0b1', 'version.packaged': '2.2.0p14', 'version.usable_until': None} diff --git a/web/plugins/wato/agent_fritzbox_smarthome.py b/web/plugins/wato/agent_fritzbox_smarthome.py index cde48d0..cc2f867 100644 --- a/web/plugins/wato/agent_fritzbox_smarthome.py +++ b/web/plugins/wato/agent_fritzbox_smarthome.py @@ -56,11 +56,25 @@ def _valuespec_special_agents_fritzbox_smarthome() -> ValueSpec: )), ('ssl', FixedValue( value=0, - totext='Agent will ignore SSL errors', + 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'), )), -- GitLab