diff --git a/README.md b/README.md index 1a45bc1..b4863dc 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,9 @@ # Munin Plugins for FritzBox -A collection of munin plugins to monitor your AVM FRITZ!Box router. The scripts have been developed using a FRITZ!Box 7590 running FRITZ!OS 7.50 and a FRITZ!WLAN Repeater 1750E running FRITZ!OS 7.27. +A collection of munin plugins to monitor your AVM FRITZ!Box +router. The scripts have been developed using a FRITZ!Box +7490,7590,5690 Pro running FRITZ!OS 7.50...7.62 and a FRITZ!WLAN +Repeater 1750E running FRITZ!OS 7.27. If you are using the scripts on a different FRITZ!Box model please let me know by @@ -9,6 +12,7 @@ If you are using the scripts on a different FRITZ!Box model please let me know b These python scripts are [Munin](http://munin-monitoring.org) plugins for monitoring the [FRITZ!Box](https://avm.de/produkte/fritzbox/) router by AVM. + ## Purpose of this Fork The scripts are build upon the original [fritzbox-munin](https://github.com/Tafkas/fritzbox-munin) with the goal to make use of the modern APIs that FRITZ!OS 7 provides. @@ -25,7 +29,9 @@ The main differences to the original version are: - FRITZ!Box router with FRITZ!OS >= 07.50 (if you are on an older FRITZ!OS version, select an older version of fritzbox-munin-fast by browsing the tags in this repository) - Munin 1.4.0 or later is required - Python 3.x - +- Recommendation: python venv installation in munin home directory + + ## Available Plugins ### Connection Uptime @@ -36,12 +42,12 @@ Shows the WAN connection uptime. ### DSL Errors Plugin: `fritzbox_dsl.py` Multigraph plugin, showing: - - DSL checksum errors - - DSL transmission errors - - line loss - - link capacity - - error correction statistics - - signal-to-noise ratio + - DSL checksum errors
![DSL checksum errors](doc/dsl_crc-month.png) + - DSL transmission errors
![DSL transmission errors](doc/dsl_errors-month.png) + - line loss
![line loss](doc/dsl_damping-month.png) + - signal-to-noise ratio
![signal-to-noise ratio](doc/dsl_snr-month.png) + - link capacity
![link capacity](doc/dsl_capacity-month.png) + - error correction statistics
![error correction statistics](doc/dsl_ecc-month.png) ### CPU & Memory Plugin: `fritzbox_ecostat.py` @@ -50,9 +56,30 @@ Multigraph plugin, showing: - CPU load - CPU temperature -### Smart Home Temperature -Plugin: `fritzbox_smart_home_temperature.py` -![Smart Home Temperature](doc/smart_home_temperature.png) +Note: Currently not supported by FRITZ!Box 5690 Pro with FRITZ!OS 7.62. + +### Smart Home +Plugin: `fritzbox_smart_home.py` +Multigraph plugin, showing + - Battery state of battery driven smart home devices +
![Battery state of battery driven smart home devices](doc/battery-month.png) + - Energy (kWh aggregated over lifetime) of smart home power devices +
![Energy (kWh aggregated over lifetime)](doc/energy-month.png) + - Instantaneous power of smart home power devices (W instantaneously, sub-sampled due to 5 minute munin sampling grid, most accurate in resolution, inaccurate w.r.t. energy/average due to sub-sampling) +
![Instantaneous power of smart home power devices](doc/smarthome_power-week.png) + - Average power of smart home power devices (W derived from energy counters, accurate w.r.t. average and hence no sub-sampling issue, tendency to visual noise in 5 minute sampling grid, but exact when averaged e.g. in weekly plots with 30 min sampling grid) +
![Average power of smart home power devices: Day](doc/smarthome_powerAvg-day.png) + ![Average power of smart home power devices: Week](doc/smarthome_powerAvg-week.png) + - Power switch on/off + - Voltage measurements for smart home power devivces +
![Voltage measurements for smart home power devivces](doc/voltage-day.png) + - Measured temperature for temperature sensors +
![Measured temperature for temperature sensors](doc/temperatures-day.png) + - Target temperature for thermostats + - Humidity sensors +
![Humidity sensors](doc/humidity-day.png) + + ### Energy Plugin: `fritzbox_energy.py` @@ -61,25 +88,66 @@ Multigraph plugin, showing: - devices connected on WiFi and LAN - system uptime +Note: Currently not supported by FRITZ!Box 5690 Pro with FRITZ!OS 7.62. + ### Link Saturation Plugin: `fritzbox_link_saturation.py` Multigraph plugin, showing saturation of WAN uplink and downlink by QoS priority +
![Uplink saturation](doc/saturation_up-day.png) + ![Downlink saturation](doc/saturation_down-day.png) + ### Traffic Plugin: `fritzbox_traffic.py` Similar to fritzbox_link_saturation, but single-graph and without QoS monitoring. -### Wifi +### Wifi Load Plugin: `fritzbox_wifi_load.py` -Multigraph plugin, showing for 2.4GHz and 5GHz +Multigraph plugin + - 2.4 GHz + - 5 GHz + - 6 GHz - WiFi uplink and downlink bandwidth usage +
![2.4 GHz Saturation](doc/bandwidth_24ghz-week.png) + ![5 GHz Saturation](doc/bandwidth_5ghz-week.png) - neighbor APs on same and on different channels - +
![2.4 GHz Neighbors](doc/neighbors_24ghz-week.png) + ![5 GHz Neighbors](doc/neighbors_5ghz-week.png) + +### Wifi Speed +Plugin: `fritzbox_wifi_speed.py` +Multigraph plugin + - 2.4 GHz instantaneous RX and TX speeds per connected device +
![2.4 GHz RX Speed](doc/wifiDeviceSpeed_ghz24_rx-week.png) + ![2.4 GHz TX Speed](doc/wifiDeviceSpeed_ghz24_tx-week.png) + - 5 GHz instantaneous RX and TX speeds per connected device +
![5 GHz RX Speed](doc/wifiDeviceSpeed_ghz5_rx-week.png) + ![5 GHz TX Speed](doc/wifiDeviceSpeed_ghz5_tx-week.png) + - 6 GHz instantaneous RX and TX speeds per connected device +
![6 GHz RX Speed](doc/wifiDeviceSpeed_ghz6_rx-week.png) + ![6 GHz TX Speed](doc/wifiDeviceSpeed_ghz6_tx-week.png) + - Ethernet instantaneous RX and TX speeds per connected device + ## Installation & Configuration + 1. Pre-requisites for the `fritzbox_traffic` and `fritzbox_connection_uptime` plugins are the [fritzconnection](https://pypi.python.org/pypi/fritzconnection) and [requests](https://pypi.python.org/pypi/requests) package. To install run + - Recommended: python venv in munin home directory + + ``` + sudo -u munin bash + cd ~munin + python3 -m venv venv + source ~munin/venv/bin/activate + ~munin/venv/bin/python3 -m pip install -r /requirements.txt + ``` + + - Alternative without venv (pip might complain that the packages are centrally managed by your distribution's packaging tool): + + ``` pip install -r requirements.txt + ``` 1. Make sure the FRITZ!Box has UPnP status information enabled. (web interface: _Home Network > Network > Network Settings > Universal Plug & Play (UPnP)_) @@ -87,21 +155,41 @@ Multigraph plugin, showing for 2.4GHz and 5GHz 1. (optional) If you want to connect to FRITZ!Box using SSL, download the Fritz certificate (web interface: _Internet > Freigaben > FritzBox Dienste > Zertifikat > Zertifikat herunterladen_) and save it to `/etc/munin/box.cer`. -1. Create entry in `/etc/munin/plugin-conf.d/munin-node`: +1. Create entry in `/etc/munin/plugin-conf.d/munin-node`, for example (please check further configuration options inside the plugin documentation, typically in form of comments at the top of each plugin): [fritzbox_*] - env.fritzbox_password - env.fritzbox_user - env.fritzbox_use_tls true - host_name fritzbox + + env.fritzbox_ip fritz.box + env.fritzbox_password + env.fritzbox_user + env.fritzbox_use_tls true + env.fritzbox_certificate + env.ecostat_modes cpu temp ram + env.dsl_modes capacity snr damping errors crc ecc + env.energy_modes power devices uptime + env.energy_product DSL + + env.wifi_freqs 24 5 6 + env.wifi_modes freqs neighbors + env.locale de + env.wifi_speeds_dev_info_storage_path + # env.traffic_remove_max true # if you do not want the possible max values + + host_name fritzbox + user munin + See the plugin files for plugin-specific configuration options. 1. For each plugin you want to activate, create a symbolic link to `/etc/munin/plugins`, e.g.: ``` - ln -s fritzbox_dsl.py /etc/munin/plugins/fritzbox_dsl.py + ln -s /usr/share/munin/plugins/fritzbox_dsl.py /etc/munin/plugins/ + ln -s /usr/share/munin/plugins/fritzbox_connection_uptime.sh /etc/munin/plugins/ + ... ``` + Please note that you need to take the .sh version in case of venv usage for fritzbox_traffic and fritzbox_connection_uptime, see above under "Pre-requisites" + 1. Restart the munin-node daemon: `service munin-node restart`. 1. Done. You should now start to see the charts on the Munin pages! diff --git a/doc/bandwidth_24ghz-week.png b/doc/bandwidth_24ghz-week.png new file mode 100644 index 0000000..0ed3c96 Binary files /dev/null and b/doc/bandwidth_24ghz-week.png differ diff --git a/doc/bandwidth_5ghz-week.png b/doc/bandwidth_5ghz-week.png new file mode 100644 index 0000000..8a9c7e1 Binary files /dev/null and b/doc/bandwidth_5ghz-week.png differ diff --git a/doc/battery-month.png b/doc/battery-month.png new file mode 100644 index 0000000..3d2fa39 Binary files /dev/null and b/doc/battery-month.png differ diff --git a/doc/dsl_capacity-month.png b/doc/dsl_capacity-month.png new file mode 100644 index 0000000..ef9cf57 Binary files /dev/null and b/doc/dsl_capacity-month.png differ diff --git a/doc/dsl_crc-month.png b/doc/dsl_crc-month.png new file mode 100644 index 0000000..5f4dd17 Binary files /dev/null and b/doc/dsl_crc-month.png differ diff --git a/doc/dsl_damping-month.png b/doc/dsl_damping-month.png new file mode 100644 index 0000000..2effaae Binary files /dev/null and b/doc/dsl_damping-month.png differ diff --git a/doc/dsl_ecc-month.png b/doc/dsl_ecc-month.png new file mode 100644 index 0000000..c0c7d1a Binary files /dev/null and b/doc/dsl_ecc-month.png differ diff --git a/doc/dsl_errors-month.png b/doc/dsl_errors-month.png new file mode 100644 index 0000000..ecb7e9f Binary files /dev/null and b/doc/dsl_errors-month.png differ diff --git a/doc/dsl_snr-month.png b/doc/dsl_snr-month.png new file mode 100644 index 0000000..30958d0 Binary files /dev/null and b/doc/dsl_snr-month.png differ diff --git a/doc/energy-month.png b/doc/energy-month.png new file mode 100644 index 0000000..1bbb57a Binary files /dev/null and b/doc/energy-month.png differ diff --git a/doc/humidity-day.png b/doc/humidity-day.png new file mode 100644 index 0000000..87a336c Binary files /dev/null and b/doc/humidity-day.png differ diff --git a/doc/neighbors_24ghz-week.png b/doc/neighbors_24ghz-week.png new file mode 100644 index 0000000..29e5d35 Binary files /dev/null and b/doc/neighbors_24ghz-week.png differ diff --git a/doc/neighbors_5ghz-week.png b/doc/neighbors_5ghz-week.png new file mode 100644 index 0000000..1da1416 Binary files /dev/null and b/doc/neighbors_5ghz-week.png differ diff --git a/doc/saturation_down-day.png b/doc/saturation_down-day.png new file mode 100644 index 0000000..33e7956 Binary files /dev/null and b/doc/saturation_down-day.png differ diff --git a/doc/saturation_up-day.png b/doc/saturation_up-day.png new file mode 100644 index 0000000..c897760 Binary files /dev/null and b/doc/saturation_up-day.png differ diff --git a/doc/smart_home_temperature.png b/doc/smart_home_temperature.png deleted file mode 100644 index dd70db1..0000000 Binary files a/doc/smart_home_temperature.png and /dev/null differ diff --git a/doc/smarthome_power-week.png b/doc/smarthome_power-week.png new file mode 100644 index 0000000..d9da6a9 Binary files /dev/null and b/doc/smarthome_power-week.png differ diff --git a/doc/smarthome_powerAvg-day.png b/doc/smarthome_powerAvg-day.png new file mode 100644 index 0000000..130326a Binary files /dev/null and b/doc/smarthome_powerAvg-day.png differ diff --git a/doc/smarthome_powerAvg-week.png b/doc/smarthome_powerAvg-week.png new file mode 100644 index 0000000..8332874 Binary files /dev/null and b/doc/smarthome_powerAvg-week.png differ diff --git a/doc/temperatures-day.png b/doc/temperatures-day.png new file mode 100644 index 0000000..be9b433 Binary files /dev/null and b/doc/temperatures-day.png differ diff --git a/doc/voltage-day.png b/doc/voltage-day.png new file mode 100644 index 0000000..7f407c1 Binary files /dev/null and b/doc/voltage-day.png differ diff --git a/doc/wifiDeviceSpeed_ghz24_rx-week.png b/doc/wifiDeviceSpeed_ghz24_rx-week.png new file mode 100644 index 0000000..4253793 Binary files /dev/null and b/doc/wifiDeviceSpeed_ghz24_rx-week.png differ diff --git a/doc/wifiDeviceSpeed_ghz24_tx-week.png b/doc/wifiDeviceSpeed_ghz24_tx-week.png new file mode 100644 index 0000000..0011542 Binary files /dev/null and b/doc/wifiDeviceSpeed_ghz24_tx-week.png differ diff --git a/doc/wifiDeviceSpeed_ghz5_rx-week.png b/doc/wifiDeviceSpeed_ghz5_rx-week.png new file mode 100644 index 0000000..49ae4c8 Binary files /dev/null and b/doc/wifiDeviceSpeed_ghz5_rx-week.png differ diff --git a/doc/wifiDeviceSpeed_ghz5_tx-week.png b/doc/wifiDeviceSpeed_ghz5_tx-week.png new file mode 100644 index 0000000..9b3428a Binary files /dev/null and b/doc/wifiDeviceSpeed_ghz5_tx-week.png differ diff --git a/doc/wifiDeviceSpeed_ghz6_rx-week.png b/doc/wifiDeviceSpeed_ghz6_rx-week.png new file mode 100644 index 0000000..0502635 Binary files /dev/null and b/doc/wifiDeviceSpeed_ghz6_rx-week.png differ diff --git a/doc/wifiDeviceSpeed_ghz6_tx-week.png b/doc/wifiDeviceSpeed_ghz6_tx-week.png new file mode 100644 index 0000000..d26b147 Binary files /dev/null and b/doc/wifiDeviceSpeed_ghz6_tx-week.png differ diff --git a/requirements.txt b/requirements.txt index e0fe74a..d511730 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,11 @@ fritzconnection>=1.3.0 requests -lxml \ No newline at end of file +lxml +chardet +pyfritzhome +fritzchecksum +fritzctl +fritzcollectd +fritzremote +fritzhome +fritzconnection diff --git a/src/FritzboxConfig.py b/src/FritzboxConfig.py index 95f51d6..5d3e1ce 100644 --- a/src/FritzboxConfig.py +++ b/src/FritzboxConfig.py @@ -12,15 +12,58 @@ class FritzboxConfig: useTls = True certificateFile = str(os.getenv('MUNIN_CONFDIR')) + '/box.cer' - # default constructor - def __init__(self): - if os.getenv('fritzbox_ip'): - self.server = str(os.getenv('fritzbox_ip')) - if os.getenv('fritzbox_port'): - self.port = int(os.getenv('fritzbox_port')) - self.user = str(os.getenv('fritzbox_user')) - self.password = str(os.getenv('fritzbox_password')) - if os.getenv('fritzbox_certificate'): - self.certificateFile = str(os.getenv('fritzbox_certificate')) - if os.getenv('fritzbox_use_tls'): - self.useTls = str(os.getenv('fritzbox_use_tls')) == 'true' + def __init__(self, + fritzbox_ip = None, + fritzbox_port = None, + fritzbox_user = None, + fritzbox_password = None, + fritzbox_certificate = None, + fritzbox_useTls = None, + ): + if fritzbox_ip is None: + if os.getenv('fritzbox_ip'): + self.server = str(os.getenv('fritzbox_ip')) + # end if + else: + self.server = fritzbox_ip + # end if + + if fritzbox_port is None: + if os.getenv('fritzbox_port'): + self.port = int(os.getenv('fritzbox_port')) + # end if + else: + self.port = fritzbox_port + # end if + + if fritzbox_user is None: + self.user = str(os.getenv('fritzbox_user')) + else: + self.user = fritzbox_user + # endif + + if fritzbox_password is None: + self.password = str(os.getenv('fritzbox_password')) + else: + self.password = fritzbox_password + # endif + + if fritzbox_certificate is None: + if os.getenv('fritzbox_certificate'): + self.certificateFile = str(os.getenv('fritzbox_certificate')) + # endif + else: + self.certificateFile = fritzbox_certificate + # endif + + if fritzbox_useTls is None: + if os.getenv('fritzbox_use_tls'): + self.useTls = str(os.getenv('fritzbox_use_tls')) == 'true' + # endif + else: + self.useTls = fritzbox_use_tls + # endif + + # end __init__ +# end class FritzboxConfig + diff --git a/src/FritzboxInterface.py b/src/FritzboxInterface.py index 8b5208a..dd96391 100755 --- a/src/FritzboxInterface.py +++ b/src/FritzboxInterface.py @@ -42,8 +42,15 @@ class FritzboxInterface: __baseUri = "" # default constructor - def __init__(self): - self.config = FritzboxConfig() + def __init__(self, + config = None + ): + if config is None: + self.config = FritzboxConfig() + else: + self.config = config + # endif + self.__session = FritzboxFileSession(self.config.server, self.config.user, self.config.port) self.__baseUri = self.__getBaseUri() diff --git a/src/fritzbox_connection_uptime.py b/src/fritzbox_connection_uptime.py index 84a123c..96423fe 100755 --- a/src/fritzbox_connection_uptime.py +++ b/src/fritzbox_connection_uptime.py @@ -34,7 +34,7 @@ def __init__(self): sys.exit("Couldn't get connection uptime: " + str(e)) def printUptime(self): - print('uptime.value %.2f' % (int(self.__connection.uptime) / 3600.0)) + print('connUptime.value %.2f' % (int(self.__connection.uptime) / 3600.0)) def printConfig(self): print("graph_title Connection Uptime") @@ -42,8 +42,9 @@ def printConfig(self): print("graph_vlabel uptime in hours") print("graph_scale no") print("graph_category network") - print("uptime.label uptime") - print("uptime.draw AREA") + print("connUptime.label uptime") + print("connUptime.type GAUGE") + print("connUptime.draw AREA") print("graph_info The uptime in hours after the last disconnect.
Public IP address (ipv4): " + self.__connection.external_ip + ", Public IP address (ipv6): " + self.__connection.external_ipv6) if __name__ == "__main__": diff --git a/src/fritzbox_connection_uptime.sh b/src/fritzbox_connection_uptime.sh new file mode 120000 index 0000000..2552dec --- /dev/null +++ b/src/fritzbox_connection_uptime.sh @@ -0,0 +1 @@ +start-in-venv.sh \ No newline at end of file diff --git a/src/fritzbox_dsl.py b/src/fritzbox_dsl.py index 37155de..6e79289 100755 --- a/src/fritzbox_dsl.py +++ b/src/fritzbox_dsl.py @@ -26,17 +26,17 @@ from lxml import html from FritzboxInterface import FritzboxInterface -PAGE = 'internet/dsl_stats_tab.lua' -PARAMS = {'update':'mainDiv', 'useajax':1, 'xhr':1} +PAGE = 'data.lua' +PARAMS = {'page': 'dslStat', 'lang': 'en', 'useajax': 1, 'xhrId': 'all', 'xhr': 1, 'no_sidrenew' : None} TITLES = { 'capacity': 'Link Capacity', - 'rate': 'Synced Rate', - 'snr': 'Signal-to-Noise Ratio', - 'damping': 'Line Loss', - 'errors': 'Transmission Errors', - 'crc': 'Checksum Errors', - 'ecc': 'Error Correction' + 'rate' : 'Synced Rate', + 'snr' : 'Signal-to-Noise Ratio', + 'damping' : 'Line Loss', + 'errors' : 'Errors: Transmission', + 'crc' : 'Errors: Checksums', + 'ecc' : 'Errors: Corrected' } TYPES = { 'capacity': 'GAUGE', @@ -63,8 +63,8 @@ def get_modes(): def print_graph(name, recv, send, prefix=""): if name: print("multigraph " + name) - print(prefix + "recv.value " + recv) - print(prefix + "send.value " + send) + print(prefix + f"recv.value {recv}") + print(prefix + f"send.value {send}") def print_dsl_stats(): """print the current DSL statistics""" @@ -72,66 +72,55 @@ def print_dsl_stats(): modes = get_modes() # download the table - data = FritzboxInterface().getPageWithLogin(PAGE, data=PARAMS) - root = html.fragments_fromstring(data) + data = FritzboxInterface().postPageWithLogin(PAGE, data=PARAMS) + + dslStats = data["data"]["negotiatedValues"] + errStats = data["data"]["errorCounters"] if 'capacity' in modes: - capacity_recv = root[1].xpath('tr[position() = 4]/td[position() = 3]')[0].text - capacity_send = root[1].xpath('tr[position() = 4]/td[position() = 4]')[0].text + capacity_recv = float(dslStats[2]["val"][0]["ds"]) + capacity_send = float(dslStats[2]["val"][0]["us"]) print_graph("dsl_capacity", capacity_recv, capacity_send) if 'rate' in modes: - rate_recv = root[1].xpath('tr[position() = 5]/td[position() = 3]')[0].text - rate_send = root[1].xpath('tr[position() = 5]/td[position() = 4]')[0].text + rate_recv = float(dslStats[3]["val"][0]["ds"]) + rate_send = float(dslStats[3]["val"][0]["us"]) print_graph("dsl_rate", rate_recv, rate_send) if 'snr' in modes: # Störabstandsmarge - snr_recv = root[1].xpath('tr[position() = 13]/td[position() = 3]')[0].text - snr_send = root[1].xpath('tr[position() = 13]/td[position() = 4]')[0].text + snr_recv = float(dslStats[12]["val"][0]["ds"]) + snr_send = float(dslStats[12]["val"][0]["us"]) print_graph("dsl_snr", snr_recv, snr_send) if 'damping' in modes: # Leitungsdämpfung - damping_recv = root[1].xpath('tr[position() = 15]/td[position() = 3]')[0].text - damping_send = root[1].xpath('tr[position() = 15]/td[position() = 4]')[0].text + damping_recv = float(dslStats[13]["val"][0]["ds"]) + damping_send = float(dslStats[13]["val"][0]["us"]) print_graph("dsl_damping", damping_recv, damping_send) if 'errors' in modes: - es_recv = root[4].xpath('tr[position() = 3]/td[position() = 2]')[0].text - es_send = root[4].xpath('tr[position() = 3]/td[position() = 3]')[0].text - ses_recv = root[4].xpath('tr[position() = 4]/td[position() = 2]')[0].text - ses_send = root[4].xpath('tr[position() = 4]/td[position() = 3]')[0].text - print_graph("dsl_errors", es_recv, es_send, prefix="es_") - print_graph(None, ses_recv, ses_send, prefix="ses_") + es_recv = float(errStats[1]["val"][0]["ds"]) + es_send = float(errStats[1]["val"][0]["us"]) + ses_recv = float(errStats[2]["val"][0]["ds"]) + ses_send = float(errStats[2]["val"][0]["us"]) + print_graph("dsl_errors", int(es_recv), int(es_send), prefix="es_") + print_graph(None, int(ses_recv), int(ses_send), prefix="ses_") if 'crc' in modes: - crc_recv = root[4].xpath('tr[position() = 7]/td[position() = 2]')[0].text - crc_send = root[4].xpath('tr[position() = 7]/td[position() = 3]')[0].text + crc_recv = float(errStats[6]["val"][0]["ds"]) + crc_send = float(errStats[6]["val"][0]["us"]) print_graph("dsl_crc", crc_recv, crc_send) if 'ecc' in modes: - corr_recv = root[4].xpath('tr[position() = 11]/td[position() = 2]')[0].text - corr_send = root[4].xpath('tr[position() = 11]/td[position() = 3]')[0].text - fail_recv = root[4].xpath('tr[position() = 15]/td[position() = 2]')[0].text - fail_send = root[4].xpath('tr[position() = 15]/td[position() = 3]')[0].text + corr_recv = float(errStats[10]["val"][0]["ds"]) + corr_send = float(errStats[10]["val"][0]["us"]) + fail_recv = float(errStats[14]["val"][0]["ds"]) + fail_send = float(errStats[14]["val"][0]["us"]) print_graph("dsl_ecc", corr_recv, corr_send, prefix="corr_") - print_graph(None, fail_recv, fail_send, prefix="fail_") - -def retrieve_max_values(): - max = {} - page = 'internet/inetstat_monitor.lua' - params = {'useajax':1, 'action':'get_graphic', 'xhr':1, 'myXhr':1} - data = FritzboxInterface().getPageWithLogin(page, data=params) + print_graph(None, fail_recv, fail_send, prefix="fail_") - # Retrieve max values - jsondata = json.loads(data)[0] - max['send'] = int(float(jsondata['upstream'])) - max['recv'] = int(float(jsondata['downstream'])) - - return max def print_config(): modes = get_modes() - max = retrieve_max_values() for mode in ['capacity', 'rate', 'snr', 'damping', 'crc']: if not mode in modes: @@ -148,7 +137,6 @@ def print_config(): print(p + ".min 0") if mode in ['capacity', 'rate']: print(p + ".cdef " + p + ",1000,*") - print(p + ".warning " + str(max[p])) if 'errors' in modes: print("multigraph dsl_errors") @@ -161,7 +149,7 @@ def print_config(): print(p + ".label " + l) print(p + ".type " + TYPES['errors']) print(p + ".graph LINE1") - print(p + ".min 0") + print(p + ".min -1") print(p + ".warning 1") if 'ecc' in modes: diff --git a/src/fritzbox_ecostat.py b/src/fritzbox_ecostat.py index 41ce0cd..3890c9f 100755 --- a/src/fritzbox_ecostat.py +++ b/src/fritzbox_ecostat.py @@ -22,6 +22,7 @@ import os import sys +import json from FritzboxInterface import FritzboxInterface PAGE = 'data.lua' @@ -56,6 +57,8 @@ def print_system_stats(): # download the graphs jsondata = FritzboxInterface().postPageWithLogin(PAGE, data=PARAMS)['data'] + + # print(json.dumps(jsondata, indent = 4)) if 'cpu' in modes: cpuload_data = jsondata['cpuutil'] diff --git a/src/fritzbox_energy.py b/src/fritzbox_energy.py index ce4dc0f..670eb7b 100755 --- a/src/fritzbox_energy.py +++ b/src/fritzbox_energy.py @@ -24,6 +24,7 @@ import os import re import sys +import json from FritzboxInterface import FritzboxInterface PAGE = 'data.lua' @@ -42,8 +43,8 @@ # date-from-text extractor foo locale = os.getenv('locale', 'de') -patternLoc = {"de": "(\d+)\s(Tag|Stunden|Minuten)", - "en": "(\d+)\s(days|hours|minutes)"} +patternLoc = {"de": r"(\d+)\s(Tag|Stunden|Minuten)", + "en": r"(\d+)\s(days|hours|minutes)"} dayLoc = {"de": "Tag", "en": "days"} hourLoc = {"de": "Stunden", "en": "hours"} minutesLoc = {"de": "Minuten", "en": "minutes"} @@ -69,7 +70,11 @@ def print_energy_stats(): type = get_type() # download the graphs - jsondata = FritzboxInterface().postPageWithLogin(PAGE, data=PARAMS)['data']['drain'] + jsondata = FritzboxInterface().postPageWithLogin(PAGE, data=PARAMS) + + # print(json.dumps(jsondata, indent = 4)) + + jsondata = jsondata['data']['drain'] devices = get_devices_for(type) if 'power' in modes: diff --git a/src/fritzbox_link_saturation.py b/src/fritzbox_link_saturation.py index 43b96dd..e6847a8 100755 --- a/src/fritzbox_link_saturation.py +++ b/src/fritzbox_link_saturation.py @@ -59,7 +59,7 @@ def print_link_saturation(): def print_config(): print("multigraph saturation_up") - print("graph_title Uplink saturation") + print("graph_title Saturation: Uplink") print("graph_vlabel bits out per ${graph_period}") print("graph_category network") print("graph_args --base 1000 --lower-limit 0") @@ -74,7 +74,7 @@ def print_config(): print("maxup.graph LINE1") print("multigraph saturation_down") - print("graph_title Downlink saturation") + print("graph_title Saturation: Downlink") print("graph_vlabel bits in per ${graph_period}") print("graph_category network") print("graph_args --base 1000 --lower-limit 0") diff --git a/src/fritzbox_smart_home.py b/src/fritzbox_smart_home.py new file mode 100755 index 0000000..a553089 --- /dev/null +++ b/src/fritzbox_smart_home.py @@ -0,0 +1,666 @@ +#!/usr/bin/env python3 +""" + fritzbox_smart_home_temperature - A munin plugin for Linux to monitor AVM Fritzbox SmartHome temperatures + Copyright (C) 2018 Bernd Oerding, 2021 Ernst Martin Witte + Authors: Bernd Oerding, Ernst Martin Witte + Like Munin, this plugin is licensed under the GNU GPL v2 license + http://www.opensource.org/licenses/GPL-2.0 + Add the following section to your munin-node's plugin configuration: + + [fritzbox_*] + env.fritzbox_ip [ip address of the fritzbox] + env.fritzbox_username [optionial, if you configured the FritzBox to use user and password] + env.fritzbox_password [fritzbox password] + + This plugin supports the following munin configuration parameters: + #%# family=auto contrib + #%# capabilities=autoconf +""" + +import json +import os +import re +import sys +import pprint +from FritzboxInterface import FritzboxInterface + +PAGE = 'data.lua' +PARAMS = {"xhr": 1, 'lang': 'en', 'page': 'sh', 'xhrId': 'all', 'no_sidrenew': None} + + +def getSimplifiedDevices(debug=False): + + + if debug : + pp = pprint.PrettyPrinter(indent=4) + + if debug: + print("requesting data through postPageWithLogin") + + data = FritzboxInterface().postPageWithLogin(PAGE, data = PARAMS) + + if debug: + pp.pprint(data) + + simplifiedDevices = dict() + + devices = data["data"]["devices"] + for device in devices: + devDisplayName = device["displayName"] + category = device["category"] + deviceType = device["type"] + units = device["units"] + connected = device["masterConnectionState"] + devID = device["id"] + + simpleDev = { "id" : devID, + "displayName" : devDisplayName, + "present" : connected == "CONNECTED", + "model" : device["model"], + "identifier" : device["actorIdentificationNumber"], + } + + if debug: + print(f"device {devDisplayName}: cat:{category}, type:{deviceType}, con:{connected}") + + for unit in units: + unitDisplayName = unit["displayName"] + skills = unit["skills"] + unitType = unit["type"] + unitID = unit["id"] + + if debug: + print(f" unit {unitDisplayName}: type:{unitType}") + + for skill in skills: + skillType = skill["type"] + + if debug: + print(f" skill, type:{skillType}") + + if (skillType == "SmartHomeThermostat"): + + if "mode" in skill: + simpleDev["mode" ] = skill["mode"] + + # new state reporting in Fritz!OS 7.51/7.56 + if "state" in skill: + state = skill["state"] + if "current" in state: + simpleDev["summerActive"] = 1 if state["current"] == "SUMMER" else 0 + simpleDev["holidyActive"] = 1 if state["current"] == "HOLIDAY" else 0 # FIXME: guess, to be tested + simpleDev["windowOpen" ] = 1 if state["current"] == "WINDOW_OPEN" else 0 # FIXME: guess, to be tested + # end if + # end if + + + # old state reporting in Fritz!OS 7.29/7.31 + if "summerActive" in skill: + simpleDev["summerActive" ] = skill["summerActive"] + + # old state reporting in Fritz!OS 7.29/7.31 + if "holidayActive" in skill: + simpleDev["holidayActive" ] = skill["holidayActive"] + + # old state reporting in Fritz!OS 7.29/7.31 + if "temperatureDropDetection" in skill and "isWindowOpen" in skill["temperatureDropDetection"]: + simpleDev["windowOpen" ] = skill["temperatureDropDetection"]["isWindowOpen"] + + if "targetTemp" in skill: + simpleDev["targetTemperatureInDegC"] = skill["targetTemp"] + else: + simpleDev["targetTemperatureInDegC"] = None + + if "usedTempSensor" in skill: + refSkills = skill["usedTempSensor"]["skills"] + for refSkill in refSkills: + if "currentInCelsius" in refSkill: + simpleDev["refTemperatureInDegC"] = refSkill["currentInCelsius"] + + elif (skillType == "SmartHomeTemperatureSensor"): + + # let's not blindly assume that the + # currentInCelsius is there. Seems that after a + # fritzbox restart, thermostats are not yet + # connected properly to the fritz box and that the + # report has the devices, but without temperature + # entry until DECT connection is established again. + simpleDev["currentTemperatureInDegC"] = skill["currentInCelsius"] if ("currentInCelsius" in skill) else None + + elif (skillType == "SmartHomeHumiditySensor"): + + # Why this check: See skill["currentInCelsius"] + simpleDev["currentHumidityInPct"] = skill["currentInPercent"] if ("currentInPercent" in skill) else None + + elif (skillType == "SmartHomeBattery"): + + # Why this check: See skill["currentInCelsius"] + simpleDev["batteryChargeLevelInPct"] = skill["chargeLevelInPercent"] if ("chargeLevelInPercent" in skill) else None + + # FIXME: currently no batteryLow indicator available + # simpleDev["batteryLow"] = FIXME + + elif (skillType == "SmartHomeMultimeter"): + + if "electricCurrentInAmpere" in skill: + simpleDev["currentInAmp"] = skill["electricCurrentInAmpere"] + + if "powerConsumptionInWatt" in skill: + simpleDev["powerInWatt"] = skill["powerConsumptionInWatt"] + + if "powerPerHour" in skill: + simpleDev["energyInKWH"] = float(skill["powerPerHour"]) / 1000 + + if "voltageInVolt" in skill: + simpleDev["voltageInVolt"] = skill["voltageInVolt"] + + elif (skillType == "SmartHomeSocket"): + pass + + elif (skillType == "SmartHomeSwitch"): + + if "state" in skill: + simpleDev["powerSwitchOn"] = skill["state"] == "ON" + + # end select skillType + # end for each skill + # end for each unit + + + simplifiedDevices[devID] = simpleDev + + if debug: + pp.pprint(simpleDev) + # end for each device + + if debug: + pp.pprint(simplifiedDevices) + + return simplifiedDevices + +# end getSimplifiedDevices + + + + +def print_smart_home_measurements(simpleDevices, debug=False): + """get the current measurements (temperature, humidity, power, states, ...)""" + + + print("multigraph temperatures") + + for dev in simpleDevices: + + if dev["present"] and "currentTemperatureInDegC" in dev: + print ("t{}.value {}" .format(dev["id"], dev["currentTemperatureInDegC"])) + + + print("") + print("multigraph humidity") + for dev in simpleDevices: + + if dev["present"] and "currentHumidityInPct" in dev: + print ("humidity{}.value {}".format(dev["id"], dev["currentHumidityInPct"])) + + + + + print("\n") + print("multigraph temperatures_target") + + for dev in simpleDevices: + + if dev["present"] and "targetTemperatureInDegC" in dev and dev["targetTemperatureInDegC"] is not None: + print ("tsoll{}.value {}" .format(dev["id"], dev["targetTemperatureInDegC"])) + + for dev in simpleDevices: + + if dev["present"]: + + print("") + print("multigraph temperatures.t{}".format(dev["id"])) + + if "refTemperatureInDegC" in dev: + print ("tref{}.value {}" .format(dev["id"], dev["refTemperatureInDegC"])) + + if "currentTemperatureInDegC" in dev: + print ("t{}.value {}" .format(dev["id"], dev["currentTemperatureInDegC"])) + + if "targetTemperatureInDegC" in dev and dev["targetTemperatureInDegC"] is not None: + print ("tsoll{}.value {}" .format(dev["id"], dev["targetTemperatureInDegC"])) + + + + print("\n") + print("multigraph thermostat_modes") + + for dev in simpleDevices: + + if dev["present"] and "windowOpen" in dev: + print ("windowopenmode{}.value {}" .format(dev["id"], int(dev["windowOpen"]))) + + + for dev in simpleDevices: + + if "windowOpen" in dev or "summerActive" in dev or "holidayActive" in dev: + + print("\n") + print("multigraph thermostat_modes.id{}".format(dev["id"])) + + if "windowOpen" in dev: + print ("windowopenmode{}.value {}" .format(dev["id"], int(dev["windowOpen"]))) + + if "summerActive" in dev: + print ("summermode{}.value {}" .format(dev["id"], int(dev["summerActive"]))) + + if "holidayActive" in dev: + print ("holidaymode{}.value {}" .format(dev["id"], int(dev["holidayActive"]))) + + + print("") + print("multigraph battery") + for dev in simpleDevices: + + if dev["present"] and "batteryChargeLevelInPct" in dev: + print ("battery{}.value {}".format(dev["id"], dev["batteryChargeLevelInPct"])) + + + + + print("") + print("multigraph batterylow") + for dev in simpleDevices: + + if dev["present"] and "batteryLow" in dev: + print ("batterylow{}.value {}".format(dev["id"], dev["batteryLow"])) + + + + print("") + print("multigraph voltage") + for dev in simpleDevices: + + if dev["present"] and "voltageInVolt" in dev: + print ("voltage{}.value {}".format(dev["id"], dev["voltageInVolt"])) + + + + print("") + print("multigraph smarthome_power") + for dev in simpleDevices: + + if dev["present"] and "powerInWatt" in dev: + print ("smarthome_power{}.value {}".format(dev["id"], dev["powerInWatt"])) + + + print("") + print("multigraph smarthome_powerAvg") + for dev in simpleDevices: + + if dev["present"] and "energyInKWH" in dev: + # it is ok to use integer Joules here: 1 Joule in 5 min + # => 0.003 W precision => We don't care, the device doesn't have this precision + energyInJoule = int(dev["energyInKWH"] * 3600000) + devID = dev["id"] + print (f"smarthome_powerAvg{devID}.value {energyInJoule:15}") + # end + + print("") + print("multigraph energy") + for dev in simpleDevices: + + if dev["present"] and "energyInKWH" in dev: + print ("energy{}.value {}".format(dev["id"], dev["energyInKWH"])) + + print("") + print("multigraph smarthome_powerswitch") + for dev in simpleDevices: + + if dev["present"] and "powerSwitchOn" in dev: + print ("smarthome_powerswitch{}.value {}".format(dev["id"], int(dev["powerSwitchOn"]))) + + +def getDevices(debug=False): + + simpleDevicesDict = getSimplifiedDevices(debug) + simpleDevices = sorted(simpleDevicesDict.values(), key = lambda x: x["displayName"]) + + return simpleDevices +# end getDevices + + +def print_config(simpleDevices, debug=False): + + + + print("multigraph temperatures") + print("graph_title AVM Fritz!Box SmartHome Temperatures (locally measured)") + print("graph_vlabel degrees Celsius") + print("graph_category smart home") + print("graph_scale no") + print("") + + for dev in simpleDevices: + + if "currentTemperatureInDegC" in dev: + print ("t{}.label {}" .format(dev["id"], dev["displayName"])) + print ("t{}.type GAUGE" .format(dev["id"])) + print ("t{}.graph LINE" .format(dev["id"])) + print ("t{}.info Locally measured temperature [{} - {}]" .format(dev["id"], dev["model"], dev["identifier"])) + + + print("\n") + print("multigraph humidity") + print("graph_title AVM Fritz!Box SmartHome Humidity") + print("graph_vlabel percent") + print("graph_category smart home") + print("graph_scale no") + print("\n") + + + for dev in simpleDevices: + if dev["present"] and "currentHumidityInPct" in dev: + print ("humidity{}.label {}" .format(dev["id"], dev["displayName"])) + print ("humidity{}.type GAUGE" .format(dev["id"])) + print ("humidity{}.graph LINE" .format(dev["id"])) + print ("humidity{}.max 100" .format(dev["id"])) + print ("humidity{}.min 0" .format(dev["id"])) + print ("humidity{}.warning 30:70" .format(dev["id"])) + print ("humidity{}.critical 20:75" .format(dev["id"])) + print ("humidity{}.info Humidity [{} - {}]" .format(dev["id"], dev["model"], dev["identifier"])) + + + + + print("\n") + print("multigraph temperatures_target") + print("graph_title AVM Fritz!Box SmartHome Target Temperatures") + print("graph_vlabel degrees Celsius") + print("graph_category smart home") + print("graph_scale no") + print("") + + for dev in simpleDevices: + + if "targetTemperatureInDegC" in dev: + print ("tsoll{}.label {}" .format(dev["id"], dev["displayName"])) + print ("tsoll{}.type GAUGE" .format(dev["id"])) + print ("tsoll{}.graph LINE" .format(dev["id"])) + print ("tsoll{}.info Target temperature [{} - {}]" .format(dev["id"], dev["model"], dev["identifier"])) + + + for dev in simpleDevices: + print("\n") + print("multigraph temperatures.t{}".format(dev["id"])) + print("graph_title Temperatures for {}".format(dev["displayName"])) + print("graph_vlabel degrees Celsius") + print("graph_category smart home") + print("graph_scale no") + print("\n") + + if "currentTemperatureInDegC" in dev: + print ("t{}.label measured locally" .format(dev["id"])) + print ("t{}.type GAUGE" .format(dev["id"])) + print ("t{}.graph LINE" .format(dev["id"])) + print ("t{}.warning 15:30" .format(dev["id"])) + print ("t{}.critical 10:35" .format(dev["id"])) + print ("t{}.info Locally measured temperature [{} - {}]" .format(dev["id"], dev["model"], dev["identifier"])) + + if "refTemperatureInDegC" in dev: + print ("tref{}.label measured ref." .format(dev["id"])) + print ("tref{}.type GAUGE" .format(dev["id"])) + print ("tref{}.graph LINE" .format(dev["id"])) + print ("tref{}.info Measured reference temperature [{} - {}]".format(dev["id"], dev["model"], dev["identifier"])) + + if "targetTemperatureInDegC" in dev: + print ("tsoll{}.label target" .format(dev["id"])) + print ("tsoll{}.type GAUGE" .format(dev["id"])) + print ("tsoll{}.graph LINE" .format(dev["id"])) + print ("tsoll{}.info Target temperature [{} - {}]" .format(dev["id"], dev["model"], dev["identifier"])) + + + print("\n") + print("multigraph thermostat_modes") + print("graph_title AVM Fritz!Box SmartHome Thermostat Modes") + print("graph_vlabel on/off") + print("graph_category smart home") + print("graph_scale no") + print("\n") + + for dev in simpleDevices: + + if dev["present"] and "windowOpen" in dev: + print ("windowopenmode{}.label {}" .format(dev["id"], dev["displayName"])) + print ("windowopenmode{}.type GAUGE" .format(dev["id"])) + print ("windowopenmode{}.graph LINE" .format(dev["id"])) + print ("windowopenmode{}.max 1" .format(dev["id"])) + print ("windowopenmode{}.min 0" .format(dev["id"])) + print ("windowopenmode{}.info Window Open Mode [{} - {}]" .format(dev["id"], dev["model"], dev["identifier"])) + + + for dev in simpleDevices: + + if "windowOpen" in dev or "summerActive" in dev or "holidayActive" in dev: + + print("\n") + print("multigraph thermostat_modes.id{}".format(dev["id"])) + print("graph_title AVM Fritz!Box SmartHome Modes for {}".format(dev["displayName"])) + print("graph_vlabel on/off") + print("graph_category smart home") + print("graph_scale no") + print("\n") + + if "windowOpen" in dev: + print ("windowopenmode{}.label Window Open" .format(dev["id"])) + print ("windowopenmode{}.type GAUGE" .format(dev["id"])) + print ("windowopenmode{}.graph LINE" .format(dev["id"])) + print ("windowopenmode{}.max 1" .format(dev["id"])) + print ("windowopenmode{}.min 0" .format(dev["id"])) + print ("windowopenmode{}.info Window Open Mode [{} - {}]" .format(dev["id"], dev["model"], dev["identifier"])) + + if "summerActive" in dev: + print ("summermode{}.label Summer Mode" .format(dev["id"])) + print ("summermode{}.type GAUGE" .format(dev["id"])) + print ("summermode{}.graph LINE" .format(dev["id"])) + print ("summermode{}.max 1" .format(dev["id"])) + print ("summermode{}.min 0" .format(dev["id"])) + print ("summermode{}.info Summer Mode [{} - {}]" .format(dev["id"], dev["model"], dev["identifier"])) + + if "holidayActive" in dev: + print ("holidaymode{}.label Holiday Mode" .format(dev["id"])) + print ("holidaymode{}.type GAUGE" .format(dev["id"])) + print ("holidaymode{}.graph LINE" .format(dev["id"])) + print ("holidaymode{}.max 1" .format(dev["id"])) + print ("holidaymode{}.min 0" .format(dev["id"])) + print ("holidaymode{}.info Holiday Mode [{} - {}]" .format(dev["id"], dev["model"], dev["identifier"])) + + + + + print("\n") + print("multigraph battery") + print("graph_title AVM Fritz!Box SmartHome Battery") + print("graph_vlabel percent") + print("graph_category smart home") + print("graph_scale no") + print("\n") + + + for dev in simpleDevices: + if dev["present"] and "batteryChargeLevelInPct" in dev: + print ("battery{}.label {}" .format(dev["id"], dev["displayName"])) + print ("battery{}.type GAUGE" .format(dev["id"])) + print ("battery{}.graph LINE" .format(dev["id"])) + print ("battery{}.max 100" .format(dev["id"])) + print ("battery{}.min 0" .format(dev["id"])) + print ("battery{}.warning 30:110" .format(dev["id"])) + print ("battery{}.critical 10:120" .format(dev["id"])) + print ("battery{}.info Battery [{} - {}]" .format(dev["id"], dev["model"], dev["identifier"])) + + + + print("\n") + print("multigraph batterylow") + print("graph_title AVM Fritz!Box SmartHome Battery Low Warning") + print("graph_vlabel ok/warning") + print("graph_category smart home") + print("graph_scale no") + print("\n") + + for dev in simpleDevices: + if dev["present"] and "batteryLow" in dev: + print ("batterylow{}.label {}" .format(dev["id"], dev["displayName"])) + print ("batterylow{}.type GAUGE" .format(dev["id"])) + print ("batterylow{}.graph LINE" .format(dev["id"])) + print ("batterylow{}.max 1" .format(dev["id"])) + print ("batterylow{}.min 0" .format(dev["id"])) + print ("batterylow{}.warning 0.5" .format(dev["id"])) + print ("batterylow{}.critical 1" .format(dev["id"])) + print ("batterylow{}.info Battery Low Warning [{} - {}]" .format(dev["id"], dev["model"], dev["identifier"])) + + + print("\n") + print("multigraph voltage") + print("graph_title AVM Fritz!Box SmartHome Voltage") + print("graph_vlabel Volt") + print("graph_category smart home") + print("graph_scale no") + print("\n") + + for dev in simpleDevices: + if dev["present"] and "voltageInVolt" in dev: + print ("voltage{}.label {}" .format(dev["id"], dev["displayName"])) + print ("voltage{}.type GAUGE" .format(dev["id"])) + print ("voltage{}.graph LINE" .format(dev["id"])) + print ("voltage{}.min 0" .format(dev["id"])) + print ("voltage{}.warning 220:240" .format(dev["id"])) + print ("voltage{}.critical 210:245" .format(dev["id"])) + print ("voltage{}.info Voltage [{} - {}]" .format(dev["id"], dev["model"], dev["identifier"])) + + + + print("\n") + print("multigraph smarthome_power") + print("graph_title AVM Fritz!Box SmartHome Power") + print("graph_vlabel Watt") + print("graph_category smart home") + print("graph_scale no") + + powerIdList = [f'smarthome_power{dev["id"]}' for dev in simpleDevices if ( dev["present"] and "powerInWatt" in dev ) ] + print("graph_order total_smarthome_power " + " ".join(powerIdList)) + + print("\n") + + for dev in simpleDevices: + if dev["present"] and "powerInWatt" in dev: + print ("smarthome_power{}.label {}" .format(dev["id"], dev["displayName"])) + print ("smarthome_power{}.type GAUGE" .format(dev["id"])) + print ("smarthome_power{}.graph LINE" .format(dev["id"])) + print ("smarthome_power{}.min 0" .format(dev["id"])) + print ("smarthome_power{}.warning 1500" .format(dev["id"])) + print ("smarthome_power{}.critical 2000" .format(dev["id"])) + print ("smarthome_power{}.info Power [{} - {}]" .format(dev["id"], dev["model"], dev["identifier"])) + # end if + # end if + + + print(f"total_smarthome_power.label total") + print(f"total_smarthome_power.cdef " + ",".join(powerIdList) + ",ADDNAN" * (len(powerIdList) - 1)); + print(f"total_smarthome_power.min 0") + + + + print("\n") + print("multigraph smarthome_powerAvg") + print("graph_title AVM Fritz!Box SmartHome Power Average (from Energy)") + print("graph_vlabel W") + print("graph_category smart home") + print("graph_scale no") + + powerAvgIdList = [f'smarthome_powerAvg{dev["id"]}' for dev in simpleDevices if ( dev["present"] and "energyInKWH" in dev ) ] + print("graph_order total_smarthome_powerAvg " + " ".join(powerAvgIdList)) + + print("\n") + + for dev in simpleDevices: + if dev["present"] and "energyInKWH" in dev: + print ("smarthome_powerAvg{}.label {}" .format(dev["id"], dev["displayName"])) + print ("smarthome_powerAvg{}.type DERIVE" .format(dev["id"])) + print ("smarthome_powerAvg{}.graph LINE" .format(dev["id"])) + print ("smarthome_powerAvg{}.min 0" .format(dev["id"])) + print ("smarthome_powerAvg{}.info Power Average [{} - {}]" .format(dev["id"], dev["model"], dev["identifier"])) + + print(f"total_smarthome_powerAvg.label total") + print(f"total_smarthome_powerAvg.cdef " + ",".join(powerAvgIdList) + ",ADDNAN" * (len(powerAvgIdList) - 1)); + print(f"total_smarthome_powerAvg.min 0") + + + print("\n") + print("multigraph energy") + print("graph_title AVM Fritz!Box SmartHome Energy") + print("graph_vlabel kWh") + print("graph_category smart home") + print("graph_scale no") + + energyIdList = [f'energy{dev["id"]}' for dev in simpleDevices if ( dev["present"] and "energyInKWH" in dev ) ] + print("graph_order total_energy " + " ".join(energyIdList)) + + print("\n") + + for dev in simpleDevices: + if dev["present"] and "energyInKWH" in dev: + print ("energy{}.label {}" .format(dev["id"], dev["displayName"])) + print ("energy{}.type GAUGE" .format(dev["id"])) + print ("energy{}.graph LINE" .format(dev["id"])) + print ("energy{}.min 0" .format(dev["id"])) + print ("energy{}.info Energy [{} - {}]" .format(dev["id"], dev["model"], dev["identifier"])) + + print(f"total_energy.label total") + print(f"total_energy.cdef " + ",".join(energyIdList) + ",ADDNAN" * (len(energyIdList) - 1)); + print(f"total_energy.min 0") + + + + print("\n") + print("multigraph smarthome_powerswitch") + print("graph_title AVM Fritz!Box SmartHome Power Switch") + print("graph_vlabel On/Off") + print("graph_category smart home") + print("graph_scale no") + print("\n") + + for dev in simpleDevices: + if dev["present"] and "powerSwitchOn" in dev: + print ("smarthome_powerswitch{}.label {}" .format(dev["id"], dev["displayName"])) + print ("smarthome_powerswitch{}.type GAUGE" .format(dev["id"])) + print ("smarthome_powerswitch{}.graph LINE" .format(dev["id"])) + print ("smarthome_powerswitch{}.min 0" .format(dev["id"])) + print ("smarthome_powerswitch{}.max 1" .format(dev["id"])) + print ("smarthome_powerswitch{}.info On/Off [{} - {}]" .format(dev["id"], dev["model"], dev["identifier"])) + + + if os.environ.get('host_name'): + print("host_name " + os.environ['host_name']) + + +if __name__ == '__main__': + if len(sys.argv) == 2 and sys.argv[1] == 'config': + devices = getDevices(debug = False) + print_config(devices) + if "MUNIN_CAP_DIRTYCONFIG" in os.environ and os.environ["MUNIN_CAP_DIRTYCONFIG"] == "1": + print("") + print_smart_home_measurements(devices, debug = False) + # end if DIRTY CONFIG + elif len(sys.argv) == 2 and sys.argv[1] == 'autoconf': + print('yes') + elif len(sys.argv) == 2 and sys.argv[1] == 'debug': + devices = getDevices(debug = True) + print_smart_home_measurements(devices, debug = True) + elif len(sys.argv) == 1 or len(sys.argv) == 2 and sys.argv[1] == 'fetch': + # Some docs say it'll be called with fetch, some say no arg at all + try: + devices = getDevices(debug = False) + print_smart_home_measurements(devices, debug = False) + except: + sys.exit("Couldn't retrieve fritzbox smarthome data") + diff --git a/src/fritzbox_smart_home_temperature.sh b/src/fritzbox_smart_home_temperature.sh new file mode 120000 index 0000000..2552dec --- /dev/null +++ b/src/fritzbox_smart_home_temperature.sh @@ -0,0 +1 @@ +start-in-venv.sh \ No newline at end of file diff --git a/src/fritzbox_traffic.py b/src/fritzbox_traffic.py index 42144c6..c6db618 100755 --- a/src/fritzbox_traffic.py +++ b/src/fritzbox_traffic.py @@ -35,13 +35,13 @@ def __init__(self): def printTraffic(self): transmission_rate = self.__connection.transmission_rate - print('down.value %d' % transmission_rate[1]) - print('up.value %d' % transmission_rate[0]) + print(f'down.value {transmission_rate[1]:d}') + print(f'up.value {transmission_rate[0]:d}') if not os.environ.get('traffic_remove_max') or "false" in os.environ.get('traffic_remove_max'): max_traffic = self.__connection.max_bit_rate - print('maxdown.value %d' % max_traffic[1]) - print('maxup.value %d' % max_traffic[0]) + print(f'maxdown.value {max_traffic[1]:d}') + print(f'maxup.value {max_traffic[0]:d}') def printConfig(self): max_traffic = self.__connection.max_bit_rate @@ -56,13 +56,13 @@ def printConfig(self): print("down.graph no") print("down.cdef down,8,*") print("down.min 0") - print(f"down.max %d{max_traffic[1]}") + print(f"down.max {max_traffic[1]:d}") print("up.label bps") print("up.type DERIVE") print("up.draw LINE") print("up.cdef up,8,*") print("up.min 0") - print(f"up.max %d{max_traffic[0]}") + print(f"up.max {max_traffic[0]:d}") print("up.negative down") print("up.info Traffic of the WAN interface.") if not os.environ.get('traffic_remove_max') or "false" in os.environ.get('traffic_remove_max'): diff --git a/src/fritzbox_traffic.sh b/src/fritzbox_traffic.sh new file mode 120000 index 0000000..2552dec --- /dev/null +++ b/src/fritzbox_traffic.sh @@ -0,0 +1 @@ +start-in-venv.sh \ No newline at end of file diff --git a/src/fritzbox_wifi_load.py b/src/fritzbox_wifi_load.py index 380f9a1..e2f9882 100755 --- a/src/fritzbox_wifi_load.py +++ b/src/fritzbox_wifi_load.py @@ -23,6 +23,7 @@ import os import sys +import pprint from FritzboxInterface import FritzboxInterface PAGE = 'data.lua' @@ -47,7 +48,7 @@ def get_freqs(): def get_modes(): return os.getenv('wifi_modes').split(' ') -def print_wifi_load(): +def print_wifi_load(debug = False): """get the current wifi bandwidth usage""" # set up the graphs (load the 10-minute view) @@ -55,6 +56,11 @@ def print_wifi_load(): # download the graphs jsondata = fritzboxHelper.postPageWithLogin(PAGE, data=PARAMS)['data'] + if debug: + pp = pprint.PrettyPrinter(indent=4) + pp.pprint(jsondata) + # end if + freqs = get_freqs() modes = get_modes() scanlist = jsondata['scanlist'] @@ -120,12 +126,18 @@ def print_config(): print(multiP + '.draw AREASTACK') if __name__ == "__main__": - if len(sys.argv) == 2 and sys.argv[1] == 'config': + if (len(sys.argv) == 1): + request = "fetch" + else: + request = sys.argv[1] + # end if + + if (request == "config"): print_config() - elif len(sys.argv) == 2 and sys.argv[1] == 'autoconf': + elif (request == 'autoconf'): print("yes") # Some docs say it'll be called with fetch, some say no arg at all - elif len(sys.argv) == 1 or (len(sys.argv) == 2 and sys.argv[1] == 'fetch'): + elif (request == 'fetch' or request == 'debug'): try: - print_wifi_load() + print_wifi_load(request == 'debug') except Exception as e: sys.exit("Couldn't retrieve fritzbox wifi load: " + str(e)) diff --git a/src/fritzbox_wifi_speeds.py b/src/fritzbox_wifi_speeds.py new file mode 100755 index 0000000..3a52b6a --- /dev/null +++ b/src/fritzbox_wifi_speeds.py @@ -0,0 +1,619 @@ +#!/usr/bin/env python3 +""" + fritzbox_wifi_speed - A munin plugin for Linux to monitor Wifi Speeds of a AVM Fritzbox Mesh + Copyright (C) 2023 Ernst Martin Witte + Author: Ernst Martin Witte + Like Munin, this plugin is licensed under the GNU GPL v2 license + http://www.opensource.org/licenses/GPL-2.0 + + Add the following section to your munin-node's plugin configuration: + + [fritzbox_*] + env.fritzbox_ip [ip address of the fritzbox] + env.fritzbox_password [fritzbox password] + env.fritzbox_user [fritzbox user, set any value if not required] + # Path where to store the persistent wifi device information (bands seen). + + # VARIABLE env.wifi_speeds_dev_info_storage_path + # + # NOTE: We need the information, which device was connected in the past to which wifi band + # (2.4 GHz or 5 GHz... or Ethernet), particularly for those devices which are currently not connected. + # Otherwise, disconnected devices will vanish from the munin plots. + # + # However, if a device is currently not connected, the FritzBox does not tell us in which + # band the box was connected to so far. ... well not exactly: The FritzBox tells us this + # information for its "own" the wifi connections, but not for those of the repeaters. + # + # You may ask why then not directly asking the repeaters for the wifi connection + # information of "currenty not connected devices". + # Unfortunately, on my setup (Fritz!Repeater 1750E and Fritz!Repeater 3000), + # I'm not able to login with anything else than the master password, not with my munin stats key. + # + # Alternatively, we could create plot entries/curves for each combination of MAC addresses and bands. + # But this would cause pollution of the plots, e.g. ethernet-only devices in the wifi + # plots, 2.4GHz-Interface MACs (typically MACs on 2.4 GHz and 5 GHz are different) listed + # in 5 GHz plots and so on. + # + # Q: Why not simply using a single aggregated data rate per device? + # A: In my stats I'd like to ... + # - identify, when 2.4 GHz or 5 GHz was used + # - to see separate stats for 2.4 GHz and 5 GHz bands for the repeaters + # + # Q: Hmm... why are you not using the "UID"s (e.g. "landevice1234") or names instead of MACs + # for device identification? + # A: Because I observed that the uid changes over time for the same MAC/wifi device, particularly + # in the guest network and when switching the wifi connection between repeaters/boxes. + # ==> Therefore, I consider device names and UUIDs as not suitable for device identification. + # + # Q: Ok... but for a Fritz!Repeater the FritzBox does only show one MAC in the "netDev" page + # (used here in this script) and two other MACs in the "wSet" page. ... and in the ARP cache + # the repeater shows up with yet another MAC. + # A: Seems that the netDev page shows the single globally/"universally" administered MAC of the + # repeater. The Ethernet/2.4 GHz/5GHz MACs are "generated, locally administered" MAC addresses, + # derived from the universal/global MAC. + # Therefore, I believe, the MAC is also suitable for identification of devices with multiple + # concurrently used wifi bands. + # + # Default: $MUNIN_PLUGSTATE/fritzbox_wifi_speed_device_info.json + # + # NOTE for SSDs: + # In case you made the "normal" munin plugin state directory to reside on a tmpfs in order + # to not write your SSD to death by munin (fine for most plugins), you should put this + # plugin state to some persistent storage. This is recommended since the wifi device information + # might change very slowly such that it takes ages after the munin server reboot until + # all devices have been seen connected. + # + # This plugin ensures that your SSD is not written to death: It only writes the file if + # the list of known devices and their (rarely changing) connection types really changed. + # The frequently changing "current wifi speeds" are not stored in this state file! + # + env.wifi_speeds_dev_info_storage_path /path/to/json-file-with-persistent-device-info.json + + This plugin supports the following munin configuration parameters: + #%# family=auto contrib + #%# capabilities=autoconf dirtyconfig +""" + +import os +import re +import sys +import pprint +from FritzboxInterface import FritzboxInterface +from FritzboxConfig import FritzboxConfig +import argparse +import warnings +import requests +import copy +import json + + +key_ghz24 = "ghz24" +key_ghz5 = "ghz5" +key_ghz6 = "ghz6" +key_eth = "eth" + + + +def makeKnownBandDescriptor(id, descr, is_symmetric) -> dict: + return { + "id": id, + "descr": descr, + "is_symmetric": is_symmetric + } +# end makeKnownBandDescriptor + + + +knownBands = { key_ghz24: makeKnownBandDescriptor(id = key_ghz24, + descr = "Wifi 2.4 GHz", + is_symmetric = 0), + key_ghz5: makeKnownBandDescriptor(id = key_ghz5, + descr = "Wifi 5 GHz", + is_symmetric = 0), + key_ghz6: makeKnownBandDescriptor(id = key_ghz6, + descr = "Wifi 6 GHz", + is_symmetric = 0), + key_eth: makeKnownBandDescriptor(id = key_eth, + descr = "Ethernet", + is_symmetric = 1), + } + + + + +def getConcurrentBandsKey(bandKeyList): + return "-".join(sorted(bandKeyList)) +# end getConcurrentBandsKey + + +def createPersistentDeviceInfoStruct(name, mac, bandKeyList): + + info = { + "name": name, + "mac": mac, + "bandsSeen": {}, + "concurrentBandsSeen": { getConcurrentBandsKey(bandKeyList): sorted(bandKeyList) } + } + + for key in knownBands.keys(): + info["bandsSeen"][key] = (key in bandKeyList) + # end for each band + + return info +# end createPersistentDeviceInfoStruct + + + +def updatePersistentDeviceInfoStruct(name, + mac, + bandKeyList, + currentPersistentDeviceInfo, + storedPersistentDeviceInfo + ): + + + if mac not in currentPersistentDeviceInfo: + if (mac in storedPersistentDeviceInfo): + currentPersistentDeviceInfo[mac] = copy.deepcopy(storedPersistentDeviceInfo[mac]) + else: + # do not create new entries for not-yet-connected devices! + if (len(bandKeyList) <= 0): + return + # end if + currentPersistentDeviceInfo[mac] = createPersistentDeviceInfoStruct(name, mac, bandKeyList) + # end if + # end if + + + for bandKey in bandKeyList: + currentPersistentDeviceInfo[mac]["bandsSeen"][bandKey] = 1 + # end for each bandKey + + # only record "concurrent bands seen" if the list is not empty + if (len(bandKeyList) <= 0): + currentPersistentDeviceInfo[mac]["concurrentBandsSeen"][getConcurrentBandsKey(bandKeyList)] = sorted(bandKeyList) + + # update the name if needed + currentPersistentDeviceInfo[mac]["name"] = name + +# end updatePersistentDeviceInfoStruct + + + + +def getPersisentDeviceInfoPath() -> str: + munin_config_setting_path = os.getenv('wifi_speeds_dev_info_storage_path') + munin_pluginstate_path = os.getenv('MUNIN_PLUGSTATE') + '/fritzbox_wifi_speed_device_info.json' + if (munin_config_setting_path is None or munin_config_setting_path == ""): + return munin_pluginstate_path + else: + return munin_config_setting_path +# end getPersisentDeviceInfoPath + + +def loadPersistentDeviceInfo(debug = False) -> dict: + + fname = getPersisentDeviceInfoPath() + + if (debug): + pp = pprint.PrettyPrinter(indent=4) + print(f"Loading persistent device info from: {fname}") + # end if debug + + if (os.path.isfile(fname)): + fh = open(fname, 'r') + data = json.load(fh) + fh.close() + else: + data = {} + # end if + + if (debug): + pp.pprint({"storedInfo": data, + "file": fname + }) + # end if debug + + return data + +# end loadPersistentDeviceInfo + + +def storePersistentDeviceInfo(currentInfo, + storedInfo, + debug = False): + fname = getPersisentDeviceInfoPath() + needsUpdate = currentInfo != storedInfo + + if (debug): + pp = pprint.PrettyPrinter(indent=4) + print("Storing persistent device info:") + pp.pprint({"currentInfo": currentInfo, + "storedInfo": storedInfo, + "needsUpdateOnDisk": needsUpdate, + "file": fname + }) + # end if debug + + if (needsUpdate): + fh = open(fname, 'w') + json.dump(currentInfo, fh) + fh.close() + # end if + +# end storePersistentDeviceInfo + + + + +def getWifiSpeeds(oneFritzBoxInterface, + debug = False): + + devicesByBands = {} + for key in knownBands.keys(): + devicesByBands[key] = [] + # end for each known band + + + storedPersistentDeviceInfo = loadPersistentDeviceInfo(debug = debug) + currentPersistentDeviceInfo = {} + + if debug : + pp = pprint.PrettyPrinter(indent=4) + pp.pprint({"FritzboxInterface.config": vars(oneFritzBoxInterface.config)}) + pp.pprint({ "storedPersistentDeviceInfo": storedPersistentDeviceInfo}) + # end if + + + + if debug: + print(f"requesting data through postPageWithLogin from {oneFritzBoxInterface.config.server}") + # end if + + + # download the graphs + PARAMS = { + 'xhr': 1, + 'lang': 'de', + 'page': 'netDev', + 'xhrId': 'all', + 'useajax': 1, + 'no_sidrenew': None + } + jsondata = oneFritzBoxInterface.postPageWithLogin('data.lua', + data = PARAMS) + + if debug: + pp.pprint(jsondata) + # end if + + + active_devices = jsondata["data"]["active"] + passive_devices = jsondata["data"]["passive"] + all_devices = active_devices + passive_devices + + # example: "LAN 1 mit 1 Gbit/s " + ethSpeedRegEx = re.compile(r'\b(\d+([\.,]\d+)?)\s*([GM])bit(/s)?', re.IGNORECASE) + + # example: "2,4 GHz, 144 / 1 Mbit/s" + wifiSpeedRegEx = re.compile(r'\b(\d+([\.,]\d+)?)\s*GHz,?\s*(\d+([\.,]\d+)?)\s*/\s*(\d+([\.,]\d+)?)\s*([GMk])bit(/s)?', re.IGNORECASE) + + wifi24GHzRegex = re.compile(r'\b2[,\.]4\s*GHz\b', re.IGNORECASE) + wifi5GHzRegex = re.compile(r'\b5\s*GHz\b', re.IGNORECASE) + wifi6GHzRegex = re.compile(r'\b6\s*GHz\b', re.IGNORECASE) + + for dev in all_devices: + + if debug: + pp.pprint({ "device_under_investigation": dev}) + # end if + + + devName = dev["name"] + connType = dev["type"] + mac = dev["mac"] + props = dev["properties"] + port = dev["port"] + + # NOTE: We store the uid (something like "landevice1234") for completeness. + # But we do not use it for identification, because the uid changes over time for the same MAC/wifi device. + # ... at least when switching the wifi connection between repeaters/boxes, + # but also looks like on a single box/repeater the uid is not stable! + devUID = dev["UID"] + + currentSpeeds = {} + if mac in storedPersistentDeviceInfo: + bandsSeenInThePast = storedPersistentDeviceInfo[mac]["bandsSeen"] + + for band in [key for key in bandsSeenInThePast.keys() if bandsSeenInThePast[key]]: + currentSpeeds[band] = { "ds": 0, + "us": 0 + } + # end for each band seen in the past + # end if + + if (debug): + pp.pprint({"currentSpeeds before update": currentSpeeds }) + # end if debug + + concurrentBands = [] + + if (connType == "ethernet"): + + match = ethSpeedRegEx.search(port) + value = 0.0 + scale = 1.0 + if (match): + value = float(match.group(1).replace(",", ".")) + unit = match.group(3) + scale = 1 if unit == "M" else 1000 + # end if + currentSpeeds[key_eth] = { "ds": value * scale, + "us": value * scale + } + concurrentBands.append(key_eth) + + elif (connType == "wlan"): + currentBands = [] + for prop in props: + propString = prop["txt"] + match = wifiSpeedRegEx.search(propString) + if (match): + downstream = match.group(3) + upstream = match.group(5) + unit = match.group(7) + scale = 1 if unit == "M" else (1000 if unit == "G" else 1.0/1000.0) + + if (wifi24GHzRegex.search(propString)): + band_key = key_ghz24 + elif (wifi5GHzRegex.search(propString)): + band_key = key_ghz5 + elif (wifi6GHzRegex.search(propString)): + band_key = key_ghz6 + else: + band_key = None + # end if band key selection + + currentSpeeds[band_key] = { "ds": downstream * scale, + "us": upstream * scale } + + concurrentBands.append(band_key) + + # end if propString matches wifi band info + # end for each property + elif (connType == "unknown"): + # do nothing: + # - known speeds from the past are kept at 0.0 MBit/s + # - no connected band is recorded + # - if not yet seen in the past: do not create a new entry in the "seen in the past" table + pass + # end if + + + if (debug): + pp.pprint({"currentSpeeds after update": currentSpeeds, + "concurrentBands after update": concurrentBands + }) + # end if debug + + + # update the persistent table of known device/bands + # (only updated for already known devices and if we have currently a connection) + updatePersistentDeviceInfoStruct(devName, + mac, + concurrentBands, + currentPersistentDeviceInfo, + storedPersistentDeviceInfo) + + mac4dsName = re.sub(r'[^\w]', '', mac).lower() + + for bandKey in currentSpeeds.keys(): + deviceEntry = { "name": devName, + "uid": devUID, + "mac": mac, + "rxSpeed_inMBitPerSec": currentSpeeds[bandKey]["ds"], + "txSpeed_inMBitPerSec": currentSpeeds[bandKey]["us"], + "ds_name": f"dev_{mac4dsName}" + } + + devicesByBands[bandKey].append(deviceEntry) + # end for each band + + if debug: + pp.pprint({ "currentPersistentDeviceInfo after update": currentPersistentDeviceInfo}) + # end if + + # end for each device + + if debug: + pp.pprint({ "devicesByBands": devicesByBands } ) + # end if + + + storePersistentDeviceInfo(currentPersistentDeviceInfo, + storedPersistentDeviceInfo, + debug = debug) + + + return devicesByBands + +# end getWifiSpeeds + + + + +def getGraphName(bandKey): + bandID = knownBands[bandKey]["id"] + return f"wifiDeviceSpeed_{bandID}" +# end getGraphName + + +def getRxTxConfigParams(bandKey): + isSymmetric = knownBands[bandKey]["is_symmetric"] + + return { + "rx_suffix" : "" if isSymmetric else '_rx', + "rx_prefix" : "" if isSymmetric else 'RX ', + "tx_suffix" : "" if isSymmetric else '_tx', + "tx_prefix" : "" if isSymmetric else 'TX ', + "show_rx" : 1, + "show_tx" : 0 if isSymmetric else 1, + } + +# end getRxTxConfigParams + + +def printConfig(devicesByBands, debug = False): + + for bandKey,devices in devicesByBands.items(): + bandDescr = knownBands[bandKey]["descr"] + graphName = getGraphName(bandKey) + sortedDevices = sorted(devices, key = lambda x: x["name"]) + dsNames = [x["ds_name"] for x in sortedDevices]; + + rxtxCfg = getRxTxConfigParams(bandKey) + + if (rxtxCfg['show_rx']): + print(f"multigraph {graphName}{rxtxCfg['rx_suffix']}") + print(f"graph_title Device Speeds ({rxtxCfg['rx_prefix']}{bandDescr})") + print("graph_vlabel Bit/s") + print("graph_args --base 1000") + # print("graph_args --logarithmic") + print("graph_category network") + + print("graph_order " + " ".join(dsNames)) + for dev in sortedDevices: + ds = dev["ds_name"] + label = dev['name'] + + print(f"{ds}.label {label}") + print(f"{ds}.type GAUGE") + print(f"{ds}.min 0") + print(f"{ds}.cdef {ds},1000000,*") + # end for each device + # end if show_rx + + + if (rxtxCfg['show_tx']): + print(f"multigraph {graphName}{rxtxCfg['tx_suffix']}") + print(f"graph_title Device Speeds ({rxtxCfg['tx_prefix']}{bandDescr})") + print("graph_vlabel Bit/s") + print("graph_args --base 1000") + # print("graph_args --logarithmic") + print("graph_category network") + + print("graph_order " + " ".join(dsNames)) + for dev in sortedDevices: + ds = dev["ds_name"] + label = dev['name'] + + print(f"{ds}.label {label}") + print(f"{ds}.type GAUGE") + print(f"{ds}.min 0") + print(f"{ds}.cdef {ds},1000000,*") + # end for each device + # end if show_tx + + # end for each band + +# end printConfig + + + +def printValues(devicesByBands, debug = False): + + if debug : + pp = pprint.PrettyPrinter(indent=4) + pp.pprint({"printValues with devicesByBands": devicesByBands}) + # end if + + for bandKey,devices in devicesByBands.items(): + + if debug : + pp.pprint({"printing band": { "bandKey": bandKey, "devices": devices}}) + # end if + + bandDescr = knownBands[bandKey]["descr"] + graphName = getGraphName(bandKey) + sortedDevices = sorted(devices, key = lambda x: x["name"]) + dsNames = [x["ds_name"] for x in sortedDevices]; + + rxtxCfg = getRxTxConfigParams(bandKey) + + if (rxtxCfg['show_rx']): + print(f"multigraph {graphName}{rxtxCfg['rx_suffix']}") + + for dev in sortedDevices: + ds = dev["ds_name"] + print(f"{ds}.value {dev['rxSpeed_inMBitPerSec']}") + # end for each device + # end if show_rx + + if (rxtxCfg['show_tx']): + print(f"multigraph {graphName}{rxtxCfg['tx_suffix']}") + + for dev in sortedDevices: + ds = dev["ds_name"] + print(f"{ds}.value {dev['txSpeed_inMBitPerSec']}") + # end for each device + # end if show_tx + + # end for each band + +#end printValues + + + +def main(): + + parser = argparse.ArgumentParser(description='Munin Statistics for Fritz!Box Wifi Device Speeds') + + parser.add_argument('--debug', '-d', action = 'store_true', + help = "enable debug output") + + parser.add_argument('requests', nargs = '*'); + + args = parser.parse_args() + + + requests = list(args.requests) + if (len(requests) == 0): + requests.append("fetch") + # end if + + devByBands = None + if "config" in requests or "fetch" in requests or "debug" in requests: + devByBands = getWifiSpeeds(FritzboxInterface(), + debug = args.debug or "debug" in requests) + # end if + + + for request in requests: + if (request == "config"): + + printConfig(devByBands) + + if "MUNIN_CAP_DIRTYCONFIG" in os.environ and os.environ["MUNIN_CAP_DIRTYCONFIG"] == "1": + print("") + printValues(devByBands) + # end if DIRTY CONFIG + + elif (request == "suggest"): + pass + elif (request == "autoconf"): + print("yes") + elif (request == "fetch"): + printValues(devByBands) + elif (request == "debug"): + printValues(devByBands, debug = True) + else: + raise Exception(f"ERROR: unknown request type \"{args.request}\""); + # end if + + # end for each request + +# end main() + + +if __name__ == "__main__": + main(); diff --git a/src/start-in-venv.sh b/src/start-in-venv.sh new file mode 100755 index 0000000..7133fc4 --- /dev/null +++ b/src/start-in-venv.sh @@ -0,0 +1,20 @@ +#!/bin/bash + +VENV=$(ls -d ~munin/venv) + +MY_ORG_SCRIPT="$0" +MY_SCRIPT_PATH=$(readlink -e "$MY_ORG_SCRIPT") +MY_SCRIPT_NAME=$(basename "$MY_ORG_SCRIPT") + +MY_BASE_PATH=$(dirname "$MY_SCRIPT_PATH" ) +if [ "${MY_BASE_PATH:0:1}" != "/" ] ; then + MY_BASE_PATH="$PWD/$MY_BASE_PATH" +fi + +PY_SCRIPT="$MY_BASE_PATH/"$(echo "$MY_SCRIPT_NAME" | sed 's:\.sh$:\.py:') + +source $VENV/bin/activate + +# in case the venv is on a filesystem without executable flags set: +$VENV/bin/python3 "$PY_SCRIPT" "$@" +