diff --git a/README.md b/README.md index 3f09eb98f76c44b1d383a29b3d51e4218b33cf4f..8a58ce76181a22a9aaf1dd6ca1f25f972e57ea43 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 fabe3576173dcd9f03e7eca4708bdfa25e159e94..1159ac6437732117d1a9136e30a569bd29ea5c40 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 0000000000000000000000000000000000000000..b52314e868c9feb5c542d517f7c46be96c46b179 --- /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 0000000000000000000000000000000000000000..61d597b61e08a8b772b93bc69a952ca0ef4cd051 --- /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 0000000000000000000000000000000000000000..6c088fd7a7b41578eb49c70221a78d1b59725bd1 --- /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 0000000000000000000000000000000000000000..6e1c0968b4c9cf0b14806af2775b0695eecd1311 --- /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 0000000000000000000000000000000000000000..d702cb982da21eb64265129736411316c179fc0d --- /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 0000000000000000000000000000000000000000..7bfeac10cc0d0b5123851487a22971cc57557b52 --- /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 0000000000000000000000000000000000000000..42875b7c17593e59a6768cede99a60a3133aa6b4 --- /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 0000000000000000000000000000000000000000..f2c748d0922e8fd0147b9fae61b69b2003f88302 --- /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 0000000000000000000000000000000000000000..0f77bf5b73d0c1de58c04821adcecfeb1595adf5 --- /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 0000000000000000000000000000000000000000..b6a9137d4fd0e3e03c0ef29975b1cec5f622fc9c --- /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 a3918e306d58b14d75a231ecec03561d8f058118..26a785476309e6f6aae7e43c7d47bd8579f0f397 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 0000000000000000000000000000000000000000..7a8fc1255de4954df05fb55b76f73ae7034464d6 --- /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 0000000000000000000000000000000000000000..737f69ae25d7f62b7be6054d2fd7d101babd8f2f --- /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 0000000000000000000000000000000000000000..d5958163afc4378f683fad50d580c97262e4931b --- /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 0000000000000000000000000000000000000000..a102e0e302422008fe6265402ee038d19651bb58 --- /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 0000000000000000000000000000000000000000..0c9d88797128a4367acbb287ca580b4c5f8bf5f3 --- /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 0000000000000000000000000000000000000000..7fd6761a10438b84ac57cd446909475e456d6cc8 --- /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 c0c2badb2d5b31d09cd5682c6379d0bceff383fa..7e6efef60ba95b88a6ecc9afc58f14dd4e5b877e 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 Binary files /dev/null and b/mkp/fritzbox_smarthome-0.8.2-20231230.mkp differ diff --git a/packages/fritzbox_smarthome b/packages/fritzbox_smarthome index 2c17bf37e7c845b3c83867016fb9dd0c6affb572..740e6c6d6568c93a7e8a2dcb84c3b499081945b8 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 cde48d0a2cf8322cc46b8b2d2ad171e9d6dc2f2f..cc2f86736981940299dbac01368d26e0920ba7d8 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'), )),