diff --git a/README.md b/README.md index ef1f7157a6ee69dee91f60c1c262d94a081dc651..2217179814db02bbc79a4d05955b82734ea0d489 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 Binary files /dev/null and b/mkp/fritzbox_smarthome-0.8.17-20240125.mkp differ diff --git a/source/agent_based/fritzbox_smarthome_battery.py b/source/agent_based/fritzbox_smarthome_battery.py index 57f355669d3b1c01787d81659ad6df845605ed75..e9c8b7554d0fea240779522e628e5a85f00eec11 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 0000000000000000000000000000000000000000..424d950788c7b273d13f3ef94e1b830ea49957c4 --- /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 0000000000000000000000000000000000000000..caca820e0ae482ac79667c3f2d1e8f7b63b3d108 --- /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 d966c890ccdefd9fba24b9349b8d78c4bff6e469..74a8773606a58dd9e30c108f12c633b956e0ab50 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 86ebd473f2b7f45ab9c3bcee70719873cbceb1f1..ac7923142d5770a692a90d2653074d6aed82145a 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 9828c3c27adc800bb5c66a4af1dbdceeb097165a..ee9aa2019a0835ff393d2e7ed02afa3121bd6b7d 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 b36185232ce42cf1a6a72f5ebdd1cce2d8d9bc5d..9669cead913fa3df938a6edfdcb9c12da314bc42 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 26a785476309e6f6aae7e43c7d47bd8579f0f397..33c8a5daf955f1cf9fb90696fc70cf81b5252f84 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 0000000000000000000000000000000000000000..c81a61a07084be0940da81fd19580ba26c732d83 --- /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 c3bb61035fddc805fbbc83ad2a06aa5529d7ba3e..dd1872ecd2976f4f9da2448c9bb0840b2850113e 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 0000000000000000000000000000000000000000..edba89994a3cc28f3f548d7e81b07d17d64eb0a4 --- /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 0000000000000000000000000000000000000000..ded95fb834d53fcb6fb3a352e9dd6d6560e1f95e --- /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 0000000000000000000000000000000000000000..97f51e7ab3ea82ed726214307180bb7c0ca24545 --- /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 0000000000000000000000000000000000000000..603c72d63538eafdc13d7a0fe5b88fd0a8d5a3f0 --- /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 cf75d6a8dcb4e4f13d8c47a58f034b9035c44f50..eed2e85fe20988f785886d57555113dc3cacade7 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 460856530155e5033de6be34a76c4aeae2116331..f335250757427df369e94484e1c1e544257738d8 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 ddd982dc7b721b1778e897221e46ca3db9095f07..5c03fcfd1e96568ffbf7e831e4183cf6bb6e9946 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 76733ecaa3209fc5d02fec3438979d4717bb926a..d8dfda80a3bc4fdbed472fae6cd154f92a47ab5c 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 cc2f86736981940299dbac01368d26e0920ba7d8..205dae003bd3e9d2b5ff695d3f41dbcb4dc2be68 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 :-)',