From a651ec72de0f8f36eba6e6f71fe355f87d777019 Mon Sep 17 00:00:00 2001 From: "th.l" <thl-cmk@outlook.com> Date: Thu, 25 Jan 2024 20:22:05 +0100 Subject: [PATCH] update project --- README.md | 2 +- mkp/fritzbox_smarthome-0.8.17-20240125.mkp | Bin 0 -> 23033 bytes .../agent_based/fritzbox_smarthome_battery.py | 11 + .../agent_based/fritzbox_smarthome_button.py | 103 +++++ .../fritzbox_smarthome_humidity.py | 79 ++++ .../fritzbox_smarthome_power_meter.py | 91 ++-- .../fritzbox_smarthome_temperature.py | 17 +- .../fritzbox_smarthome_thermostat.py | 72 ++- .../agent_based/utils/fritzbox_smarthome.py | 181 ++++++-- source/checks/agent_fritzbox_smarthome | 6 +- source/gui/dashboard/avm | 327 ++++++++++++++ source/gui/metrics/fritzbox_smarthome.py | 59 ++- source/gui/views/avm_fritzbox | 59 +++ .../gui/views/avm_smart_home_devices_metrics | 62 +++ .../gui/views/avm_smart_home_devices_status | 63 +++ .../gui/views/invavmsmarthomedevices_filtered | 69 +++ .../check_parameters/fritzbox_smarthome.py | 17 + .../agent_fritzbox_smarthome.py | 412 +++++++++++------- source/packages/fritzbox_smarthome | 25 +- .../web/plugins/views/fritzbox_smarthome.py | 2 +- .../plugins/wato/agent_fritzbox_smarthome.py | 7 + 21 files changed, 1377 insertions(+), 287 deletions(-) create mode 100644 mkp/fritzbox_smarthome-0.8.17-20240125.mkp create mode 100644 source/agent_based/fritzbox_smarthome_button.py create mode 100644 source/agent_based/fritzbox_smarthome_humidity.py create mode 100644 source/gui/dashboard/avm create mode 100644 source/gui/views/avm_fritzbox create mode 100644 source/gui/views/avm_smart_home_devices_metrics create mode 100644 source/gui/views/avm_smart_home_devices_status create mode 100644 source/gui/views/invavmsmarthomedevices_filtered diff --git a/README.md b/README.md index ef1f715..2217179 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -[PACKAGE]: ../../raw/master/mkp/fritzbox_smarthome-0.8.8-20240109.mkp "fritzbox_smarthome-0.8.8-20240109.mkp" +[PACKAGE]: ../../raw/master/mkp/fritzbox_smarthome-0.8.17-20240125.mkp "fritzbox_smarthome-0.8.17-20240125.mkp" # AVM Fritz!Box Smarthome This repository contains a additional check_MK Fritz!Box Agent which can gather informations over the AVM AHA HTTP Interface about SmartHome Devices connected to an Fritz!Box. diff --git a/mkp/fritzbox_smarthome-0.8.17-20240125.mkp b/mkp/fritzbox_smarthome-0.8.17-20240125.mkp new file mode 100644 index 0000000000000000000000000000000000000000..e6199e134be455277941021d53230cb8c9ebcae6 GIT binary patch literal 23033 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 literal 0 HcmV?d00001 diff --git a/source/agent_based/fritzbox_smarthome_battery.py b/source/agent_based/fritzbox_smarthome_battery.py index 57f3556..e9c8b75 100644 --- a/source/agent_based/fritzbox_smarthome_battery.py +++ b/source/agent_based/fritzbox_smarthome_battery.py @@ -13,10 +13,12 @@ from typing import Dict from cmk.base.plugins.agent_based.agent_based_api.v1 import ( + check_levels, Result, Service, State, register, + render, ) 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 @@ -56,6 +58,15 @@ def check_fritzbox_smarthome_battery_single( else: yield Result(state=State(params.get('battery_low', 2)), summary=_message) + if section.battery is not None: + yield from check_levels( + value=section.battery, + label='Battery', + metric_name='battery', + render_func=render.percent, + levels_lower=params.get('levels_lower'), + ) + def check_fritzbox_smarthome_battery_multiple( item, params, section: AvmSmartHomeDevice | Dict[str, AvmSmartHomeDevice] diff --git a/source/agent_based/fritzbox_smarthome_button.py b/source/agent_based/fritzbox_smarthome_button.py new file mode 100644 index 0000000..424d950 --- /dev/null +++ b/source/agent_based/fritzbox_smarthome_button.py @@ -0,0 +1,103 @@ +#!/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 : 2024-01-10 +# File : fritzbox_smarthome_button.py (check plugin) +# +# + +from time import localtime, strftime +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, AvmButton, AVM_TIME_FORMAT + + +def discovery_fritzbox_smarthome_button_single( + section: AvmSmartHomeDevice | Dict[str, AvmSmartHomeDevice] +) -> DiscoveryResult: + if isinstance(section, AvmSmartHomeDevice): + if isinstance(section.buttons, list): + for button in section.buttons: + item = button.name.split(':')[-1].strip() # name="Button01: Top right" + yield Service(item=item, parameters={'discovered_id': button.id}) + + +def discovery_fritzbox_smarthome_button_multiple( + section: AvmSmartHomeDevice | Dict[str, AvmSmartHomeDevice] +) -> DiscoveryResult: + if not isinstance(section, AvmSmartHomeDevice): + for device_id, device in section.items(): + if isinstance(device.buttons, list): + for button in device.buttons: + item = button.name.split(':')[-1].strip() # name="Button01: Top right" + yield Service(item=f'{device_id} {item}', parameters={'discovered_id': button.id}) + + +def check_fritzbox_smarthome_button_single( + item, params, section: AvmSmartHomeDevice | Dict[str, AvmSmartHomeDevice] +) -> CheckResult: + if not isinstance(section, AvmSmartHomeDevice) or section.buttons is None: + return + + button_found = None + for button in section.buttons: + if button.id == params['discovered_id']: + button_found: AvmButton | None = button + break + + if not button_found: + return + + if button_found.last_pressed_time_stamp is not None: + yield Result( + state=State.OK, + summary=f'Last pressed: {strftime(AVM_TIME_FORMAT, localtime(button_found.last_pressed_time_stamp))}' + ) + else: + yield Result(state=State.OK, summary='Button never pressed') + + yield Result(state=State.OK, notice=f'ID: {button_found.id}') + yield Result(state=State.OK, notice=f'Identifier: {button_found.identifier}') + + +def check_fritzbox_smarthome_button_multiple( + item, params, section: AvmSmartHomeDevice | Dict[str, AvmSmartHomeDevice] +) -> CheckResult: + if isinstance(section, Dict): + try: + yield from check_fritzbox_smarthome_button_single(item, params, section[item]) + except KeyError: + return + + +register.check_plugin( + name='fritzbox_smarthome_button_single', + service_name='Button %s', + sections=['fritzbox_smarthome'], + discovery_function=discovery_fritzbox_smarthome_button_single, + check_function=check_fritzbox_smarthome_button_single, + # check_ruleset_name='fritzbox_smarthome_button_single', + check_default_parameters={} +) + + +register.check_plugin( + name='fritzbox_smarthome_button_multiple', + service_name='Smarthome Button %s', + sections=['fritzbox_smarthome'], + discovery_function=discovery_fritzbox_smarthome_button_multiple, + check_function=check_fritzbox_smarthome_button_multiple, + # check_ruleset_name='fritzbox_smarthome_button_multiple', + check_default_parameters={} +) diff --git a/source/agent_based/fritzbox_smarthome_humidity.py b/source/agent_based/fritzbox_smarthome_humidity.py new file mode 100644 index 0000000..caca820 --- /dev/null +++ b/source/agent_based/fritzbox_smarthome_humidity.py @@ -0,0 +1,79 @@ +#!/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 : 2024-01-10 +# File : fritzbox_smarthome_humidity.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.humidity import check_humidity +from cmk.base.plugins.agent_based.utils.fritzbox_smarthome import AvmSmartHomeDevice + + +def discovery_fritzbox_smarthome_humidity_single( + section: AvmSmartHomeDevice | Dict[str, AvmSmartHomeDevice] +) -> DiscoveryResult: + if isinstance(section, AvmSmartHomeDevice): + if section.humidity: + yield Service() + + +def discovery_fritzbox_smarthome_humidity_multiple( + section: AvmSmartHomeDevice | Dict[str, AvmSmartHomeDevice] +) -> DiscoveryResult: + if not isinstance(section, AvmSmartHomeDevice): + for device_id, device in section.items(): + if device.humidity: + yield Service(item=str(device_id)) + + +def check_fritzbox_smarthome_humidity_single( + params, section: AvmSmartHomeDevice | Dict[str, AvmSmartHomeDevice] +) -> CheckResult: + if not isinstance(section, AvmSmartHomeDevice) or not section.humidity: + return + print(params) + if section.humidity.rel_humidity: + yield from check_humidity( + humidity=section.humidity.rel_humidity, + params=params.get('auto-migration-wrapper-key'), + ) + + +def check_fritzbox_smarthome_humidity_multiple( + item, params, section: AvmSmartHomeDevice | Dict[str, AvmSmartHomeDevice] +) -> CheckResult: + if isinstance(section, Dict): + try: + yield from check_fritzbox_smarthome_humidity_single(params, section[item]) + except KeyError: + return + + +register.check_plugin( + name='fritzbox_smarthome_humidity_single', + service_name='Humidity', + sections=['fritzbox_smarthome'], + discovery_function=discovery_fritzbox_smarthome_humidity_single, + check_function=check_fritzbox_smarthome_humidity_single, + check_ruleset_name='single_humidity', + check_default_parameters={} +) + +register.check_plugin( + name='fritzbox_smarthome_humidity', + service_name='Smarthome Humidity %s', + sections=['fritzbox_smarthome'], + discovery_function=discovery_fritzbox_smarthome_humidity_multiple, + check_function=check_fritzbox_smarthome_humidity_multiple, + check_ruleset_name='humidity', + check_default_parameters={} +) diff --git a/source/agent_based/fritzbox_smarthome_power_meter.py b/source/agent_based/fritzbox_smarthome_power_meter.py index d966c89..74a8773 100644 --- a/source/agent_based/fritzbox_smarthome_power_meter.py +++ b/source/agent_based/fritzbox_smarthome_power_meter.py @@ -173,18 +173,21 @@ def discovery_fritzbox_smarthome_energy_multiple( def _cost_period_x( - value_store: FritzBoxValueStore, - period_name: str, + cost_kwh: float, current_period: int, - rate_name: str, - metric_name: str, last_reading: float, - precision: int, message: str, - cost_kwh: float, + metric_name: str, + period_name: str, + precision: int, + rate_name: str, + unit: str, + value_store: FritzBoxValueStore, + hours: float, + power: float ) -> CheckResult: # reset all - # value_store[rate_name] = 0 + # value_store.set(rate_name) = 0 if stored_period := value_store.get(key=period_name): value_store.set(key=period_name, value=current_period) @@ -199,12 +202,15 @@ def _cost_period_x( value_store.set(key=rate_name, value=cost) cost = round(cost, precision) - - yield Result( - state=State.OK, - notice=message.replace('__value__', f'{cost:.4f}') + cost_estimated = round((power / 1000 * cost_kwh * hours), 2) + + yield from check_levels( + value=cost, + label=f'{message} (estimated per {message}: {cost_estimated})', + metric_name=metric_name, + notice_only=True, + render_func=lambda x: f'{x:.4f}{unit}' ) - yield Metric(name=metric_name, value=cost) def check_fritzbox_smarthome_energy_single( @@ -270,48 +276,60 @@ def check_fritzbox_smarthome_energy_single( yield Result(state=State.OK, notice='Cost for this:') loca_time = localtime() yield from _cost_period_x( - value_store=value_store, - period_name='current_hour', + cost_kwh=cost_kwh, current_period=loca_time.tm_hour, - rate_name='cost_this_hour', # don't reuse -> cost_per_hour - metric_name='cost_per_hour', last_reading=energy, + message=f'Hour', + metric_name='cost_per_hour', + period_name='current_hour', precision=6, - message=f'Hour_: __value__{unit_sign}', - cost_kwh=cost_kwh, + rate_name='cost_this_hour', # don't reuse -> cost_per_hour + unit=unit_sign, + value_store=value_store, + hours=1, + power=section.power_meter.power, ) yield from _cost_period_x( - value_store=value_store, - period_name='current_day', + cost_kwh=cost_kwh, current_period=loca_time.tm_mday, - rate_name='cost_this_day', # don't reuse -> cost_per_day - metric_name='cost_per_day', last_reading=energy, + message=f'Day', + metric_name='cost_per_day', + period_name='current_day', precision=4, - message=f'Day__: __value__{unit_sign}', - cost_kwh=cost_kwh, + rate_name='cost_this_day', # don't reuse -> cost_per_day + unit=unit_sign, + value_store=value_store, + hours=24, + power=section.power_meter.power, ) yield from _cost_period_x( - value_store=value_store, - period_name='current_month', + cost_kwh=cost_kwh, current_period=loca_time.tm_mon, - rate_name='cost_this_month', # don't reuse -> cost_per_month - metric_name='cost_per_month', last_reading=energy, + message=f'Month', + metric_name='cost_per_month', + period_name='current_month', precision=4, - message=f'Month: __value__{unit_sign}', - cost_kwh=cost_kwh, + rate_name='cost_this_month', # don't reuse -> cost_per_month + unit=unit_sign, + value_store=value_store, + hours=24 * 365 / 12, + power=section.power_meter.power, ) yield from _cost_period_x( - value_store=value_store, - period_name='current_year', + cost_kwh=cost_kwh, current_period=loca_time.tm_year, - rate_name='cost_this_year', # don't reuse -> cost_per_year - metric_name='cost_per_year', last_reading=energy, + message=f'Year', + metric_name='cost_per_year', + period_name='current_year', precision=4, - message=f'Year_: __value__{unit_sign}', - cost_kwh=cost_kwh, + rate_name='cost_this_year', # don't reuse -> cost_per_year + unit=unit_sign, + value_store=value_store, + hours=24 * 365, + power=section.power_meter.power, ) yield Result(state=State.OK, notice=' ') @@ -355,3 +373,4 @@ register.check_plugin( check_ruleset_name='energy_multiple', check_default_parameters={} ) + diff --git a/source/agent_based/fritzbox_smarthome_temperature.py b/source/agent_based/fritzbox_smarthome_temperature.py index 86ebd47..ac79231 100644 --- a/source/agent_based/fritzbox_smarthome_temperature.py +++ b/source/agent_based/fritzbox_smarthome_temperature.py @@ -47,16 +47,19 @@ def check_fritzbox_smarthome_temperature_single( params=params, ) if section.temperature.offset: - _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"))}' + temp_sensor = section.temperature.celsius + section.temperature.offset * -1 + _details = ( + f'Temperature measured at the sensor' + f': {_render_temp_with_unit(temp_sensor, params.get("output_unit", "c"))}' ) - yield Result(state=State.OK, notice=_message) + _summary = f'At the sensor: {_render_temp_with_unit(temp_sensor, params.get("output_unit", "c"))}' + yield Result(state=State.OK, summary=_summary, details=_details) + yield Result( state=State.OK, - notice=f'Temperature offset: ' - f'{_render_temp_with_unit(section.temperature.offset, params.get("output_unit", "c"))}' + summary=f'Offset: {_render_temp_with_unit(section.temperature.offset, params.get("output_unit", "c"))}', + details=f'Temperature offset: ' + f'{_render_temp_with_unit(section.temperature.offset, params.get("output_unit", "c"))}' ) diff --git a/source/agent_based/fritzbox_smarthome_thermostat.py b/source/agent_based/fritzbox_smarthome_thermostat.py index 9828c3c..ee9aa20 100644 --- a/source/agent_based/fritzbox_smarthome_thermostat.py +++ b/source/agent_based/fritzbox_smarthome_thermostat.py @@ -21,9 +21,7 @@ from cmk.base.plugins.agent_based.agent_based_api.v1 import ( 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' +from cmk.base.plugins.agent_based.utils.fritzbox_smarthome import AvmSmartHomeDevice, AVM_TIME_FORMAT def discovery_fritzbox_smarthome_thermostat_single( @@ -68,6 +66,7 @@ def check_fritzbox_smarthome_thermostat_single( yield Result(state=State(params.get('state_off', 0)), summary=f'Temperature target: radiator off') else: deviation = thermostat.temp_current - thermostat.temp_target + yield Metric(name='temp_deviation', value=deviation) if deviation == 0: yield Result(state=State.OK, summary=f'Temperature current: {thermostat.temp_target}°C') else: @@ -88,8 +87,8 @@ def check_fritzbox_smarthome_thermostat_single( ) 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 Result(state=State.OK, notice=f'Temperature cool-down: {thermostat.temp_economic}°C') yield Metric(name='temp_current', value=thermostat.temp_current) yield Metric(name='temp_comfort', value=thermostat.temp_comfort) @@ -98,7 +97,7 @@ def check_fritzbox_smarthome_thermostat_single( if thermostat.next_change: yield Result( state=State.OK, - notice=f'End of period: {strftime(_TIME_FORMAT, localtime(thermostat.next_change.end_period))}' + notice=f'End of period: {strftime(AVM_TIME_FORMAT, localtime(thermostat.next_change.end_period))}' ) yield Result( state=State.OK, @@ -114,6 +113,69 @@ def check_fritzbox_smarthome_thermostat_single( summary=f'Error Code: {thermostat.error_code} (see details)', details=_message) + _adaptive_heating_active = { + 0: 'inactive', + 1: 'activ' + } + if thermostat.adaptive_heating_active is not None: + yield Result( + state=State.OK, + notice=f'Adaptive heating: {_adaptive_heating_active.get(thermostat.adaptive_heating_active)}') + + _adaptive_heating_running = { + 0: 'not running', + 1: 'running', + } + if thermostat.adaptive_heating_active == 1 and thermostat.adaptive_heating_running is not None: + yield Result( + state=State.OK, + notice=f'Adaptive heating: {_adaptive_heating_running.get(thermostat.adaptive_heating_running)}' + ) + + _boost_active = { + 0: 'inactive', + 1: 'active', + } + if thermostat.boost_active is not None: + _message = f'Boost mode: {_boost_active.get(thermostat.boost_active)}' + if not thermostat.boost_active: + yield Result(state=State.OK, notice=_message) + else: + yield Result(state=State(params.get('state_boost_mode', 1)), notice=_message) + if thermostat.boost_active_end_time is not None: + _end_time = strftime(AVM_TIME_FORMAT, localtime(thermostat.boost_active_end_time)) + _message = f'Boost mode end: {_end_time}' + yield Result(state=State(params.get('state_boost_mode', 1)), notice=_message) + + _holiday_active = { + 0: 'inactive', + 1: 'active', + } + if thermostat.holiday_active is not None: + yield Result(state=State.OK, notice=f'Holiday mode: {_holiday_active.get(thermostat.holiday_active)}') + + _summer_active = { + 0: 'inactive', + 1: 'active', + } + if thermostat.summer_active is not None: + yield Result(state=State.OK, notice=f'Summer mode: {_summer_active.get(thermostat.summer_active)}') + + _windows_open = { + 0: 'inactive', + 1: 'active', + } + if thermostat.window_open_activ is not None: + _message = f'Windows open mode: {_windows_open.get(thermostat.window_open_activ)}' + if not thermostat.window_open_activ: + yield Result(state=State.OK, notice=_message) + else: + yield Result(state=State(params.get('state_windows_open', 1)), notice=_message) + if thermostat.window_open_active_end_time is not None: + _end_time = strftime(AVM_TIME_FORMAT, localtime(thermostat.window_open_active_end_time)) + _message = f'Window open mode end: {_end_time}' + yield Result(state=State(params.get('state_windows_open', 1)), notice=_message) + def check_fritzbox_smarthome_thermostat_multiple( item, params, section: AvmSmartHomeDevice | Dict[str, AvmSmartHomeDevice] diff --git a/source/agent_based/utils/fritzbox_smarthome.py b/source/agent_based/utils/fritzbox_smarthome.py index b361852..9669cea 100644 --- a/source/agent_based/utils/fritzbox_smarthome.py +++ b/source/agent_based/utils/fritzbox_smarthome.py @@ -21,6 +21,19 @@ from json import loads, dumps from cmk.checkers import plugin_contexts +@dataclass(frozen=True) +class AvmButton: + identifier: str + id: str + name: str + last_pressed_time_stamp: int + + +@dataclass(frozen=True) +class AvmHumidity: + rel_humidity: int + + @dataclass(frozen=True) class AvmTemperature: celsius: float | None @@ -52,15 +65,15 @@ class AvmThermostat: temp_current: float | None temp_economic: float | None temp_target: float | None + next_change: AvmNextChange | None = None 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 + window_open_active_end_time: int | None = None @dataclass(frozen=True) @@ -80,6 +93,7 @@ class AvmSmartHomeDevice: name: str present: int product_name: str + battery: int | None = None battery_low: int | None = None device_lock: int | None = None lock: int | None = None @@ -89,17 +103,59 @@ class AvmSmartHomeDevice: temperature: AvmTemperature | None = None thermostat: AvmThermostat | None = None tx_busy: int | None = None - - -_AVM_THERMOSTAT = 'hkr' -_AVM_SWITCH = 'switch' + buttons: list[AvmButton] | None = None + humidity: AvmHumidity | None = None + + +_AVM_ADAPTIVE_HEATING_ACTIVE = 'adaptiveHeatingActive' +_AVM_ADAPTIVE_HEATING_RUNNING = 'adaptiveHeatingRunning' +_AVM_BATTERY = 'battery' +_AVM_BATTERY_LOW = 'batterylow' +_AVM_BOOST_ACTIVE = 'boostactive' +_AVM_BOOST_ACTIVE_END_TIME = 'boostactiveendtime' +_AVM_CELSIUS = 'celsius' +_AVM_DEVICE_LOCK = 'devicelock' +_AVM_END_PERIOD = 'endperiod' +_AVM_ENERGY = 'energy' +_AVM_ERROR_CODE = 'errorcode' +_AVM_FUNCTION_BIT_MASK = 'functionbitmask' +_AVM_FW_REVISION = 'fwversion' +_AVM_HOLIDAY_ACTIVE = 'holidayactive' +_AVM_HUMIDITY = 'humidity' +_AVM_ID = 'id' +_AVM_IDENTIFIER = 'identifier' +_AVM_LAST_PRESSED_TIME_STAMP = 'lastpressedtimestamp' +_AVM_LOCK = 'lock' +_AVM_MANUFACTURER = 'manufacturer' +_AVM_MODE = 'mode' +_AVM_NAME='name' +_AVM_NEXT_CHANGE = 'nextchange' +_AVM_OFFSET = 'offset' +_AVM_POWER = 'power' _AVM_POWER_METER = 'powermeter' -_AVM_TEMPERATURE = 'temperature' +_AVM_PRESENT = 'present' +_AVM_PRODUCT_NAME = 'productname' +_AVM_REL_HUMIDITY = 'rel_humidity' _AVM_SIMPLE_ON_OFF = 'simpleonoff' -_AVM_NEXT_CHANGE = 'nextchange' +_AVM_STATE = 'state' +_AVM_SUMMER_ACTIVE = 'summeractive' +_AVM_SWITCH = 'switch' +_AVM_TEMPERATURE = 'temperature' +_AVM_TEMP_CHANGE = 'tchange' +_AVM_TEMP_COMFORT = 'komfort' +_AVM_TEMP_CURRENT = 'tist' +_AVM_TEMP_ECONOMIC = 'absenk' +_AVM_TEMP_TARGET = 'tsoll' +_AVM_THERMOSTAT = 'hkr' +_AVM_TX_BUSY = 'txbusy' +_AVM_VOLTAGE = 'voltage' +_AVM_WINDOW_OPEN_ACTIV = 'windowopenactiv' +_AVM_WINDOW_OPEN_ACTIVE_END_TIME = 'windowopenactiveendtime' _OMD_ROOT = environ["OMD_ROOT"] +AVM_TIME_FORMAT = '%Y-%m-%dT%H:%M:%S' + class FritzBoxValueStore: """ @@ -150,91 +206,136 @@ class FritzBoxValueStore: self._file.write_text(dumps(self._counters)) +def _get_battery(device: Dict[str, Any]) -> int | None: + try: + return int(device[_AVM_THERMOSTAT][_AVM_BATTERY]) + except (KeyError, ValueError): + pass + + try: + return int(device[_AVM_BATTERY]) + except (KeyError, ValueError): + pass + + def _get_battery_low(device: Dict[str, Any]) -> int | None: try: - return int(device[_AVM_THERMOSTAT]['batterylow']) + return int(device[_AVM_THERMOSTAT][_AVM_BATTERY_LOW]) + except (KeyError, ValueError): + pass + + try: + return int(device[_AVM_BATTERY_LOW]) except (KeyError, ValueError): pass def _get_lock(device: Dict[str, Any]) -> int | None: try: - return int(device[_AVM_THERMOSTAT]['lock']) + return int(device[_AVM_THERMOSTAT][_AVM_LOCK]) except (KeyError, ValueError): pass try: - return int(device[_AVM_SWITCH]['lock']) + return int(device[_AVM_SWITCH][_AVM_LOCK]) except (KeyError, ValueError): pass def _get_device_lock(device: Dict[str, Any]) -> int | None: try: - return int(device[_AVM_THERMOSTAT]['devicelock']) + return int(device[_AVM_THERMOSTAT][_AVM_DEVICE_LOCK]) except (KeyError, ValueError): pass try: - return int(device[_AVM_SWITCH]['devicelock']) + return int(device[_AVM_SWITCH][_AVM_DEVICE_LOCK]) except (KeyError, ValueError): pass +def _get_buttons(device: Dict[str, any]) -> List[AvmButton] | None: + return [ + AvmButton( + identifier=device[key][_AVM_IDENTIFIER], + id=device[key][_AVM_ID], + name=device[key][_AVM_NAME], + last_pressed_time_stamp=_get_int(device[key][_AVM_LAST_PRESSED_TIME_STAMP]) + ) for key in device.keys() if key.startswith('button') + ] + + def _get_int(value: str | None) -> int | None: - if value is not None and value.isdigit(): + try: return int(value) + except (ValueError, TypeError): + return def _get_float(value: str | None, scale: float = 1.0) -> float | None: - if value is not None and value.isdigit(): + try: return float(value) / scale + except (ValueError, TypeError): + return def parse_avm_smarthome_device(raw_device: Dict[str, Any]) -> AvmSmartHomeDevice: return AvmSmartHomeDevice( + battery=_get_battery(raw_device), battery_low=_get_battery_low(raw_device), device_lock=_get_device_lock(raw_device), - fbm=_get_int(raw_device.get('functionbitmask')), - functions=get_avm_device_functions_from_fbm(_get_int(raw_device.get('functionbitmask'))), - fw_version=str(raw_device['fwversion']), - id=str(raw_device['id']), - identifier=str(raw_device['identifier']), + fbm=_get_int(raw_device.get(_AVM_FUNCTION_BIT_MASK)), + functions=get_avm_device_functions_from_fbm(_get_int(raw_device.get(_AVM_FUNCTION_BIT_MASK))), + fw_version=str(raw_device[_AVM_FW_REVISION]), + id=str(raw_device[_AVM_ID]), + identifier=str(raw_device[_AVM_IDENTIFIER]), lock=_get_lock(raw_device), - manufacturer=str(raw_device['manufacturer']), - name=str(raw_device['name']), - present=_get_int(raw_device.get('present')), - product_name=str(raw_device['productname']), - tx_busy=_get_int(raw_device.get('txbusy')), + manufacturer=str(raw_device[_AVM_MANUFACTURER]), + name=str(raw_device[_AVM_NAME]), + present=_get_int(raw_device.get(_AVM_PRESENT)), + product_name=str(raw_device[_AVM_PRODUCT_NAME]), + tx_busy=_get_int(raw_device.get(_AVM_TX_BUSY)), temperature=AvmTemperature( - celsius=_get_float(value=raw_device[_AVM_TEMPERATURE].get('celsius'), scale=10.0), - offset=_get_float(value=raw_device[_AVM_TEMPERATURE].get('offset'), scale=10.0), + celsius=_get_float(value=raw_device[_AVM_TEMPERATURE].get(_AVM_CELSIUS), scale=10.0), + offset=_get_float(value=raw_device[_AVM_TEMPERATURE].get(_AVM_OFFSET), scale=10.0), ) if raw_device.get(_AVM_TEMPERATURE) else None, thermostat=AvmThermostat( - temp_current=_get_float(value=raw_device[_AVM_THERMOSTAT].get('tist'), scale=2.0), - temp_target=_get_float(value=raw_device[_AVM_THERMOSTAT].get('tsoll'), scale=2.0), - temp_economic=_get_float(value=raw_device[_AVM_THERMOSTAT].get('absenk'), scale=2.0), - temp_comfort=_get_float(value=raw_device[_AVM_THERMOSTAT].get('komfort'), scale=2.0), - error_code=_get_int(value=raw_device[_AVM_THERMOSTAT].get('errorcode')), + temp_current=_get_float(value=raw_device[_AVM_THERMOSTAT].get(_AVM_TEMP_CURRENT), scale=2.0), + temp_target=_get_float(value=raw_device[_AVM_THERMOSTAT].get(_AVM_TEMP_TARGET), scale=2.0), + temp_economic=_get_float(value=raw_device[_AVM_THERMOSTAT].get(_AVM_TEMP_ECONOMIC), scale=2.0), + temp_comfort=_get_float(value=raw_device[_AVM_THERMOSTAT].get(_AVM_TEMP_COMFORT), scale=2.0), + error_code=_get_int(value=raw_device[_AVM_THERMOSTAT].get(_AVM_ERROR_CODE)), next_change=AvmNextChange( - end_period=_get_int(raw_device[_AVM_THERMOSTAT][_AVM_NEXT_CHANGE].get('endperiod')), + end_period=_get_int(raw_device[_AVM_THERMOSTAT][_AVM_NEXT_CHANGE].get(_AVM_END_PERIOD)), temp_change_to=_get_float( - value=raw_device[_AVM_THERMOSTAT][_AVM_NEXT_CHANGE].get('tchange'), scale=2.0 + value=raw_device[_AVM_THERMOSTAT][_AVM_NEXT_CHANGE].get(_AVM_TEMP_CHANGE), scale=2.0 ), ) if raw_device[_AVM_THERMOSTAT].get(_AVM_NEXT_CHANGE) else None, + adaptive_heating_active=_get_int(value=raw_device[_AVM_THERMOSTAT].get(_AVM_ADAPTIVE_HEATING_ACTIVE)), + adaptive_heating_running=_get_int(value=raw_device[_AVM_THERMOSTAT].get(_AVM_ADAPTIVE_HEATING_RUNNING)), + boost_active=_get_int(value=raw_device[_AVM_THERMOSTAT].get(_AVM_BOOST_ACTIVE)), + boost_active_end_time=_get_int(value=raw_device[_AVM_THERMOSTAT].get(_AVM_BOOST_ACTIVE_END_TIME)), + holiday_active=_get_int(value=raw_device[_AVM_THERMOSTAT].get(_AVM_HOLIDAY_ACTIVE)), + summer_active=_get_int(value=raw_device[_AVM_THERMOSTAT].get(_AVM_SUMMER_ACTIVE)), + window_open_activ=_get_int(value=raw_device[_AVM_THERMOSTAT].get(_AVM_WINDOW_OPEN_ACTIV)), + window_open_active_end_time=_get_int(value=raw_device[_AVM_THERMOSTAT].get(_AVM_WINDOW_OPEN_ACTIVE_END_TIME)), ) if raw_device.get(_AVM_THERMOSTAT) else None, switch=AvmSwitch( - state=_get_int(raw_device[_AVM_SWITCH].get('state')), - mode=str(raw_device[_AVM_SWITCH]['mode']), + state=_get_int(raw_device[_AVM_SWITCH].get(_AVM_STATE)), + mode=str(raw_device[_AVM_SWITCH][_AVM_MODE]), ) if raw_device.get(_AVM_SWITCH) else None, power_meter=AvmPowerMeter( - voltage=_get_float(raw_device[_AVM_POWER_METER].get('voltage'),1000), - power=_get_float(raw_device[_AVM_POWER_METER].get('power'), 1000), - energy=_get_float(raw_device[_AVM_POWER_METER].get('energy')), # / 1000, + voltage=_get_float(raw_device[_AVM_POWER_METER].get(_AVM_VOLTAGE),1000), + power=_get_float(raw_device[_AVM_POWER_METER].get(_AVM_POWER), 1000), + energy=_get_float(raw_device[_AVM_POWER_METER].get(_AVM_ENERGY)), # / 1000, ) if raw_device.get(_AVM_POWER_METER) else None, simple_on_off=AvmSimpleOnOff( - state=_get_int(raw_device[_AVM_SIMPLE_ON_OFF].get('state')), + state=_get_int(raw_device[_AVM_SIMPLE_ON_OFF].get(_AVM_STATE)), ) if raw_device.get(_AVM_SIMPLE_ON_OFF) else None, + humidity=AvmHumidity( + rel_humidity=_get_int(raw_device[_AVM_HUMIDITY].get(_AVM_REL_HUMIDITY)) + ) if raw_device.get(_AVM_HUMIDITY) else None, + buttons=_get_buttons(raw_device), ) diff --git a/source/checks/agent_fritzbox_smarthome b/source/checks/agent_fritzbox_smarthome index 26a7854..33c8a5d 100644 --- a/source/checks/agent_fritzbox_smarthome +++ b/source/checks/agent_fritzbox_smarthome @@ -26,9 +26,6 @@ def agent_fritzbox_smarthome_arguments(params, hostname, ipaddress): if (ssl := params.get("ssl")) is not None: args.append("--ignore_ssl") - # if (piggyback := params.get("piggyback")) is not None: - # args.append("--piggyback") - if (prefix := params.get("prefix")) is not None: args.extend(["--prefix"] + [hostname]) @@ -38,6 +35,9 @@ def agent_fritzbox_smarthome_arguments(params, hostname, ipaddress): if (no_piggyback := params.get("no_piggyback")) is not None: args.append("--no-piggyback") + if (no_pbkdf2 := params.get("no_pbkdf2")) is not None: + args.append("--no-pbkdf2") + return args diff --git a/source/gui/dashboard/avm b/source/gui/dashboard/avm new file mode 100644 index 0000000..c81a61a --- /dev/null +++ b/source/gui/dashboard/avm @@ -0,0 +1,327 @@ +{'avm': {'add_context_to_title': False, + 'context': {'host_labels': {'host_labels_1_bool': 'and', + 'host_labels_1_vs_1_bool': 'and', + 'host_labels_1_vs_1_vs': 'fritz/smarthome/device:yes', + 'host_labels_1_vs_2_bool': 'or', + 'host_labels_1_vs_2_vs': 'cmk/os_family:FRITZ!OS', + 'host_labels_1_vs_3_bool': 'and', + 'host_labels_1_vs_@:@_bool': 'and', + 'host_labels_1_vs_count': '3', + 'host_labels_1_vs_indexof_1': '1', + 'host_labels_1_vs_indexof_2': '2', + 'host_labels_1_vs_indexof_3': '3', + 'host_labels_1_vs_indexof_@:@': '', + 'host_labels_1_vs_orig_indexof_1': '1', + 'host_labels_1_vs_orig_indexof_2': '2', + 'host_labels_1_vs_orig_indexof_3': '3', + 'host_labels_1_vs_orig_indexof_@:@': '', + 'host_labels_@!@_bool': 'and', + 'host_labels_@!@_vs_1_bool': 'and', + 'host_labels_@!@_vs_@:@_bool': 'and', + 'host_labels_@!@_vs_count': '1', + 'host_labels_@!@_vs_indexof_1': '1', + 'host_labels_@!@_vs_indexof_@:@': '', + 'host_labels_@!@_vs_orig_indexof_1': '1', + 'host_labels_@!@_vs_orig_indexof_@:@': '', + 'host_labels_count': '1', + 'host_labels_indexof_1': '1', + 'host_labels_indexof_@!@': '', + 'host_labels_orig_indexof_1': '1', + 'host_labels_orig_indexof_@!@': ''}}, + 'dashlets': [{'background': True, + 'context': {'host_labels': {'host_labels_1_bool': 'and', + 'host_labels_1_vs_1_bool': 'and', + 'host_labels_1_vs_1_vs': 'cmk/os_family:FRITZ!OS', + 'host_labels_1_vs_2_bool': 'and', + 'host_labels_1_vs_@:@_bool': 'and', + 'host_labels_1_vs_count': '2', + 'host_labels_1_vs_indexof_1': '1', + 'host_labels_1_vs_indexof_2': '2', + 'host_labels_1_vs_indexof_@:@': '', + 'host_labels_1_vs_orig_indexof_1': '1', + 'host_labels_1_vs_orig_indexof_2': '2', + 'host_labels_1_vs_orig_indexof_@:@': '', + 'host_labels_@!@_bool': 'and', + 'host_labels_@!@_vs_1_bool': 'and', + 'host_labels_@!@_vs_@:@_bool': 'and', + 'host_labels_@!@_vs_count': '1', + 'host_labels_@!@_vs_indexof_1': '1', + 'host_labels_@!@_vs_indexof_@:@': '', + 'host_labels_@!@_vs_orig_indexof_1': '1', + 'host_labels_@!@_vs_orig_indexof_@:@': '', + 'host_labels_count': '1', + 'host_labels_indexof_1': '1', + 'host_labels_indexof_@!@': '', + 'host_labels_orig_indexof_1': '1', + 'host_labels_orig_indexof_@!@': ''}}, + 'name': 'avm_fritzbox', + 'position': (1, 1), + 'show_title': True, + 'single_infos': [], + 'size': (0, 0), + 'type': 'linked_view'}, + {'background': True, + 'context': {'host_labels': {'host_labels_1_bool': 'and', + 'host_labels_1_vs_1_bool': 'and', + 'host_labels_1_vs_1_vs': 'fritz/smarthome/device:yes', + 'host_labels_1_vs_2_bool': 'and', + 'host_labels_1_vs_@:@_bool': 'and', + 'host_labels_1_vs_count': '2', + 'host_labels_1_vs_indexof_1': '1', + 'host_labels_1_vs_indexof_2': '2', + 'host_labels_1_vs_indexof_@:@': '', + 'host_labels_1_vs_orig_indexof_1': '1', + 'host_labels_1_vs_orig_indexof_2': '2', + 'host_labels_1_vs_orig_indexof_@:@': '', + 'host_labels_@!@_bool': 'and', + 'host_labels_@!@_vs_1_bool': 'and', + 'host_labels_@!@_vs_@:@_bool': 'and', + 'host_labels_@!@_vs_count': '1', + 'host_labels_@!@_vs_indexof_1': '1', + 'host_labels_@!@_vs_indexof_@:@': '', + 'host_labels_@!@_vs_orig_indexof_1': '1', + 'host_labels_@!@_vs_orig_indexof_@:@': '', + 'host_labels_count': '1', + 'host_labels_indexof_1': '1', + 'host_labels_indexof_@!@': '', + 'host_labels_orig_indexof_1': '1', + 'host_labels_orig_indexof_@!@': ''}}, + 'name': 'avm_smart_home_devices_status', + 'position': (1, 14), + 'show_title': True, + 'single_infos': [], + 'size': (0, 0), + 'type': 'linked_view'}, + {'background': True, + 'context': {'host_labels': {'host_labels_1_bool': 'and', + 'host_labels_1_vs_1_bool': 'and', + 'host_labels_1_vs_1_vs': 'fritz/smarthome/device:yes', + 'host_labels_1_vs_2_bool': 'and', + 'host_labels_1_vs_@:@_bool': 'and', + 'host_labels_1_vs_count': '2', + 'host_labels_1_vs_indexof_1': '1', + 'host_labels_1_vs_indexof_2': '2', + 'host_labels_1_vs_indexof_@:@': '', + 'host_labels_1_vs_orig_indexof_1': '1', + 'host_labels_1_vs_orig_indexof_2': '2', + 'host_labels_1_vs_orig_indexof_@:@': '', + 'host_labels_@!@_bool': 'and', + 'host_labels_@!@_vs_1_bool': 'and', + 'host_labels_@!@_vs_@:@_bool': 'and', + 'host_labels_@!@_vs_count': '1', + 'host_labels_@!@_vs_indexof_1': '1', + 'host_labels_@!@_vs_indexof_@:@': '', + 'host_labels_@!@_vs_orig_indexof_1': '1', + 'host_labels_@!@_vs_orig_indexof_@:@': '', + 'host_labels_count': '1', + 'host_labels_indexof_1': '1', + 'host_labels_indexof_@!@': '', + 'host_labels_orig_indexof_1': '1', + 'host_labels_orig_indexof_@!@': ''}}, + 'name': 'avm_smart_home_devices_metrics', + 'position': (1, 37), + 'show_title': True, + 'single_infos': [], + 'size': (0, 0), + 'type': 'linked_view'}, + {'background': True, + 'context': {'host_labels': {'host_labels_1_bool': 'and', + 'host_labels_1_vs_1_bool': 'and', + 'host_labels_1_vs_1_vs': 'fritz/smarthome/device:yes', + 'host_labels_1_vs_2_bool': 'and', + 'host_labels_1_vs_@:@_bool': 'and', + 'host_labels_1_vs_count': '2', + 'host_labels_1_vs_indexof_1': '1', + 'host_labels_1_vs_indexof_2': '2', + 'host_labels_1_vs_indexof_@:@': '', + 'host_labels_1_vs_orig_indexof_1': '1', + 'host_labels_1_vs_orig_indexof_2': '2', + 'host_labels_1_vs_orig_indexof_@:@': '', + 'host_labels_@!@_bool': 'and', + 'host_labels_@!@_vs_1_bool': 'and', + 'host_labels_@!@_vs_@:@_bool': 'and', + 'host_labels_@!@_vs_count': '1', + 'host_labels_@!@_vs_indexof_1': '1', + 'host_labels_@!@_vs_indexof_@:@': '', + 'host_labels_@!@_vs_orig_indexof_1': '1', + 'host_labels_@!@_vs_orig_indexof_@:@': '', + 'host_labels_count': '1', + 'host_labels_indexof_1': '1', + 'host_labels_indexof_@!@': '', + 'host_labels_orig_indexof_1': '1', + 'host_labels_orig_indexof_@!@': ''}, + 'service': {'service': 'Temperature'}}, + 'graph_render_options': {'fixed_timerange': False, + 'font_size': 8.0, + 'show_controls': False, + 'show_graph_time': True, + 'show_legend': True, + 'show_margin': False, + 'show_pin': True, + 'show_time_axis': True, + 'show_vertical_axis': True, + 'vertical_axis_width': 'fixed'}, + 'graph_template': 'temperature', + 'position': (1, -1), + 'presentation': 'lines', + 'show_title': True, + 'single_infos': [], + 'size': (0, 0), + 'timerange': 90000, + 'type': 'combined_graph'}, + {'background': True, + 'context': {'host_labels': {'host_labels_1_bool': 'and', + 'host_labels_1_vs_1_bool': 'and', + 'host_labels_1_vs_1_vs': 'fritz/smarthome/device:yes', + 'host_labels_1_vs_2_bool': 'and', + 'host_labels_1_vs_@:@_bool': 'and', + 'host_labels_1_vs_count': '2', + 'host_labels_1_vs_indexof_1': '1', + 'host_labels_1_vs_indexof_2': '2', + 'host_labels_1_vs_indexof_@:@': '', + 'host_labels_1_vs_orig_indexof_1': '1', + 'host_labels_1_vs_orig_indexof_2': '2', + 'host_labels_1_vs_orig_indexof_@:@': '', + 'host_labels_@!@_bool': 'and', + 'host_labels_@!@_vs_1_bool': 'and', + 'host_labels_@!@_vs_@:@_bool': 'and', + 'host_labels_@!@_vs_count': '1', + 'host_labels_@!@_vs_indexof_1': '1', + 'host_labels_@!@_vs_indexof_@:@': '', + 'host_labels_@!@_vs_orig_indexof_1': '1', + 'host_labels_@!@_vs_orig_indexof_@:@': '', + 'host_labels_count': '1', + 'host_labels_indexof_1': '1', + 'host_labels_indexof_@!@': '', + 'host_labels_orig_indexof_1': '1', + 'host_labels_orig_indexof_@!@': ''}, + 'service': {'service': 'Energy'}}, + 'graph_render_options': {'fixed_timerange': False, + 'font_size': 8.0, + 'show_controls': False, + 'show_graph_time': True, + 'show_legend': True, + 'show_margin': False, + 'show_pin': True, + 'show_time_axis': True, + 'show_vertical_axis': True, + 'vertical_axis_width': 'fixed'}, + 'graph_template': 'fritzbox_smart_home_energy_cost', + 'position': (54, 106), + 'presentation': 'sum', + 'show_title': True, + 'single_infos': [], + 'size': (0, 0), + 'timerange': 90000, + 'type': 'combined_graph'}, + {'background': True, + 'context': {}, + 'name': 'invavmsmarthomedevices_filtered', + 'position': (1, 71), + 'show_title': True, + 'single_infos': [], + 'size': (0, 0), + 'type': 'linked_view'}, + {'background': True, + 'context': {'host_labels': {'host_labels_1_bool': 'and', + 'host_labels_1_vs_1_bool': 'and', + 'host_labels_1_vs_1_vs': 'fritz/smarthome/device:yes', + 'host_labels_1_vs_2_bool': 'and', + 'host_labels_1_vs_@:@_bool': 'and', + 'host_labels_1_vs_count': '2', + 'host_labels_1_vs_indexof_1': '1', + 'host_labels_1_vs_indexof_2': '2', + 'host_labels_1_vs_indexof_@:@': '', + 'host_labels_1_vs_orig_indexof_1': '1', + 'host_labels_1_vs_orig_indexof_2': '2', + 'host_labels_1_vs_orig_indexof_@:@': '', + 'host_labels_@!@_bool': 'and', + 'host_labels_@!@_vs_1_bool': 'and', + 'host_labels_@!@_vs_@:@_bool': 'and', + 'host_labels_@!@_vs_count': '1', + 'host_labels_@!@_vs_indexof_1': '1', + 'host_labels_@!@_vs_indexof_@:@': '', + 'host_labels_@!@_vs_orig_indexof_1': '1', + 'host_labels_@!@_vs_orig_indexof_@:@': '', + 'host_labels_count': '1', + 'host_labels_indexof_1': '1', + 'host_labels_indexof_@!@': '', + 'host_labels_orig_indexof_1': '1', + 'host_labels_orig_indexof_@!@': ''}, + 'service': {'service': 'Power'}}, + 'graph_render_options': {'fixed_timerange': False, + 'font_size': 8.0, + 'show_controls': False, + 'show_graph_time': True, + 'show_legend': True, + 'show_margin': False, + 'show_pin': True, + 'show_time_axis': True, + 'show_vertical_axis': True, + 'vertical_axis_width': 'fixed'}, + 'graph_template': 'fritzbox_smart_home_electrical_power', + 'position': (1, 106), + 'presentation': 'stacked', + 'show_title': True, + 'single_infos': [], + 'size': (0, 0), + 'timerange': 90000, + 'type': 'combined_graph'}, + {'background': True, + 'context': {'host_labels': {'host_labels_1_bool': 'and', + 'host_labels_1_vs_1_bool': 'and', + 'host_labels_1_vs_@:@_bool': 'and', + 'host_labels_1_vs_count': '1', + 'host_labels_1_vs_indexof_1': '1', + 'host_labels_1_vs_indexof_@:@': '', + 'host_labels_1_vs_orig_indexof_1': '1', + 'host_labels_1_vs_orig_indexof_@:@': '', + 'host_labels_@!@_bool': 'and', + 'host_labels_@!@_vs_1_bool': 'and', + 'host_labels_@!@_vs_@:@_bool': 'and', + 'host_labels_@!@_vs_count': '1', + 'host_labels_@!@_vs_indexof_1': '1', + 'host_labels_@!@_vs_indexof_@:@': '', + 'host_labels_@!@_vs_orig_indexof_1': '1', + 'host_labels_@!@_vs_orig_indexof_@:@': '', + 'host_labels_count': '1', + 'host_labels_indexof_1': '1', + 'host_labels_indexof_@!@': '', + 'host_labels_orig_indexof_1': '1', + 'host_labels_orig_indexof_@!@': ''}, + 'serviceregex': {'neg_service_regex': '', + 'service_regex': 'Humidity'}}, + 'graph_render_options': {'fixed_timerange': False, + 'font_size': 8.0, + 'show_controls': False, + 'show_graph_time': True, + 'show_legend': True, + 'show_margin': False, + 'show_pin': True, + 'show_time_axis': True, + 'show_vertical_axis': True, + 'vertical_axis_width': 'fixed'}, + 'graph_template': 'fritzbox_smart_home_humidity', + 'position': (54, -2), + 'presentation': 'lines', + 'show_title': True, + 'single_infos': [], + 'size': (0, 0), + 'timerange': 90000, + 'type': 'combined_graph'}], + 'description': 'AVM Devices\n', + 'hidden': False, + 'hidebutton': False, + 'icon': 'logo-avm_fritz', + 'is_show_more': False, + 'link_from': {}, + 'mandatory_context_filters': [], + 'mtime': 1706207998, + 'name': 'avm', + 'packaged': False, + 'public': True, + 'show_title': True, + 'single_infos': [], + 'sort_index': 99, + 'title': 'AVM', + 'topic': 'other'}} diff --git a/source/gui/metrics/fritzbox_smarthome.py b/source/gui/metrics/fritzbox_smarthome.py index c3bb610..dd1872e 100644 --- a/source/gui/metrics/fritzbox_smarthome.py +++ b/source/gui/metrics/fritzbox_smarthome.py @@ -40,6 +40,11 @@ check_metrics["check_mk-fritzbox_smarthome_thermostat_multiple"] = { "temp_comfort": {"auto_graph": False}, } +metric_info["battery"] = { + "title": _("Battery"), + "color": "14/b", + "unit": "%", +} metric_info["cost_last_reading"] = { "title": _("Cost last"), "color": "11/b", @@ -92,7 +97,7 @@ metric_info["temp_target"] = { "unit": "c", } metric_info["temp_economic"] = { - "title": _("Temperature economic"), + "title": _("Temperature cool-down"), "color": "31/a", "unit": "c", } @@ -101,21 +106,11 @@ metric_info["temp_comfort"] = { "color": "11/a", "unit": "c", } - -graph_info["fritzbox_smart_home_energy_surrent"] = { - "title": "Electrical energy consumption since last reading", - "metrics": [ - ("energy_current", "area") - ] -} - -graph_info["fritzbox_smart_home_energy_time_span"] = { - "title": "Electrical energy time between readings", - "metrics": [ - ("energy_timespan", "area") - ] +metric_info["temp_deviation"] = { + "title": _("Temperature deviation"), + "color": "35/a", + "unit": "c", } - graph_info["fritzbox_smart_home_energy_cost"] = { "title": "Electrical energy cost", "metrics": [ @@ -132,6 +127,20 @@ graph_info["fritzbox_smart_home_energy_cost"] = { ], } +graph_info["fritzbox_smart_home_energy_surrent"] = { + "title": "Electrical energy consumption since last reading", + "metrics": [ + ("energy_current", "area") + ] +} + +graph_info["fritzbox_smart_home_energy_time_span"] = { + "title": "Electrical energy time between readings", + "metrics": [ + ("energy_timespan", "area") + ] +} + graph_info["fritzbox_smart_home_energy_total"] = { "title": "Electrical energy consumption total", "metrics": [ @@ -139,6 +148,18 @@ graph_info["fritzbox_smart_home_energy_total"] = { ] } +graph_info["fritzbox_smart_home_electrical_power"] = { + "title": "Electrical power", + "metrics": [ + ("power", "area") + ] +} +graph_info["fritzbox_smart_home_humidity"] = { + "title": "Relative humidity", + "metrics": [ + ("humidity", "area") + ] +} graph_info["fritzbox_smart_home_temp_control"] = { "title": _("Thermostat temperature control"), "metrics": [ @@ -147,7 +168,7 @@ graph_info["fritzbox_smart_home_temp_control"] = { ], "scalars": [ ("temp_comfort", "Temperature comfort"), - ("temp_economic", "Temperature economic"), + ("temp_economic", "Temperature cool-down"), ], "optional_metrics": [ "temp_target", @@ -173,3 +194,9 @@ perfometer_info.append({ "half_value": 100, "exponent": 3, }) + +perfometer_info.append({ + "type": "linear", + 'segments': ['battery'], + 'total': 100, +}) \ No newline at end of file diff --git a/source/gui/views/avm_fritzbox b/source/gui/views/avm_fritzbox new file mode 100644 index 0000000..edba899 --- /dev/null +++ b/source/gui/views/avm_fritzbox @@ -0,0 +1,59 @@ +{'avm_fritzbox': {'add_context_to_title': False, + 'browser_reload': 0, + 'column_headers': 'pergroup', + 'context': {'host_labels': {'host_labels_1_bool': 'and', + 'host_labels_1_vs_1_bool': 'and', + 'host_labels_1_vs_1_vs': 'cmk/os_family:FRITZ!OS', + 'host_labels_1_vs_2_bool': 'and', + 'host_labels_1_vs_@:@_bool': 'and', + 'host_labels_1_vs_count': '2', + 'host_labels_1_vs_indexof_1': '1', + 'host_labels_1_vs_indexof_2': '2', + 'host_labels_1_vs_indexof_@:@': '', + 'host_labels_1_vs_orig_indexof_1': '1', + 'host_labels_1_vs_orig_indexof_2': '2', + 'host_labels_1_vs_orig_indexof_@:@': '', + 'host_labels_@!@_bool': 'and', + 'host_labels_@!@_vs_1_bool': 'and', + 'host_labels_@!@_vs_@:@_bool': 'and', + 'host_labels_@!@_vs_count': '1', + 'host_labels_@!@_vs_indexof_1': '1', + 'host_labels_@!@_vs_indexof_@:@': '', + 'host_labels_@!@_vs_orig_indexof_1': '1', + 'host_labels_@!@_vs_orig_indexof_@:@': '', + 'host_labels_count': '1', + 'host_labels_indexof_1': '1', + 'host_labels_indexof_@!@': '', + 'host_labels_orig_indexof_1': '1', + 'host_labels_orig_indexof_@!@': ''}}, + 'datasource': 'hosts', + 'description': 'Displaying the overall state of AVM Fritzbox ' + 'Devices\n', + 'force_checkboxes': False, + 'group_painters': [], + 'hidden': True, + 'hidebutton': True, + 'icon': 'checkmk', + 'is_show_more': False, + 'layout': 'table', + 'link_from': {}, + 'mobile': False, + 'mustsearch': False, + 'name': 'avm_fritzbox', + 'num_columns': 1, + 'packaged': False, + 'painters': [{'name': 'host', 'parameters': {'color_choices': ['colorize_up', 'colorize_down', 'colorize_unreachable', 'colorize_pending', 'colorize_downtime']}, 'link_spec': ('views', 'host'), 'tooltip': 'host_addresses', 'join_value': None, 'column_title': '', 'column_type': 'column'}, + {'name': 'svc_plugin_output', 'parameters': {}, 'link_spec': None, 'tooltip': None, 'join_value': 'Connection', 'column_title': '', 'column_type': 'join_column'}, + {'name': 'svc_plugin_output', 'parameters': {}, 'link_spec': None, 'tooltip': None, 'join_value': 'Link Info', 'column_title': '', 'column_type': 'join_column'}, + {'name': 'perfometer', 'parameters': {}, 'link_spec': None, 'tooltip': None, 'join_value': 'Interface WAN', 'column_title': '', 'column_type': 'join_column'}, + {'name': 'perfometer', 'parameters': {}, 'link_spec': None, 'tooltip': None, 'join_value': 'NTP server', 'column_title': '', 'column_type': 'join_column'}, + {'name': 'perfometer', 'parameters': {}, 'link_spec': None, 'tooltip': None, 'join_value': 'Uptime', 'column_title': '', 'column_type': 'join_column'}], + 'play_sounds': False, + 'public': False, + 'single_infos': [], + 'sort_index': 6, + 'sorters': [('sitealias', False, None), + ('host_name', False, None)], + 'title': 'AVM Fritzbox', + 'topic': 'analyze', + 'user_sortable': True}} diff --git a/source/gui/views/avm_smart_home_devices_metrics b/source/gui/views/avm_smart_home_devices_metrics new file mode 100644 index 0000000..ded95fb --- /dev/null +++ b/source/gui/views/avm_smart_home_devices_metrics @@ -0,0 +1,62 @@ +{'avm_smart_home_devices_metrics': {'add_context_to_title': False, + 'browser_reload': 0, + 'column_headers': 'pergroup', + 'context': {'host_labels': {'host_labels_1_bool': 'and', + 'host_labels_1_vs_1_bool': 'and', + 'host_labels_1_vs_1_vs': 'fritz/smarthome/device:yes', + 'host_labels_1_vs_2_bool': 'and', + 'host_labels_1_vs_@:@_bool': 'and', + 'host_labels_1_vs_count': '2', + 'host_labels_1_vs_indexof_1': '1', + 'host_labels_1_vs_indexof_2': '2', + 'host_labels_1_vs_indexof_@:@': '', + 'host_labels_1_vs_orig_indexof_1': '1', + 'host_labels_1_vs_orig_indexof_2': '2', + 'host_labels_1_vs_orig_indexof_@:@': '', + 'host_labels_@!@_bool': 'and', + 'host_labels_@!@_vs_1_bool': 'and', + 'host_labels_@!@_vs_@:@_bool': 'and', + 'host_labels_@!@_vs_count': '1', + 'host_labels_@!@_vs_indexof_1': '1', + 'host_labels_@!@_vs_indexof_@:@': '', + 'host_labels_@!@_vs_orig_indexof_1': '1', + 'host_labels_@!@_vs_orig_indexof_@:@': '', + 'host_labels_count': '1', + 'host_labels_indexof_1': '1', + 'host_labels_indexof_@!@': '', + 'host_labels_orig_indexof_1': '1', + 'host_labels_orig_indexof_@!@': ''}}, + 'datasource': 'hosts', + 'description': 'Displaying the overall ' + 'state ofAVM SmartHome ' + 'Devices\n', + 'force_checkboxes': False, + 'group_painters': [], + 'hidden': True, + 'hidebutton': True, + 'icon': 'checkmk', + 'is_show_more': False, + 'layout': 'table', + 'link_from': {}, + 'mobile': False, + 'mustsearch': False, + 'name': 'avm_smart_home_devices_metrics', + 'num_columns': 1, + 'packaged': False, + 'painters': [{'name': 'host', 'parameters': {'color_choices': ['colorize_up', 'colorize_down', 'colorize_unreachable', 'colorize_pending', 'colorize_downtime']}, 'link_spec': ('views', 'host'), 'tooltip': 'host_addresses', 'join_value': None, 'column_title': '', 'column_type': 'column'}, + {'name': 'perfometer', 'parameters': {}, 'link_spec': None, 'tooltip': None, 'join_value': '~Battery', 'column_title': '', 'column_type': 'join_column'}, + {'name': 'perfometer', 'parameters': {}, 'link_spec': None, 'tooltip': None, 'join_value': '~Energy', 'column_title': '', 'column_type': 'join_column'}, + {'name': 'perfometer', 'parameters': {}, 'link_spec': None, 'tooltip': None, 'join_value': '~Humidity', 'column_title': '', 'column_type': 'join_column'}, + {'name': 'perfometer', 'parameters': {}, 'link_spec': None, 'tooltip': None, 'join_value': '~Power', 'column_title': '', 'column_type': 'join_column'}, + {'name': 'perfometer', 'parameters': {}, 'link_spec': None, 'tooltip': None, 'join_value': '~Temperature', 'column_title': '', 'column_type': 'join_column'}, + {'name': 'perfometer', 'parameters': {}, 'link_spec': None, 'tooltip': None, 'join_value': '~Thermostat', 'column_title': '', 'column_type': 'join_column'}, + {'name': 'perfometer', 'parameters': {}, 'link_spec': None, 'tooltip': None, 'join_value': '~Voltage', 'column_title': '', 'column_type': 'join_column'}], + 'play_sounds': False, + 'public': False, + 'single_infos': [], + 'sort_index': 6, + 'sorters': [('sitealias', False, None), + ('host_name', False, None)], + 'title': 'AVM SmartHome Devices (Metrics)', + 'topic': 'analyze', + 'user_sortable': True}} diff --git a/source/gui/views/avm_smart_home_devices_status b/source/gui/views/avm_smart_home_devices_status new file mode 100644 index 0000000..97f51e7 --- /dev/null +++ b/source/gui/views/avm_smart_home_devices_status @@ -0,0 +1,63 @@ +{'avm_smart_home_devices_status': {'add_context_to_title': False, + 'browser_reload': 0, + 'column_headers': 'pergroup', + 'context': {'host_labels': {'host_labels_1_bool': 'and', + 'host_labels_1_vs_1_bool': 'and', + 'host_labels_1_vs_1_vs': 'fritz/smarthome/device:yes', + 'host_labels_1_vs_2_bool': 'and', + 'host_labels_1_vs_@:@_bool': 'and', + 'host_labels_1_vs_count': '2', + 'host_labels_1_vs_indexof_1': '1', + 'host_labels_1_vs_indexof_2': '2', + 'host_labels_1_vs_indexof_@:@': '', + 'host_labels_1_vs_orig_indexof_1': '1', + 'host_labels_1_vs_orig_indexof_2': '2', + 'host_labels_1_vs_orig_indexof_@:@': '', + 'host_labels_@!@_bool': 'and', + 'host_labels_@!@_vs_1_bool': 'and', + 'host_labels_@!@_vs_@:@_bool': 'and', + 'host_labels_@!@_vs_count': '1', + 'host_labels_@!@_vs_indexof_1': '1', + 'host_labels_@!@_vs_indexof_@:@': '', + 'host_labels_@!@_vs_orig_indexof_1': '1', + 'host_labels_@!@_vs_orig_indexof_@:@': '', + 'host_labels_count': '1', + 'host_labels_indexof_1': '1', + 'host_labels_indexof_@!@': '', + 'host_labels_orig_indexof_1': '1', + 'host_labels_orig_indexof_@!@': ''}}, + 'datasource': 'hosts', + 'description': 'Displaying the overall ' + 'state ofAVM SmartHome ' + 'Devices\n', + 'force_checkboxes': False, + 'group_painters': [], + 'hidden': True, + 'hidebutton': True, + 'icon': 'checkmk', + 'is_show_more': False, + 'layout': 'table', + 'link_from': {}, + 'mobile': False, + 'mustsearch': False, + 'name': 'avm_smart_home_devices_status', + 'num_columns': 1, + 'packaged': False, + 'painters': [{'name': 'host', 'parameters': {'color_choices': ['colorize_up', 'colorize_down', 'colorize_unreachable', 'colorize_pending', 'colorize_downtime']}, 'link_spec': ('views', 'host'), 'tooltip': 'host_addresses', 'join_value': None, 'column_title': '', 'column_type': 'column'}, + {'name': 'svc_plugin_output', 'parameters': {}, 'link_spec': None, 'tooltip': None, 'join_value': '~Device status', 'column_title': '', 'column_type': 'join_column'}, + {'name': 'svc_plugin_output', 'parameters': {}, 'link_spec': None, 'tooltip': None, 'join_value': '~Power socket', 'column_title': '', 'column_type': 'join_column'}, + {'name': 'service_state', 'parameters': {}, 'link_spec': None, 'tooltip': None, 'join_value': '~Battery', 'column_title': '', 'column_type': 'join_column'}, + {'name': 'service_state', 'parameters': {}, 'link_spec': None, 'tooltip': None, 'join_value': '~Humidity', 'column_title': '', 'column_type': 'join_column'}, + {'name': 'service_state', 'parameters': {}, 'link_spec': None, 'tooltip': None, 'join_value': '~Temperature', 'column_title': '', 'column_type': 'join_column'}, + {'name': 'service_state', 'parameters': {}, 'link_spec': None, 'tooltip': None, 'join_value': '~Thermostat', 'column_title': '', 'column_type': 'join_column'}, + {'name': 'service_state', 'parameters': {}, 'link_spec': None, 'tooltip': None, 'join_value': '~Power', 'column_title': '', 'column_type': 'join_column'}, + {'name': 'service_state', 'parameters': {}, 'link_spec': None, 'tooltip': None, 'join_value': '~Voltage', 'column_title': '', 'column_type': 'join_column'}], + 'play_sounds': False, + 'public': False, + 'single_infos': [], + 'sort_index': 6, + 'sorters': [('sitealias', False, None), + ('host_name', False, None)], + 'title': 'AVM SmartHome Devices (status)', + 'topic': 'analyze', + 'user_sortable': True}} diff --git a/source/gui/views/invavmsmarthomedevices_filtered b/source/gui/views/invavmsmarthomedevices_filtered new file mode 100644 index 0000000..603c72d --- /dev/null +++ b/source/gui/views/invavmsmarthomedevices_filtered @@ -0,0 +1,69 @@ +{'invavmsmarthomedevices_filtered': {'add_context_to_title': True, + 'browser_reload': 0, + 'column_headers': 'pergroup', + 'context': {'host_labels': {'host_labels_1_bool': 'and', + 'host_labels_1_vs_1_bool': 'and', + 'host_labels_1_vs_1_vs': 'fritz/smarthome/device:yes', + 'host_labels_1_vs_2_bool': 'and', + 'host_labels_1_vs_@:@_bool': 'and', + 'host_labels_1_vs_count': '2', + 'host_labels_1_vs_indexof_1': '1', + 'host_labels_1_vs_indexof_2': '2', + 'host_labels_1_vs_indexof_@:@': '', + 'host_labels_1_vs_orig_indexof_1': '1', + 'host_labels_1_vs_orig_indexof_2': '2', + 'host_labels_1_vs_orig_indexof_@:@': '', + 'host_labels_@!@_bool': 'and', + 'host_labels_@!@_vs_1_bool': 'and', + 'host_labels_@!@_vs_@:@_bool': 'and', + 'host_labels_@!@_vs_count': '1', + 'host_labels_@!@_vs_indexof_1': '1', + 'host_labels_@!@_vs_indexof_@:@': '', + 'host_labels_@!@_vs_orig_indexof_1': '1', + 'host_labels_@!@_vs_orig_indexof_@:@': '', + 'host_labels_count': '1', + 'host_labels_indexof_1': '1', + 'host_labels_indexof_@!@': '', + 'host_labels_orig_indexof_1': '1', + 'host_labels_orig_indexof_@!@': ''}, + 'invavmsmarthomedevices_functions': {'invavmsmarthomedevices_functions': ''}, + 'invavmsmarthomedevices_fw_version': {'invavmsmarthomedevices_fw_version': ''}, + 'invavmsmarthomedevices_id': {'invavmsmarthomedevices_id': ''}, + 'invavmsmarthomedevices_identifier': {'invavmsmarthomedevices_identifier': ''}, + 'invavmsmarthomedevices_manufacturer': {'invavmsmarthomedevices_manufacturer': ''}, + 'invavmsmarthomedevices_name': {'invavmsmarthomedevices_name': ''}, + 'invavmsmarthomedevices_product_name': {'invavmsmarthomedevices_product_name': ''}}, + 'datasource': 'invavmsmarthomedevices', + 'description': 'A view for searching in ' + 'the inventory data for ' + 'Smart home devices ' + '(filtered)\n', + 'force_checkboxes': False, + 'group_painters': [], + 'hidden': False, + 'hidebutton': False, + 'icon': None, + 'inventory_join_macros': {'macros': []}, + 'is_show_more': True, + 'layout': 'table', + 'link_from': {}, + 'mobile': False, + 'mustsearch': False, + 'name': 'invavmsmarthomedevices_filtered', + 'num_columns': 1, + 'packaged': False, + 'painters': [{'name': 'host', 'parameters': {'color_choices': []}, 'link_spec': ('views', 'inv_host'), 'tooltip': None, 'join_value': None, 'column_title': '', 'column_type': 'column'}, + {'name': 'invavmsmarthomedevices_id', 'parameters': {}, 'link_spec': None, 'tooltip': None, 'join_value': None, 'column_title': '', 'column_type': 'column'}, + {'name': 'invavmsmarthomedevices_product_name', 'parameters': {}, 'link_spec': None, 'tooltip': None, 'join_value': None, 'column_title': '', 'column_type': 'column'}, + {'name': 'invavmsmarthomedevices_fw_version', 'parameters': {}, 'link_spec': None, 'tooltip': None, 'join_value': None, 'column_title': '', 'column_type': 'column'}, + {'name': 'invavmsmarthomedevices_identifier', 'parameters': {}, 'link_spec': None, 'tooltip': None, 'join_value': None, 'column_title': '', 'column_type': 'column'}, + {'name': 'invavmsmarthomedevices_functions', 'parameters': {}, 'link_spec': None, 'tooltip': None, 'join_value': None, 'column_title': '', 'column_type': 'column'}], + 'play_sounds': False, + 'public': False, + 'single_infos': [], + 'sort_index': 30, + 'sorters': [], + 'title': 'Search Smart home devices ' + '(filtered)', + 'topic': 'inventory', + 'user_sortable': True}} diff --git a/source/gui/wato/check_parameters/fritzbox_smarthome.py b/source/gui/wato/check_parameters/fritzbox_smarthome.py index cf75d6a..eed2e85 100644 --- a/source/gui/wato/check_parameters/fritzbox_smarthome.py +++ b/source/gui/wato/check_parameters/fritzbox_smarthome.py @@ -78,6 +78,16 @@ def _parameter_valuespec_fritzbox_smarthome_thermostat(): title=_('Monitoring state if thermostat is off'), default_value=0, )), + ('state_windows_open', + MonitoringState( + title=_('Monitoring state if windows open active'), + default_value=1, + )), + ('state_boost_mode', + MonitoringState( + title=_('Monitoring state if boost mode active'), + default_value=1, + )), ('state_on_error', MonitoringState( title=_('Monitoring state on error'), @@ -119,6 +129,13 @@ def _parameter_valuespec_fritzbox_smarthome_battery(): title=_('Monitoring state on low battery'), default_value=2, )), + ('levels_lower', + Tuple( + title=_('Lower levels for battery'), + elements=[ + Integer(title=_('Warning below'), default_value=50, unit=_('%')), + Integer(title=_('Critical below'), default_value=40, unit=_('%')), + ])), ], ) diff --git a/source/lib/python3/cmk/special_agents/agent_fritzbox_smarthome.py b/source/lib/python3/cmk/special_agents/agent_fritzbox_smarthome.py index 4608565..f335250 100644 --- a/source/lib/python3/cmk/special_agents/agent_fritzbox_smarthome.py +++ b/source/lib/python3/cmk/special_agents/agent_fritzbox_smarthome.py @@ -5,26 +5,107 @@ # 2023-12-18: modified to work with cmk 2.2.x # changed to return the complete XML response back as json # 2023-12-28: added data/option for testing +# 2024-01-11: reworked to support PBKDF2 -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 argparse import ArgumentParser, RawTextHelpFormatter +from collections import Counter +from hashlib import pbkdf2_hmac, md5 +from json import dumps from re import sub as re_sub +from requests import exceptions as r_exceptions, Response, session +from sys import exit, stderr, stdout +from time import sleep +from urllib3 import disable_warnings +from xml.etree import ElementTree + from cmk.utils.password_store import replace_passwords +class AvmSession: + def __init__( + self, + host: str, + ignore_ssl: bool = False, + protocol: str = 'https', + port: str = '443', + no_pbkdf2: bool = False, + ): + self._session = session() + self._base_url = f'{protocol}://{host}:{port}' + self._verify = not ignore_ssl + self._pbkdf2 = not no_pbkdf2 + self._sid = '' + + if not self._verify: + disable_warnings() + if self._pbkdf2: + self._version = '&version=2' + else: + self._version = '' + + def _get(self, url) -> Response: + try: + response = self._session.get(url=url, verify=self._verify) + except (r_exceptions.ConnectionError, r_exceptions.SSLError) as e: + stderr.write(f'fritzbox_smarthome\n {e}\n') + exit(1) + + if response.status_code != 200: + stdout.write(f'can not connect, status: {response.status_code}, {response.text}') + exit(1) + return response + + def get(self, url: str) -> Response: + return self._get(url=f'{self._base_url}/{url}{self._version}&sid={self._sid}') + + def login(self, username: str, password: str): + # CALL /login_sid.lua and grab challenge + response = self._get(url=f'{self._base_url}/login_sid.lua?{self._version}') + + xml_login = str_to_xml(response.text) + check_block_time(xml=xml_login) # stop if block time > 10 + + challenge = xml_login.find('Challenge').text + + if self._pbkdf2: + challenge_response = calculate_pbkdf2_response(challenge=challenge, password=password) + else: + challenge_response = calculate_md5_response(challenge=challenge, password=password) + + # CALL /login_sid.lua?username=<username>&response=<challenge_response> + # and grab session-id + url = f'{self._base_url}/login_sid.lua?username={username}&response={challenge_response}{self._version}' + response = self._get(url=url) + xml_login_solve = str_to_xml(response.text) + check_block_time(xml=xml_login_solve) # stop if block time > 10 + self._sid = xml_login_solve.find('SID').text + if self._sid == '0000000000000000': + raise Exception('Check credentials\n') + + def logout(self): + url = f'/login_sid.lua?logout=logout' + self.get(url=url) + self._session.close() + + # based on: https://stackoverflow.com/a/47081240 def parse_xml_to_json(xml): response = {} for key in xml.keys(): response[key] = xml.get(key) + + # add index to duplicate child names (i.e. button) + tags = [child.tag for child in list(xml)] + duplicates = [k for k, v in Counter(tags).items() if v > 1] + if duplicates: + for tag in duplicates: + index = 0 + for child in list(xml): + if child.tag == tag: + child.tag = f'{child.tag}{index}' + index += 1 + for child in list(xml): for key in child.keys(): response[key] = child.get(key) @@ -37,17 +118,34 @@ def parse_xml_to_json(xml): return response +def str_to_xml(text: str) -> ElementTree.Element: + try: + return ElementTree.fromstring(text) + except ElementTree.ParseError as e: + stderr.write(f'XML parse error. {e}') + exit(1) + + def parse_args(): - parser = argparse.ArgumentParser( + parser = ArgumentParser( description='Check_MK Fritz!Box Smarthome Agent\n' 'This is an additional check_MK Fritz!Box Agent which can gather information\'s over the \n' 'AVM AHA HTTP Interface about SmartHome Devices connected to an Fritz!Box.', - formatter_class=argparse.RawTextHelpFormatter, + formatter_class=RawTextHelpFormatter, ) parser.add_argument( - 'host', + 'host', type=str, help='Host name or IP address of your Fritz!Box', ) + parser.add_argument( + '--username', type=str, required=True, + help='The username to logon to the Fritz!Box', + ) + parser.add_argument( + '--password', type=str, required=True, + help='The password to logon the Fritz!Box', + ) + parser.add_argument( '--debug', action='store_true', default=False, help='Debug mode: let Python exceptions come through', @@ -63,222 +161,202 @@ def parse_args(): ' to your Fritz!Box use this option.', ) parser.add_argument( - '--password', nargs='?', - help='The password to logon the Fritz!Box', - ) - parser.add_argument( - '--username', nargs='?', - help='The username to logon to the Fritz!Box', - ) - parser.add_argument( - '--port', nargs='?', type=int, default=443, + '--port', type=int, default=443, help='The TCP port on witch to access the Fritz!Box', ) parser.add_argument( - '--prefix', nargs='?', + '--prefix', type=str, help='The prefix is used to group all the Smarthome devices from one Fritz!Box in CMK.' ) parser.add_argument( - '--protocol', nargs='?', choices=['http', 'https'], default='https', + '--protocol', type=str, choices=['http', 'https'], default='https', help='The protocol used to access the Fritz!Box', ) - + parser.add_argument( + '--no-pbkdf2', action='store_true', default=False, + help='This will disable the use of PBDKF2 (Password-Based Key Derivation Function 2) and ' + 'fall back to MD5 (less secure)' + ) parser.add_argument( '--testing', action='store_true', default=False, help='Development usage only (might be ignored)' ) + args = parser.parse_args() return args -def check_fritzbox_smarthome(args): - base_address = '%s://%s:%d' % (args.protocol, args.host, args.port) - - ctx = ssl.create_default_context() - if args.ignore_ssl: - ctx.check_hostname = False - ctx.verify_mode = ssl.CERT_NONE - - # CALL /login_sid.lua - # and grab challenge - response = urlopen(base_address + '/login_sid.lua', context=ctx) - if args.password: - xml_login = ET.fromstring(response.read()) - challenge = xml_login.find('Challenge').text - blocktime = int(xml_login.find('BlockTime').text) - if blocktime > 0: - sys.stdout.write('<<<fritzbox_smarthome:sep(0)>>>') - sys.stdout.write(json.dumps({'block_time': blocktime})) - exit() +def check_block_time(xml: ElementTree.fromstring) -> bool: + block_time = int(xml.find('BlockTime').text) + if 10 < block_time > 0: + sleep(block_time) + elif block_time > 10: + stdout.write('<<<fritzbox_smarthome:sep(0)>>>') + stdout.write(dumps({'block_time': block_time})) + exit() - # create challenge_response (hash with md5: '<challenge>-<password>') - # TODO: check if challenge is PBKDF2 (startswith $2) - digest = hashlib.md5() - digest.update(challenge.encode('utf-16le')) - digest.update('-'.encode('utf-16le')) - digest.update(args.password.encode('utf-16le')) + return False - challenge_response = challenge + '-' + digest.hexdigest() - # CALL /login_sid.lua?username=<username>&response=<challenge_response> - # and grab sessionid - if args.username: - response = urlopen( - base_address + '/login_sid.lua?username=%s&response=%s' % (args.username, challenge_response), - context=ctx) - else: - response = urlopen(base_address + '/login_sid.lua?response=%s' % challenge_response, context=ctx) +def calculate_md5_response(challenge: str, password: str) -> str: + digest = md5() + digest.update(challenge.encode('utf-16le')) + digest.update('-'.encode('utf-16le')) + digest.update(password.encode('utf-16le')) - xml_login_solve = ET.fromstring(response.read()) - sessionid = xml_login_solve.find('SID').text + return challenge + '-' + digest.hexdigest() - blocktime = int(xml_login_solve.find('BlockTime').text) - if blocktime > 0: - sys.stdout.write('<<<fritzbox_smarthome:sep(0)>>>') - sys.stdout.write(json.dumps({'block_time': blocktime})) - exit() - if args.password and sessionid == '0000000000000000': - raise Exception('Check credentials\n') +def calculate_pbkdf2_response(challenge: str, password: str) -> str: + """ Calculate the response for a given challenge via PBKDF2 """ + # Extract all necessary values encoded into the challenge + version, iter1, salt1, iter2, salt2 = challenge.split('$') + # Hash twice, once with static salt... + hash1 = pbkdf2_hmac('sha256', password.encode(), bytes.fromhex(salt1), int(iter1)) + # Once with dynamic salt. + hash2 = pbkdf2_hmac('sha256', hash1, bytes.fromhex(salt2), int(iter2)) + return f'{salt2}${hash2.hex()}' + + +def check_fritzbox_smarthome(args): + + avm_session = AvmSession( + ignore_ssl=args.ignore_ssl, + host=args.host, + protocol=args.protocol, + port=args.port, + no_pbkdf2=args.no_pbkdf2, + ) + + avm_session.login(username=args.username, password=args.password) + + # get device data + response = avm_session.get(url=f'/webservices/homeautoswitch.lua?switchcmd=getdevicelistinfos') + + response_read = response.text - # Write section header - response = urlopen( - base_address + '/webservices/homeautoswitch.lua?switchcmd=getdevicelistinfos&sid=%s' % sessionid, context=ctx) - response_read = response.read() if args.debug: - sys.stdout.write('Raw XML:\n') - sys.stdout.write(str(response_read)) - sys.stdout.write('\n') + stdout.write('Raw XML:\n') + stdout.write(str(response_read)) + stdout.write('\n') - xml_devicelist = ET.fromstring(response_read) + xml_device_list = str_to_xml(response_read) devices = [] if args.testing: __switch_01 = { - "identifier": "08761 0116372", - "id": "99", - "functionbitmask": "35712", - "fwversion": "04.26", - "manufacturer": "AVM", - "productname": "FRITZ!DECT 200", - "present": "1", - "txbusy": "0", - "name": "TV-living_room", - "switch": { - "state": "1", - "mode": "manuell", - "lock": "0", - "devicelock": "0" + 'identifier': '08761 0116372', + 'id': '99', + 'functionbitmask': '35712', + 'fwversion': '04.26', + 'manufacturer': 'AVM', + 'productname': 'FRITZ!DECT 200', + 'present': '1', + 'txbusy': '0', + 'name': 'TV-living_room', + 'switch': { + 'state': '1', + 'mode': 'manuell', + 'lock': '0', + 'devicelock': '0' }, - "simpleonoff": { - "state": "1" + 'simpleonoff': { + 'state': '1' }, - "powermeter": { - "voltage": "235814", - "power": "4220", - "energy": "145427" + 'powermeter': { + 'voltage': '235814', + 'power': '4220', + 'energy': '145427' }, - "temperature": { - "celsius": "190", - "offset": "0" + 'temperature': { + 'celsius': '190', + 'offset': '0' } } __repeater_01 = { - "identifier": "11657 0057950", - "id": "98", - "functionbitmask": "1024", - "fwversion": "04.16", - "manufacturer": "AVM", - "productname": "FRITZ!DECT Repeater 100", - "present": "0", - "txbusy": "0", - "name": "FRITZ!DECT Rep 100 #1" + '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" + '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" + '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" + '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'): + for xml_device in xml_device_list.findall('device'): devices.append(parse_xml_to_json(xml_device)) if args.no_piggyback: - sys.stdout.write('<<<fritzbox_smarthome:sep(0)>>>\n') - # if len(devices) == 1: - # sys.stdout.write(json.dumps(devices[0])) # single device - # else: - sys.stdout.write(json.dumps(devices)) - sys.stdout.write('\n') + stdout.write('<<<fritzbox_smarthome:sep(0)>>>\n') + stdout.write(dumps(devices)) + stdout.write('\n') else: for json_device in devices: - name = json_device["name"].replace(' ', '_') + 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') + stdout.write(f'<<<<{name}>>>>\n') + stdout.write('<<<fritzbox_smarthome:sep(0)>>>\n') + stdout.write(dumps(json_device)) + stdout.write('\n') + + # logout (invalidate the session-id) + avm_session.logout() def main(): replace_passwords() args = parse_args() - try: - check_fritzbox_smarthome(args) - except: - if args.debug: - raise - sys.stderr.write('fritzbox_smarthome\n %s\n' % traceback.format_exc()) - sys.exit(2) + check_fritzbox_smarthome(args) diff --git a/source/packages/fritzbox_smarthome b/source/packages/fritzbox_smarthome index ddd982d..5c03fcf 100644 --- a/source/packages/fritzbox_smarthome +++ b/source/packages/fritzbox_smarthome @@ -10,18 +10,14 @@ 'devices, I have only implemented the checks for the following ' 'devices:\n' '\n' - 'FRITZ!DECT Repeater 100\n' - 'FRITZ!DECT 200\n' - 'FRITZ!DECT 302\n' + ' - FRITZ!DECT Repeater 100\n' + ' - FRITZ!DECT 200/210\n' + ' - FRITZ!DECT 301/302\n' + ' - FRITZ!DECT 440\n' '\n' 'So if you want the package to be extended to support your ' 'sensors as well, see\n' 'https://thl-cmk.hopto.org/gitlab/checkmk/various/fritzbox_smarthome/-/blob/master/CONTRIBUTING.md\n' - '\n' - 'Also, my FRIT!BOX is not brand new, so it may not include all ' - 'the features that the smart home \n' - 'devices support. E.g. window open/close for the FRITZ!DECT ' - '302.\n' '\n', 'download_url': 'https://thl-cmk.hopto.org/gitlab/checkmk/various/fritzbox_smarthome', 'files': {'agent_based': ['fritzbox_smarthome.py', @@ -34,7 +30,9 @@ 'fritzbox_smarthome_app_lock.py', 'fritzbox_smarthome_device_lock.py', 'fritzbox_smarthome_power_socket.py', - 'fritzbox_smarthome_switch.py'], + 'fritzbox_smarthome_switch.py', + 'fritzbox_smarthome_button.py', + 'fritzbox_smarthome_humidity.py'], 'agents': ['special/agent_fritzbox_smarthome'], 'checkman': ['fritzbox_smarthome'], 'checks': ['agent_fritzbox_smarthome'], @@ -45,13 +43,18 @@ 'wato/check_parameters/temperature_single.py', 'wato/check_parameters/voltage_single.py', 'wato/check_parameters/fritzbox_smarthome_lock.py', - 'wato/check_parameters/fritzbox_smarthome_power_coscket.py'], + 'wato/check_parameters/fritzbox_smarthome_power_coscket.py', + 'dashboard/avm', + 'views/avm_fritzbox', + 'views/avm_smart_home_devices_metrics', + 'views/avm_smart_home_devices_status', + 'views/invavmsmarthomedevices_filtered'], 'lib': ['python3/cmk/special_agents/agent_fritzbox_smarthome.py'], 'web': ['plugins/wato/agent_fritzbox_smarthome.py', 'plugins/views/fritzbox_smarthome.py']}, 'name': 'fritzbox_smarthome', 'title': 'Fritz!Box SmartHome', - 'version': '0.8.8-20240109', + 'version': '0.8.17-20240125', 'version.min_required': '2.2.0b1', 'version.packaged': '2.2.0p17', 'version.usable_until': '2.3.0b1'} diff --git a/source/web/plugins/views/fritzbox_smarthome.py b/source/web/plugins/views/fritzbox_smarthome.py index 76733ec..d8dfda8 100644 --- a/source/web/plugins/views/fritzbox_smarthome.py +++ b/source/web/plugins/views/fritzbox_smarthome.py @@ -34,7 +34,7 @@ inventory_displayhints.update({ '.hardware.avm.smart_home_devices:*.name': {'title': _l('Name')}, '.hardware.avm.smart_home_devices:*.manufacturer': {'title': _l('Manufacturer')}, '.hardware.avm.smart_home_devices:*.product_name': {'title': _l('Product name')}, - '.hardware.avm.smart_home_devices:*.fw_version': {'title': _l('Firmware version')}, + '.hardware.avm.smart_home_devices:*.fw_version': {'title': _l('Firmware version'), 'short': _l('Firmware')}, '.hardware.avm.smart_home_devices:*.identifier': {'title': _l('Identifier')}, '.hardware.avm.smart_home_devices:*.functions': {'title': _l('Functions')}, }) diff --git a/source/web/plugins/wato/agent_fritzbox_smarthome.py b/source/web/plugins/wato/agent_fritzbox_smarthome.py index cc2f867..205dae0 100644 --- a/source/web/plugins/wato/agent_fritzbox_smarthome.py +++ b/source/web/plugins/wato/agent_fritzbox_smarthome.py @@ -72,6 +72,13 @@ def _valuespec_special_agents_fritzbox_smarthome() -> ValueSpec: totext='', title=_('Disable piggyback'), )), + ('no_pbkdf2', FixedValue( + value=True, + help='The login will fallback from PBDKF2 (Password-Based Key Derivation Function 2) to MD5 ' + '(less secure) challenge response method', + totext='', + title=_('Disable PBDKF2'), + )), ('testing', FixedValue( value=True, help='Development only, will be (most likely) ignored in production :-)', -- GitLab