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" "$@"
+