diff --git a/.gitignore b/.gitignore index edd1b43..a901c14 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,5 @@ *.iml .idea __pycache__ +*.db +config_custom.yaml \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..f46cab2 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,41 @@ +# Changelog + +## 1.2.0 + +Please ensure, that the directory of the application has the same user/group as the telegraf daemon. +By default, the `install.sh` will set the owner to `telegraf:telegraf`. + +It is required to re-execute the pip installer: + +```bash +pip3 install -r requirements.txt +``` + +### Phone Call Counter +Added a feature to see how many calls are +- missed +- incoming +- outgoing + +In addition, the total time spent on these call types is also counted. + +This feature uses a local SQLite database to store the calls and to avoid duplicates while a day. +It stores some data there, to allow more statistics later. +The data will also be cleaned up after some days to avoid too much redundant data. +In the YAML configuration file, it is possible to configure this feature. + +To see the phone call visualization in Grafana, please update your Dashboard configuration with the current version. + +### YAML configuration possibility +With this release, it is possible to configure most parameters via a YAML configuration file. +The only parameter, which still has to be used via CLI is the address `-i` parameter. + +Arguments from the CLI will always overwrite this config. + +To have full separation between default config and custom config and to not overwrite something, a custom configuration should be written in a file called `config_custom.yaml`. +The configuration of this file will be merged with the default `config.yaml` file. + +For more information about this feature, please look into the `README.md`. + +### install.sh optimization +The `install.sh` file will no longer overwrite the `telegraf_fritzbox.conf` file in `/etc/telegraf/telegraf.d/`` diff --git a/GrafanaFritzBoxDashboard.json b/GrafanaFritzBoxDashboard.json index 3776ca1..464ea87 100644 --- a/GrafanaFritzBoxDashboard.json +++ b/GrafanaFritzBoxDashboard.json @@ -70,7 +70,7 @@ }, "textMode": "value" }, - "pluginVersion": "8.3.3", + "pluginVersion": "8.3.4", "targets": [ { "datasource": { @@ -153,7 +153,7 @@ "showThresholdLabels": false, "showThresholdMarkers": true }, - "pluginVersion": "8.3.3", + "pluginVersion": "8.3.4", "targets": [ { "datasource": { @@ -248,7 +248,7 @@ "showThresholdLabels": false, "showThresholdMarkers": true }, - "pluginVersion": "8.3.3", + "pluginVersion": "8.3.4", "targets": [ { "datasource": { @@ -333,7 +333,7 @@ }, "textMode": "auto" }, - "pluginVersion": "8.3.3", + "pluginVersion": "8.3.4", "targets": [ { "datasource": { @@ -431,7 +431,7 @@ }, "textMode": "auto" }, - "pluginVersion": "8.3.3", + "pluginVersion": "8.3.4", "targets": [ { "datasource": { @@ -525,7 +525,7 @@ }, "textMode": "auto" }, - "pluginVersion": "8.3.3", + "pluginVersion": "8.3.4", "targets": [ { "alias": "Current Speed", @@ -705,7 +705,7 @@ }, "textMode": "value" }, - "pluginVersion": "8.3.3", + "pluginVersion": "8.3.4", "targets": [ { "datasource": { @@ -780,7 +780,7 @@ }, "textMode": "value" }, - "pluginVersion": "8.3.3", + "pluginVersion": "8.3.4", "targets": [ { "datasource": { @@ -854,7 +854,7 @@ }, "textMode": "value" }, - "pluginVersion": "8.3.3", + "pluginVersion": "8.3.4", "targets": [ { "datasource": { @@ -1317,7 +1317,7 @@ }, "textMode": "auto" }, - "pluginVersion": "8.3.3", + "pluginVersion": "8.3.4", "targets": [ { "alias": "Current Speed", @@ -1498,7 +1498,7 @@ "text": {}, "textMode": "value" }, - "pluginVersion": "8.3.3", + "pluginVersion": "8.3.4", "targets": [ { "datasource": { @@ -1569,7 +1569,7 @@ }, "textMode": "auto" }, - "pluginVersion": "8.3.3", + "pluginVersion": "8.3.4", "targets": [ { "datasource": { @@ -1657,7 +1657,7 @@ }, "textMode": "auto" }, - "pluginVersion": "8.3.3", + "pluginVersion": "8.3.4", "targets": [ { "datasource": { @@ -2190,7 +2190,7 @@ }, "textMode": "value" }, - "pluginVersion": "8.3.3", + "pluginVersion": "8.3.4", "targets": [ { "datasource": { @@ -2613,7 +2613,7 @@ }, "textMode": "auto" }, - "pluginVersion": "8.3.3", + "pluginVersion": "8.3.4", "targets": [ { "alias": "1 hour", @@ -3422,7 +3422,7 @@ "text": {}, "textMode": "auto" }, - "pluginVersion": "8.3.3", + "pluginVersion": "8.3.4", "targets": [ { "alias": "1 hour", @@ -3551,6 +3551,737 @@ ], "title": "TotalBytesSent", "type": "stat" + }, + { + "datasource": { + "type": "influxdb", + "uid": "Jt65v01nk" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "continuous-GrYlRd" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 6, + "x": 0, + "y": 27 + }, + "id": 55, + "options": { + "displayMode": "lcd", + "orientation": "horizontal", + "reduceOptions": { + "calcs": [ + "sum" + ], + "fields": "", + "values": false + }, + "showUnfilled": true + }, + "pluginVersion": "8.3.4", + "targets": [ + { + "alias": "Missed Calls", + "datasource": { + "type": "influxdb", + "uid": "Jt65v01nk" + }, + "groupBy": [ + { + "params": [ + "$__interval" + ], + "type": "time" + }, + { + "params": [ + "none" + ], + "type": "fill" + } + ], + "measurement": "FritzBox", + "orderByTime": "ASC", + "policy": "default", + "refId": "A", + "resultFormat": "time_series", + "select": [ + [ + { + "params": [ + "CountMissedCalls" + ], + "type": "field" + }, + { + "params": [], + "type": "sum" + } + ] + ], + "tags": [] + }, + { + "alias": "Outgoing Calls", + "datasource": { + "type": "influxdb", + "uid": "Jt65v01nk" + }, + "groupBy": [ + { + "params": [ + "$__interval" + ], + "type": "time" + }, + { + "params": [ + "none" + ], + "type": "fill" + } + ], + "hide": false, + "measurement": "FritzBox", + "orderByTime": "ASC", + "policy": "default", + "refId": "B", + "resultFormat": "time_series", + "select": [ + [ + { + "params": [ + "CountOutCalls" + ], + "type": "field" + }, + { + "params": [], + "type": "sum" + } + ] + ], + "tags": [] + }, + { + "alias": "Received Calls", + "datasource": { + "type": "influxdb", + "uid": "Jt65v01nk" + }, + "groupBy": [ + { + "params": [ + "$__interval" + ], + "type": "time" + }, + { + "params": [ + "none" + ], + "type": "fill" + } + ], + "hide": false, + "measurement": "FritzBox", + "orderByTime": "ASC", + "policy": "default", + "refId": "C", + "resultFormat": "time_series", + "select": [ + [ + { + "params": [ + "CountReceivedCalls" + ], + "type": "field" + }, + { + "params": [], + "type": "sum" + } + ] + ], + "tags": [] + } + ], + "title": "Phone Calls Count Sum", + "type": "bargauge" + }, + { + "datasource": { + "type": "influxdb", + "uid": "Jt65v01nk" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "bars", + "fillOpacity": 10, + "gradientMode": "hue", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "smooth", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": true, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "area" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "#EAB839", + "value": 2 + }, + { + "color": "#EF843C", + "value": 3 + }, + { + "color": "#E24D42", + "value": 4 + }, + { + "color": "red", + "value": 5 + }, + { + "color": "#6ED0E0", + "value": 6 + }, + { + "color": "#1F78C1", + "value": 7 + }, + { + "color": "#BA43A9", + "value": 10 + } + ] + }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 18, + "x": 6, + "y": 27 + }, + "id": 59, + "options": { + "legend": { + "calcs": [ + "sum" + ], + "displayMode": "list", + "placement": "bottom" + }, + "tooltip": { + "mode": "single" + } + }, + "pluginVersion": "8.3.4", + "targets": [ + { + "alias": "Missed Calls", + "datasource": { + "type": "influxdb", + "uid": "Jt65v01nk" + }, + "groupBy": [ + { + "params": [ + "$__interval" + ], + "type": "time" + }, + { + "params": [ + "none" + ], + "type": "fill" + } + ], + "measurement": "FritzBox", + "orderByTime": "ASC", + "policy": "default", + "refId": "A", + "resultFormat": "time_series", + "select": [ + [ + { + "params": [ + "CountMissedCalls" + ], + "type": "field" + }, + { + "params": [], + "type": "sum" + } + ] + ], + "tags": [] + }, + { + "alias": "Out Calls", + "datasource": { + "type": "influxdb", + "uid": "Jt65v01nk" + }, + "groupBy": [ + { + "params": [ + "$__interval" + ], + "type": "time" + }, + { + "params": [ + "none" + ], + "type": "fill" + } + ], + "hide": false, + "measurement": "FritzBox", + "orderByTime": "ASC", + "policy": "default", + "refId": "B", + "resultFormat": "time_series", + "select": [ + [ + { + "params": [ + "CountOutCalls" + ], + "type": "field" + }, + { + "params": [], + "type": "sum" + } + ] + ], + "tags": [] + }, + { + "alias": "Received Calls", + "datasource": { + "type": "influxdb", + "uid": "Jt65v01nk" + }, + "groupBy": [ + { + "params": [ + "$__interval" + ], + "type": "time" + }, + { + "params": [ + "none" + ], + "type": "fill" + } + ], + "hide": false, + "measurement": "FritzBox", + "orderByTime": "ASC", + "policy": "default", + "refId": "C", + "resultFormat": "time_series", + "select": [ + [ + { + "params": [ + "CountReceivedCalls" + ], + "type": "field" + }, + { + "params": [], + "type": "sum" + } + ] + ], + "tags": [] + } + ], + "title": "Phone Call Count History", + "type": "timeseries" + }, + { + "datasource": { + "type": "influxdb", + "uid": "Jt65v01nk" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "continuous-GrYlRd" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "#EAB839", + "value": 30 + }, + { + "color": "red", + "value": 60 + }, + { + "color": "#6ED0E0", + "value": 120 + } + ] + }, + "unit": "m" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 6, + "x": 0, + "y": 35 + }, + "id": 57, + "options": { + "displayMode": "lcd", + "orientation": "horizontal", + "reduceOptions": { + "calcs": [ + "sum" + ], + "fields": "", + "values": false + }, + "showUnfilled": true + }, + "pluginVersion": "8.3.4", + "targets": [ + { + "alias": "Time Outgoing Calls", + "datasource": { + "type": "influxdb", + "uid": "Jt65v01nk" + }, + "groupBy": [ + { + "params": [ + "$__interval" + ], + "type": "time" + }, + { + "params": [ + "none" + ], + "type": "fill" + } + ], + "measurement": "FritzBox", + "orderByTime": "ASC", + "policy": "default", + "refId": "A", + "resultFormat": "time_series", + "select": [ + [ + { + "params": [ + "TimeOutCalls" + ], + "type": "field" + }, + { + "params": [], + "type": "sum" + } + ] + ], + "tags": [] + }, + { + "alias": "Time Received Calls", + "datasource": { + "type": "influxdb", + "uid": "Jt65v01nk" + }, + "groupBy": [ + { + "params": [ + "$__interval" + ], + "type": "time" + }, + { + "params": [ + "null" + ], + "type": "fill" + } + ], + "hide": false, + "measurement": "FritzBox", + "orderByTime": "ASC", + "policy": "default", + "refId": "B", + "resultFormat": "time_series", + "select": [ + [ + { + "params": [ + "TimeReceivedCalls" + ], + "type": "field" + }, + { + "params": [], + "type": "sum" + } + ] + ], + "tags": [] + } + ], + "title": "Phone Calls Time Sum", + "type": "bargauge" + }, + { + "datasource": { + "type": "influxdb", + "uid": "Jt65v01nk" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "bars", + "fillOpacity": 25, + "gradientMode": "hue", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "smooth", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "area" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "#EAB839", + "value": 30 + }, + { + "color": "#EF843C", + "value": 45 + }, + { + "color": "#E24D42", + "value": 60 + }, + { + "color": "red", + "value": 90 + }, + { + "color": "#6ED0E0", + "value": 120 + } + ] + }, + "unit": "m" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 18, + "x": 6, + "y": 35 + }, + "id": 61, + "options": { + "legend": { + "calcs": [ + "sum" + ], + "displayMode": "list", + "placement": "bottom" + }, + "tooltip": { + "mode": "single" + } + }, + "pluginVersion": "8.3.4", + "targets": [ + { + "alias": "Time Received Calls", + "datasource": { + "type": "influxdb", + "uid": "Jt65v01nk" + }, + "groupBy": [ + { + "params": [ + "$__interval" + ], + "type": "time" + }, + { + "params": [ + "none" + ], + "type": "fill" + } + ], + "measurement": "FritzBox", + "orderByTime": "ASC", + "policy": "default", + "refId": "A", + "resultFormat": "time_series", + "select": [ + [ + { + "params": [ + "TimeReceivedCalls" + ], + "type": "field" + }, + { + "params": [], + "type": "sum" + } + ] + ], + "tags": [] + }, + { + "alias": "Time Outgoing Calls", + "datasource": { + "type": "influxdb", + "uid": "Jt65v01nk" + }, + "groupBy": [ + { + "params": [ + "$__interval" + ], + "type": "time" + }, + { + "params": [ + "none" + ], + "type": "fill" + } + ], + "hide": false, + "measurement": "FritzBox", + "orderByTime": "ASC", + "policy": "default", + "refId": "B", + "resultFormat": "time_series", + "select": [ + [ + { + "params": [ + "TimeOutCalls" + ], + "type": "field" + }, + { + "params": [], + "type": "sum" + } + ] + ], + "tags": [] + } + ], + "title": "Phone Call Time History", + "type": "timeseries" } ], "refresh": "1m", @@ -3561,13 +4292,13 @@ "list": [] }, "time": { - "from": "now-12h", + "from": "now-6h", "to": "now" }, "timepicker": {}, "timezone": "", "title": "FritzBoxDashboard (Telegraf)", "uid": "INoNQciRk", - "version": 29, + "version": 40, "weekStart": "monday" } \ No newline at end of file diff --git a/README.md b/README.md index 4222537..146dbef 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,7 @@ It allows reusing existing Grafana Dashboards without big changes. * All variables are cast into appropriate types (integer for numbers, string for expressions and float for 64bit total traffic) ## Install -## Prerequisites +### Prerequisites * Telegraf, InfluxDB, Grafana is already installed * Install Guides: * [How to Install TIG stack (Telegraf, Influx and Grafana) on Ubuntu](https://onlyoneaman.medium.com/how-to-install-tig-stack-telegraf-influx-and-grafana-on-ubuntu-405755901ac2) @@ -43,8 +43,10 @@ It allows reusing existing Grafana Dashboards without big changes. * Recommended: Have a dedicated user on the Fritz!Box (for example: fritz-mon) * Clone the project to your server instance -## Installation -First clone the project and edit the file `telegraf_fritzbox.conf` to configure the Fritz!Box IP address and credentials. +### Installation +First clone the project and edit the file `telegraf_fritzbox.conf` to configure the Fritz!Box IP address. + +Please have a look into the `Configuration` section to read more about the parameters and possibilities. You need to install pip (Ubuntu example): ```bash @@ -53,6 +55,9 @@ sudo pip3 install -r requirements.txt sudo ./install.sh ``` +The `install.sh` script sets the permission of the directory to the user/group `telegraf`. +If this is different on your installation, please change it in the script. + To check if everything is working, you can execute the `command` from the `telegraf_fritzbox.conf` file in your shell. If everything is fine, it outputs data like this: @@ -68,3 +73,27 @@ systemctl restart telegraf ``` Now you can import the Grafana Dashboard. It might be required to change the datasource, if the telegraf database is not the default datasource. + +### Configuration + +This application has some additional configurations. +The default settings are stored in the `config.yaml` file. +Be careful when editing this file, because it may be overwritten during an update. + +To have a stable custom configuration, you can create a file `config_custom.yaml` in the same directory (e.g. `/opt/telegraf_fritzbox/config_custom.yaml`) and add your changes there. +The file `config_custom.yaml` is merged with the default configuration. +With this, it is only necessary to add the values you want to change. + +Beside this configuration possibility you still have to configure the address of the Fritz!Box with the CLI parameter `-i `. + +It is also possible to not use the YAML configuration and set up everything via CLI parameter. +To see which are available, please use the following command: + +```bash +python3 telegraf_fritzbox.py -h +``` + +These options can be used for a preview directly with the script or they can be added to the `telegraf_fritzbox.conf` file. +Please aware, that this file will not be overwritten with the `install.sh` script. +If you want to change options, you have to do it at `/etc/telegraf/telegraf.d/telegraf_fritzbox.conf` file after you've started the `install.sh` script once. + diff --git a/config.yaml b/config.yaml new file mode 100644 index 0000000..5e9bf69 --- /dev/null +++ b/config.yaml @@ -0,0 +1,25 @@ +features: + # When this option is enabled, the application tracks calls. + # If no phones are configured, you can disable this function. + # + # Possible values: + # True - Activates phone call tracking + # False - Deactivates phone call tracking + enable_phone_call_tracking: True + +defaults: + # Default value for the database name (InfluxDB) + influx_database: 'FritzBox' + # Number of days to retrieve call tracking in Fritz!Box + fritzbox_phone_call_days: 1 + # Days how long phone call data should be kept in the local SQLite. + # Note: this value has to be higher than `fritzbox_phone_call_days` + fritzbox_phone_days_local_storage: 5 + +connection: + # The username of the monitor user + fritzbox_username: + # The password of the monitor user + fritzbox_password: + # The port of the Fritz!Box. + fritzbox_port: \ No newline at end of file diff --git a/docs/grafana_fritzbox.jpg b/docs/grafana_fritzbox.jpg index d33dbcb..15cab28 100644 Binary files a/docs/grafana_fritzbox.jpg and b/docs/grafana_fritzbox.jpg differ diff --git a/install.sh b/install.sh index 6a1037d..44359b4 100755 --- a/install.sh +++ b/install.sh @@ -2,11 +2,16 @@ DEST_DIR=/opt/telegraf_fritzbox -# copy config to telegraf -cp telegraf_fritzbox.conf /etc/telegraf/telegraf.d/ +# copy config to telegraf, but don't overwrite it +if [[ ! -e /etc/telegraf/telegraf.d/telegraf_fritzbox.conf ]]; then + cp telegraf_fritzbox.conf /etc/telegraf/telegraf.d/ +fi # create directory mkdir -p ${DEST_DIR} cp telegraf_fritzbox.py ${DEST_DIR}/ +cp config.yaml ${DEST_DIR}/ cp -fR models ${DEST_DIR}/ cp -fR modules ${DEST_DIR}/ + +chown -R telegraf:telegraf ${DEST_DIR} diff --git a/models/config_model.py b/models/config_model.py new file mode 100644 index 0000000..c6e59e2 --- /dev/null +++ b/models/config_model.py @@ -0,0 +1,40 @@ +from dataclasses import dataclass + + +@dataclass +class ConfigurationModel: + """Contains Configuration data""" + + def __init__(self): + self.connection_address: str = "" + self.connection_username: str = "" + self.connection_password: str = "" + self.connection_port: str = "" + self.defaults_database: str = "FritzBox" + self.defaults_phone_days: int = 1 + self.defaults_phone_days_kept: int = 5 + self.features_enable_phone_call_tracking: bool = True + + def set_connection_address(self, address: str) -> None: + self.connection_address = address + + def set_connection_username(self, username: str) -> None: + self.connection_username = username + + def set_connection_password(self, password: str) -> None: + self.connection_password = password + + def set_connection_port(self, port: str) -> None: + self.connection_port = port + + def set_defaults_database(self, database: str) -> None: + self.defaults_database = database + + def set_defaults_phone_days(self, phone_days: str) -> None: + self.defaults_phone_days = phone_days + + def set_defaults_phone_days_kept(self, phone_days_kept: str) -> None: + self.defaults_phone_days_kept = phone_days_kept + + def set_features_enable_phone_call_tracking(self, phone_tracking: bool) -> None: + self.features_enable_phone_call_tracking = phone_tracking diff --git a/models/phone/fritzbox_phone_model.py b/models/phone/fritzbox_phone_model.py new file mode 100644 index 0000000..5607b46 --- /dev/null +++ b/models/phone/fritzbox_phone_model.py @@ -0,0 +1,28 @@ +from dataclasses import dataclass + +from models.fritzbox_model_interface import FritzboxModelInterface +from modules.influx_print import InfluxPrint + + +@dataclass +class FritzboxPhoneModel(FritzboxModelInterface): + """Contains Phone data""" + + def __init__(self, count_missed_calls: int, count_out_calls: int, count_received_calls: int, + time_missed_calls: int, time_out_calls: int, time_received_calls: int): + self.count_missed_calls: int = count_missed_calls + self.count_out_calls: int = count_out_calls + self.count_received_calls: int = count_received_calls + self.time_missed_calls: int = time_missed_calls + self.time_out_calls: int = time_out_calls + self.time_received_calls: int = time_received_calls + + def influx_data(self) -> str: + influx_result = list() + InfluxPrint.append(influx_result, "CountMissedCalls", self.count_missed_calls) + InfluxPrint.append(influx_result, "CountOutCalls", self.count_out_calls) + InfluxPrint.append(influx_result, "CountReceivedCalls", self.count_received_calls) + InfluxPrint.append(influx_result, "TimeReceivedCalls", self.time_received_calls) + InfluxPrint.append(influx_result, "TimeOutCalls", self.time_out_calls) + InfluxPrint.append(influx_result, "TimeReceivedCalls", self.time_received_calls) + return ",".join(influx_result) diff --git a/modules/configuration.py b/modules/configuration.py new file mode 100644 index 0000000..c9c11d4 --- /dev/null +++ b/modules/configuration.py @@ -0,0 +1,49 @@ +import logging +import os +from typing import Any + +import hiyapyco + +from models.config_model import ConfigurationModel + + +class Configuration: + def __init__(self): + self.__config_yaml = self.__load_config() + self.__config = ConfigurationModel() + self.__config.set_connection_username(self.__connection_fritz_username()) + self.__config.set_connection_password(self.__connection_fritz_password()) + self.__config.set_connection_port(self.__connection_fritz_port()) + self.__config.set_defaults_database(self.__default_database()) + self.__config.set_defaults_phone_days(self.__defaults_phone_days()) + self.__config.set_defaults_phone_days_kept(self.__defaults_phone_days_kept()) + self.__config.set_features_enable_phone_call_tracking(self.__features_phone_call_tracking()) + + def get(self): + return self.__config + + def __load_config(self) -> Any: + dir_path = os.path.dirname(os.path.realpath(__file__)) + return hiyapyco.load(f'{dir_path}/../config.yaml', f'{dir_path}/../config_custom.yaml', method=hiyapyco.METHOD_MERGE, interpolate=True, + failonmissingfiles=False, loglevelmissingfiles=logging.DEBUG) + + def __connection_fritz_username(self): + return self.__config_yaml.get('connection')['fritzbox_username'] + + def __connection_fritz_password(self): + return self.__config_yaml.get('connection')['fritzbox_password'] + + def __connection_fritz_port(self): + return self.__config_yaml.get('connection')['fritzbox_port'] + + def __default_database(self): + return self.__config_yaml.get('defaults')['influx_database'] + + def __defaults_phone_days(self): + return self.__config_yaml.get('defaults')['fritzbox_phone_call_days'] + + def __defaults_phone_days_kept(self): + return self.__config_yaml.get('defaults')['fritzbox_phone_days_local_storage'] + + def __features_phone_call_tracking(self) -> bool: + return self.__config_yaml.get('features')['enable_phone_call_tracking'] diff --git a/modules/database/sqllite.py b/modules/database/sqllite.py new file mode 100644 index 0000000..223126c --- /dev/null +++ b/modules/database/sqllite.py @@ -0,0 +1,47 @@ +import os +import sqlite3 as sql +import sys +from sqlite3 import Connection, Cursor +from typing import Iterable + + +class Database: + __connection: Connection + + def __init__(self): + dir_path = os.path.dirname(os.path.realpath(__file__)) + try: + self.__connection = sql.connect(f'{dir_path}/../../fritz.db') + self.__create_tables() + except Exception as exception: + print(f"Cannot open database [{exception}]", file=sys.stderr) + exit(1) + + def __create_tables(self) -> None: + with self.__connection: + self.__connection.execute(self.__create_table_phone_calls()) + + def __create_table_phone_calls(self) -> str: + return """ + CREATE TABLE if not exists PHONE_CALLS ( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + call_id varchar(30) NOT NULL, + call_name VARCHAR(255), + call_number VARCHAR(100), + call_duration NUMBER(8, 0), + call_type NUMBER(2,0) NOT NULL, + call_date NUMBER(12,0) NOT NULL + ); + """ + + def insert(self, sql_query: str, data: Iterable) -> None: + with self.__connection: + self.__connection.executemany(sql_query, data) + + def select(self, sql_query) -> list: + with self.__connection: + return self.__connection.execute(sql_query).fetchall() + + def execute(self, sql_query) -> None: + with self.__connection: + self.__connection.execute(sql_query) diff --git a/modules/fritz_connect.py b/modules/fritz_connect.py index 499beb1..318f79e 100644 --- a/modules/fritz_connect.py +++ b/modules/fritz_connect.py @@ -3,13 +3,24 @@ from fritzconnection.lib.fritzhosts import FritzHosts from fritzconnection.lib.fritzstatus import FritzStatus +from models.config_model import ConfigurationModel +from modules.database.sqllite import Database + class FritzboxConnect: - def __init__(self, address: str, username: str, password: str, port: str): - self.__FCALL = FritzCall(address=address, user=username, password=password, port=port, timeout=2.0) + def __init__(self, config: ConfigurationModel): + address: str = config.connection_address + username: str = config.connection_username + password: str = config.connection_password + port: str = config.connection_port + + if config.features_enable_phone_call_tracking: + self.__FCALL = FritzCall(address=address, user=username, password=password, port=port, timeout=2.0) self.__FCONN = FritzConnection(address=address, user=username, password=password, port=port, timeout=2.0) self.__FHOSTS = FritzHosts(address=address, user=username, password=password, port=port, timeout=2.0) self.__FSTAT = FritzStatus(address=address, user=username, password=password, port=port, timeout=2.0) + self.__DB = Database() + self.__CONFIG = config # get info of DSL (WANPPP) or Cable (WANIP) if len(self.read_module('WANPPPConnection1', 'GetInfo')) > 0: @@ -17,6 +28,9 @@ def __init__(self, address: str, username: str, password: str, port: str): else: self.__FCONN_INFO = self.read_module('WANIPConnection1', 'GetInfo') + def config(self): + return self.__CONFIG + def call(self): return self.__FCALL @@ -38,3 +52,6 @@ def read_module(self, module: str, action: str): except: answer = dict() # return an empty dict in case of failure return answer + + def database(self): + return self.__DB diff --git a/modules/influx_print.py b/modules/influx_print.py index 5fcea66..583a12e 100644 --- a/modules/influx_print.py +++ b/modules/influx_print.py @@ -1,3 +1,6 @@ +from datetime import datetime + + class InfluxPrint: def __init__(self, fritzbox_db: str, fritzbox_host: str): self.__FB_DB = fritzbox_db @@ -23,8 +26,14 @@ def append(influx_list: list, tag_name: str, value): return influx_list else: raise Exception("Unable to map value for influxdb") + return influx_list + @staticmethod + def calculate_time_in_seconds(timestr: str) -> int: + pt = datetime.strptime(timestr, '%M:%S') + return pt.second + pt.minute * 60 + pt.hour * 3600 + @staticmethod def append_float(influx_list: list, tag_name: str, value: float): if isinstance(value, float): diff --git a/modules/phone/fritz_connect_phone.py b/modules/phone/fritz_connect_phone.py new file mode 100644 index 0000000..0514838 --- /dev/null +++ b/modules/phone/fritz_connect_phone.py @@ -0,0 +1,127 @@ +from datetime import datetime, timedelta, date, time +from enum import Enum + +from fritzconnection.lib.fritzcall import RECEIVED_CALL_TYPE, MISSED_CALL_TYPE, OUT_CALL_TYPE + +from models.phone.fritzbox_phone_model import FritzboxPhoneModel +from modules.fritz_connect import FritzboxConnect + + +class FritzboxConnectPhone: + def __init__(self, fc: FritzboxConnect): + self.__FC_CALL = fc.call() + self.__DB = fc.database() + self.__DAYS = fc.config().defaults_phone_days + self.__DAYS_KEPT = fc.config().defaults_phone_days_kept + self.__CALLS_MISSED = self.__update_db_and_filter( + self.__FC_CALL.get_missed_calls(days=self.__DAYS), + CallType.missed) + self.__CALLS_OUTGOING = self.__update_db_and_filter( + self.__FC_CALL.get_out_calls(days=self.__DAYS), + CallType.outgoing) + self.__CALLS_RECEIVED = self.__update_db_and_filter( + self.__FC_CALL.get_received_calls(days=self.__DAYS), + CallType.received) + self.__cleanup() + + def stats(self) -> FritzboxPhoneModel: + phone_model = FritzboxPhoneModel( + count_missed_calls=self.count_missed_calls(), + count_out_calls=self.count_out_calls(), + count_received_calls=self.count_received_calls(), + time_missed_calls=self.time_missed_calls(), + time_out_calls=self.time_out_calls(), + time_received_calls=self.time_received_calls() + ) + + return phone_model + + def count_missed_calls(self) -> int: + return len(self.__CALLS_MISSED) + + def count_out_calls(self) -> int: + return len(self.__CALLS_OUTGOING) + + def count_received_calls(self) -> int: + return len(self.__CALLS_RECEIVED) + + def time_missed_calls(self) -> int: + return self.__aggregate_call_time(self.__CALLS_MISSED) + + def time_out_calls(self) -> int: + return self.__aggregate_call_time(self.__CALLS_OUTGOING) + + def time_received_calls(self) -> int: + return self.__aggregate_call_time(self.__CALLS_RECEIVED) + + def __aggregate_call_time(self, phone_calls: list): + call_time: int = 0 + for entry in phone_calls: + call_time = call_time + self.__calculate_time_in_seconds(entry.Duration) + return call_time + + def __calculate_time_in_seconds(self, timestr: str) -> int: + pt = datetime.strptime(timestr, '%M:%S') + return pt.second + pt.minute * 60 + pt.hour * 3600 + + def __update_db_and_filter(self, phone_calls_fb: list, call_type) -> list: + self.__check_call_type(call_type) + sql_query = f""" + SELECT call_id FROM PHONE_CALLS + WHERE call_date > {int((self.__datetime_start_today() - timedelta(days=self.__DAYS)).timestamp())} + AND call_type = {call_type.value}""" + phone_calls_db = self.__DB.select(sql_query) + + if len(phone_calls_fb) > 0: + if len(phone_calls_fb) == 0: + self.__add_calls_to_database(phone_calls_fb) + else: + for entry_fb in phone_calls_fb[:]: + for entry_db in phone_calls_db: + if entry_fb.Id == entry_db[0]: + phone_calls_fb.remove(entry_fb) + self.__add_calls_to_database(phone_calls_fb) + return phone_calls_fb + + def __add_calls_to_database(self, calls: list) -> None: + if len(calls) > 0: + data: list = [] + for entry in calls: + data.append( + (entry.Id, entry.Name, entry.Caller, self.__calculate_time_in_seconds(entry.Duration), + self.call_type(entry.Type), int(entry.date.timestamp()))) + self.__DB.insert( + """ + INSERT INTO PHONE_CALLS (call_id, call_name, call_number, call_duration, call_type, call_date) + values + (?, ?, ?, ?, ?, ?)""", + data) + + def call_type(self, call_type: str) -> int: + if int(call_type) == RECEIVED_CALL_TYPE: + return CallType.received.value + if int(call_type) == OUT_CALL_TYPE: + return CallType.outgoing.value + if int(call_type) == MISSED_CALL_TYPE: + return CallType.missed.value + + def __check_call_type(self, call_type) -> bool: + if isinstance(call_type, CallType): + return True + raise Exception("Wrong type for enum CallType") + + def __datetime_start_today(self) -> datetime: + return datetime.combine(date.today(), time()) + + def __cleanup(self): + sql_query = f""" + DELETE FROM PHONE_CALLS + WHERE call_date < {int((self.__datetime_start_today() - timedelta(days=self.__DAYS_KEPT)).timestamp())} + """ + self.__DB.execute(sql_query) + + +class CallType(Enum): + received: int = 0 + outgoing: int = 1 + missed: int = 2 diff --git a/requirements.txt b/requirements.txt index d482929..5670c4d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1,2 @@ -fritzconnection >= 1.9.1 \ No newline at end of file +fritzconnection >= 1.9.1 +HiYaPyCo~=0.4.16 \ No newline at end of file diff --git a/telegraf_fritzbox.py b/telegraf_fritzbox.py index c9ad061..3efa7e8 100644 --- a/telegraf_fritzbox.py +++ b/telegraf_fritzbox.py @@ -3,6 +3,7 @@ from fritzconnection.cli.utils import get_cli_arguments +from modules.configuration import Configuration from modules.fritz_connect import FritzboxConnect from modules.general.fritzbox_general import FritzboxConnectGeneral from modules.general.fritzbox_status import FritzboxConnectStatus @@ -12,77 +13,82 @@ from modules.network.fritz_connect_network import FritzboxConnectNetwork from modules.network.fritz_connect_wan import FritzboxConnectWAN from modules.network.fritz_connect_wlan import FritzboxConnectWLAN, WLANType - # Default database for InfluxDB -FRITZBOX_DEFAULT_DATABASE = 'FritzBox' - - -def fritzbox_host_name(fc: FritzboxConnect) -> str: - host_config = fc.read_module('LANHostConfigManagement1', 'GetInfo') - try: - return host_config['NewDomainName'] - except KeyError: - return "fritz.box" +from modules.phone.fritz_connect_phone import FritzboxConnectPhone -def show_help() -> None: - print() - print('Options:') - print('-i | --ip-address [FRITZ_IP_ADDRESS]: ' - 'IP-address of the Fritz!Box (Default 169.254.1.1)') - print('--port [FRITZ_TCP_PORT] : ' - 'Port of the Fritz!Box (Default: 49000)') - print('-u | --username [FRITZ_USERNAME] : ' - 'Fritz!Box username (Default: admin)') - print('-p | --password [FRITZ_PASSWORD] : ' - 'Fritz!Box password for the monitoring user.') - print('-e | --encrypt [ENCRYPT] : ' - 'Use a secure connection to your Fritz!Box. Can be True or False (Default: False)') - print(f'-d | --database [FRITZBOX_DATABASE]: ' - f'Fritz!Box Database. (Default: {FRITZBOX_DEFAULT_DATABASE})') +class Application: + def __init__(self): + self.__config: Configuration = Configuration() + def execute(self) -> None: + args = self.__get_cli_args() + if not args.password and self.__config.get().connection_password is None: + print('Please configure a password to access the Fritz!Box.', file=sys.stderr) + self.__show_help() + sys.exit(1) -def get_cli_args() -> argparse.Namespace: - args = get_cli_arguments() - parser = argparse.ArgumentParser() - parser.add_argument('-d', '--database', - nargs='?', default=FRITZBOX_DEFAULT_DATABASE, const=None, - dest='database', - help='Specify a database name of the InfluxDB.' - 'Default: %s' % FRITZBOX_DEFAULT_DATABASE) - return parser.parse_known_args(namespace=args)[0] - + if args.address: + self.__config.get().set_connection_address(args.address) + if args.username: + self.__config.get().set_connection_username(args.username) + if args.password: + self.__config.get().set_connection_password(args.password) -def execute() -> None: - args = get_cli_args() - if not args.password: - print('Please configure a password to access the Fritz!Box.') - show_help() - sys.exit(1) - else: try: - fritz_connect = FritzboxConnect( - address=args.address, - username=args.username, - password=args.password, - port=args.port) + fritz_connect = FritzboxConnect(config=self.__config.get()) except BaseException as exception: - print(exception) - print("Cannot connect to Fritz!Box. ") - show_help() + print(exception, file=sys.stderr) + print("Cannot connect to Fritz!Box.", file=sys.stderr) + self.__show_help() sys.exit(1) - influxp = InfluxPrint(args.database, fritzbox_host_name(fritz_connect)) - influxp.print("general", FritzboxConnectGeneral(fritz_connect).stats().influx_data()) - influxp.print("status", FritzboxConnectStatus(fritz_connect).stats().influx_data()) - influxp.print("lan", FritzboxConnectLAN(fritz_connect).stats().influx_data()) - influxp.print("dsl", FritzboxConnectDSL(fritz_connect).stats().influx_data()) - influxp.print("wlan_2.4GHz", FritzboxConnectWLAN(fritz_connect, WLANType.WLAN_2_4_GHZ).stats().influx_data()) - influxp.print("wlan_5GHz", FritzboxConnectWLAN(fritz_connect, WLANType.WLAN_5_GHZ).stats().influx_data()) - influxp.print("wlan_Guest", FritzboxConnectWLAN(fritz_connect, WLANType.WLAN_GUEST).stats().influx_data()) - influxp.print("wan", FritzboxConnectWAN(fritz_connect).stats().influx_data()) - influxp.print("network", FritzboxConnectNetwork(fritz_connect).stats().influx_data()) + influxp = InfluxPrint(args.database, self.__fritzbox_host_name(fritz_connect)) + influxp.print("general", FritzboxConnectGeneral(fritz_connect).stats().influx_data()) + influxp.print("status", FritzboxConnectStatus(fritz_connect).stats().influx_data()) + influxp.print("lan", FritzboxConnectLAN(fritz_connect).stats().influx_data()) + influxp.print("dsl", FritzboxConnectDSL(fritz_connect).stats().influx_data()) + influxp.print("wlan_2.4GHz", FritzboxConnectWLAN(fritz_connect, WLANType.WLAN_2_4_GHZ).stats().influx_data()) + influxp.print("wlan_5GHz", FritzboxConnectWLAN(fritz_connect, WLANType.WLAN_5_GHZ).stats().influx_data()) + influxp.print("wlan_Guest", FritzboxConnectWLAN(fritz_connect, WLANType.WLAN_GUEST).stats().influx_data()) + influxp.print("wan", FritzboxConnectWAN(fritz_connect).stats().influx_data()) + influxp.print("network", FritzboxConnectNetwork(fritz_connect).stats().influx_data()) + if self.__config.get().features_enable_phone_call_tracking: + influxp.print("phone", FritzboxConnectPhone(fritz_connect).stats().influx_data()) + + def __fritzbox_host_name(self, fc: FritzboxConnect) -> str: + host_config = fc.read_module('LANHostConfigManagement1', 'GetInfo') + try: + return host_config['NewDomainName'] + except KeyError: + return "fritz.box" + + def __show_help(self) -> None: + print() + print('Options:') + print('-i | --ip-address [FRITZ_IP_ADDRESS]: ' + 'IP-address of the Fritz!Box (Default 169.254.1.1)') + print('--port [FRITZ_TCP_PORT] : ' + 'Port of the Fritz!Box (Default: 49000)') + print('-u | --username [FRITZ_USERNAME] : ' + 'Fritz!Box username (Default: admin)') + print('-p | --password [FRITZ_PASSWORD] : ' + 'Fritz!Box password for the monitoring user.') + print('-e | --encrypt [ENCRYPT] : ' + 'Use a secure connection to your Fritz!Box. Can be True or False (Default: False)') + print(f'-d | --database [FRITZBOX_DATABASE]: ' + f'Fritz!Box Database. (Default: {self.__config.get().defaults_database})') + + def __get_cli_args(self) -> argparse.Namespace: + args = get_cli_arguments() + parser = argparse.ArgumentParser() + parser.add_argument('-d', '--database', + nargs='?', default=self.__config.get().defaults_database, const=None, + dest='database', + help='Specify a database name of the InfluxDB.' + 'Default: %s' % self.__config.get().defaults_database) + return parser.parse_known_args(namespace=args)[0] if __name__ == '__main__': - execute() + Application().execute()