├── .gitignore ├── .github ├── dependabot.yml └── workflows │ └── docker-build-publish.yml ├── go.mod ├── systemd └── fritzbox_exporter.service ├── grafana ├── README.md └── Dashboard_for_grafana6.json ├── health.go ├── docker-compose.yml ├── luaTest.json ├── Dockerfile ├── k8s-fritzbox.yaml ├── fritzbox_lua ├── README.md └── lua_client.go ├── luaTest-many.json ├── README.md ├── metrics-lua.json ├── metrics-lua_cable.json ├── LICENSE ├── go.sum ├── metrics.json ├── fritzbox_upnp └── service.go └── main.go /.gitignore: -------------------------------------------------------------------------------- 1 | fritzbox_exporter 2 | .project 3 | vendor/* 4 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | # Maintain dependencies for GitHub Actions 4 | - package-ecosystem: "github-actions" 5 | directory: "/" 6 | schedule: 7 | interval: "weekly" 8 | 9 | # Enable version updates for Docker 10 | - package-ecosystem: "docker" 11 | directory: "/" 12 | schedule: 13 | interval: "weekly" 14 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/sberk42/fritzbox_exporter 2 | 3 | go 1.16 4 | 5 | require ( 6 | github.com/heptiolabs/healthcheck v0.0.0-20211123025425-613501dd5deb // indirect 7 | github.com/namsral/flag v1.7.4-pre // indirect 8 | github.com/prometheus/client_golang v1.11.0 // indirect 9 | github.com/sirupsen/logrus v1.8.1 // indirect 10 | golang.org/x/text v0.3.7 // indirect 11 | ) 12 | -------------------------------------------------------------------------------- /systemd/fritzbox_exporter.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=FritzBox Prometheus Exporter 3 | 4 | [Service] 5 | User=fritzbox_exporter 6 | Group=fritzbox_exporter 7 | EnvironmentFile=/opt/fritzbox_exporter/.fritzbox_exporter.env 8 | ExecStart=/opt/fritzbox_exporter/fritzbox_exporter -gateway-url http://fritz.box:49000 -metrics-file /opt/fritzbox_exporter/metrics.json -listen-address 127.0.0.1:9101 9 | 10 | [Install] 11 | WantedBy=multi-user.target 12 | -------------------------------------------------------------------------------- /grafana/README.md: -------------------------------------------------------------------------------- 1 | # Grafana dashboard working with this exporter 2 | 3 | This dashboard is based on the following dashboard: 4 | https://grafana.com/grafana/dashboards/713 5 | 6 | Instead of influx it uses prometheus and has been modified and enhanced. 7 | 8 | The dashboard is now also published to [Grafana](https://grafana.com/grafana/dashboards/12579) 9 | 10 | The dashboard is now also working with grafana 7, for grafana 6 use [Dashboard_for_grafana6.json](Dashboard_for_grafana6.json) 11 | -------------------------------------------------------------------------------- /health.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/heptiolabs/healthcheck" 7 | ) 8 | 9 | // createHealthChecks will create the readiness and liveness endpoints and add the check functions. 10 | func createHealthChecks(gatewayUrl string) healthcheck.Handler { 11 | health := healthcheck.NewHandler() 12 | 13 | health.AddReadinessCheck("FRITZ!Box connection", 14 | healthcheck.HTTPGetCheck(gatewayUrl+"/any.xml", time.Duration(3)*time.Second)) 15 | 16 | health.AddLivenessCheck("go-routines", healthcheck.GoroutineCountCheck(100)) 17 | return health 18 | } 19 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | fritzbox-prometheus-exporter: 4 | hostname: fritzbox-prometheus-exporter 5 | build: 6 | context: . 7 | dockerfile: Dockerfile 8 | container_name: fritzbox-prometheus-exporter 9 | # for dns issues like "dial tcp: lookup fritz.box on 127.0.0.11:53: no such host" 10 | # uncomment and fill the following line: 11 | # dns: YOUR_FRITZBOX_IP 12 | ports: 13 | - "9042:9042" 14 | #expose: 15 | # - "9042" 16 | restart: unless-stopped 17 | environment: 18 | USERNAME: your_fritzbox_username 19 | PASSWORD: your_fritzbox_password 20 | GATEWAY_URL: http://192.168.0.1:49000 21 | LISTEN_ADDRESS: 0.0.0.0:9042 22 | -------------------------------------------------------------------------------- /luaTest.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "path": "data.lua", 4 | "params": "page=overview" 5 | }, 6 | { 7 | "path": "data.lua", 8 | "params": "page=dslOv" 9 | }, 10 | { 11 | "path": "data.lua", 12 | "params": "page=dectMon" 13 | }, 14 | { 15 | "path": "data.lua", 16 | "params": "page=netDev" 17 | }, 18 | { 19 | "path": "data.lua", 20 | "params": "page=usbOv" 21 | }, 22 | { 23 | "path": "data.lua", 24 | "params": "page=sh_dev" 25 | }, 26 | { 27 | "path": "data.lua", 28 | "params": "page=energy" 29 | }, 30 | { 31 | "path": "data.lua", 32 | "params": "page=ecoStat" 33 | }, 34 | { 35 | "path": "GET:webservices/homeautoswitch.lua", 36 | "params": "switchcmd=getdevicelistinfos" 37 | }, 38 | { 39 | "path": "GET:webservices/homeautoswitch.lua", 40 | "params": "switchcmd=getbasicdevicestats" 41 | } 42 | ] -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # syntax=docker/dockerfile:1 2 | 3 | # Build Image 4 | FROM golang:alpine3.15 AS builder 5 | RUN go install github.com/sberk42/fritzbox_exporter@latest \ 6 | && mkdir /app \ 7 | && mv /go/bin/fritzbox_exporter /app 8 | 9 | WORKDIR /app 10 | 11 | COPY metrics.json metrics-lua.json /app/ 12 | 13 | # Runtime Image 14 | FROM alpine:3.23.0 as runtime-image 15 | 16 | ARG REPO=sberk42/fritzbox_exporter 17 | 18 | LABEL org.opencontainers.image.source https://github.com/${REPO} 19 | 20 | ENV USERNAME username 21 | ENV PASSWORD password 22 | ENV GATEWAY_URL http://fritz.box:49000 23 | ENV LISTEN_ADDRESS 0.0.0.0:9042 24 | 25 | RUN mkdir /app \ 26 | && addgroup -S -g 1000 fritzbox \ 27 | && adduser -S -u 1000 -G fritzbox fritzbox \ 28 | && chown -R fritzbox:fritzbox /app 29 | 30 | WORKDIR /app 31 | 32 | COPY --chown=fritzbox:fritzbox --from=builder /app /app 33 | 34 | EXPOSE 9042 35 | 36 | ENTRYPOINT [ "sh", "-c", "/app/fritzbox_exporter" ] 37 | CMD [ "-username", "${USERNAME}", "-password", "${PASSWORD}", "-gateway-url", "${GATEWAY_URL}", "-listen-address", "${LISTEN_ADDRESS}" ] 38 | -------------------------------------------------------------------------------- /k8s-fritzbox.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | labels: 5 | app: fritzbox-exporter 6 | name: fritzbox-exporter 7 | namespace: metrics-app 8 | spec: 9 | progressDeadlineSeconds: 600 10 | replicas: 1 11 | revisionHistoryLimit: 10 12 | selector: 13 | matchLabels: 14 | app: fritzbox-exporter 15 | strategy: 16 | rollingUpdate: 17 | maxSurge: 25% 18 | maxUnavailable: 25% 19 | type: RollingUpdate 20 | template: 21 | metadata: 22 | labels: 23 | app: fritzbox-exporter 24 | name: fritzbox-exporter 25 | spec: 26 | containers: 27 | - env: 28 | - name: GWURL 29 | value: 'http://ip-fritzbox:49000' 30 | - name: USERNAME 31 | value: 'username' 32 | - name: PASSWORD 33 | value: 'password' 34 | image: alexxanddr/fritzbox-exporter:latest 35 | imagePullPolicy: Always 36 | name: fritzbox-exporter 37 | resources: {} 38 | securityContext: 39 | privileged: false 40 | terminationMessagePath: /dev/termination-log 41 | terminationMessagePolicy: File 42 | dnsPolicy: ClusterFirst 43 | hostAliases: 44 | - hostnames: 45 | - fritz.box 46 | ip: 'ip-fritzbox' 47 | restartPolicy: Always 48 | schedulerName: default-scheduler 49 | securityContext: {} 50 | terminationGracePeriodSeconds: 30 51 | --- 52 | apiVersion: v1 53 | kind: Service 54 | metadata: 55 | annotations: 56 | prometheus.io/path: /metrics 57 | prometheus.io/port: "9042" 58 | prometheus.io/scrape: "true" 59 | labels: 60 | app: fritzbox-exporter 61 | name: fritzbox-exporter 62 | namespace: metrics-app 63 | spec: 64 | clusterIP: 65 | ports: 66 | - name: tcp 67 | port: 9042 68 | protocol: TCP 69 | targetPort: 9042 70 | selector: 71 | app: fritzbox-exporter 72 | sessionAffinity: None 73 | type: ClusterIP 74 | status: 75 | loadBalancer: {} 76 | -------------------------------------------------------------------------------- /fritzbox_lua/README.md: -------------------------------------------------------------------------------- 1 | # Client for LUA API of FRITZ!Box UI 2 | 3 | **Note:** This client only support calls that return JSON (some seem to return HTML they are not supported) 4 | 5 | There does not seem to be a complete documentation of the API, the authentication and getting a sid (Session ID) is described here: 6 | [https://avm.de/fileadmin/user_upload/Global/Service/Schnittstellen/AVM_Technical_Note_-_Session_ID.pdf] 7 | 8 | ## Details 9 | Most of the calls seem to be using the data.lua url with a http FORM POST request. As parameters the page and session id are required (e.g.: sid=&page=engery). The result is JSON with the data needed to create the respective UI. 10 | Some calls (like inetstat_monitor.lua) seem to use GET rather than POST, the client also supports them, but prefix GET: is needed, otherwise a post is done. 11 | 12 | Since no public documentation for the JSON format of the various pages seem to exist, you need to observe the calls made by the UI and analyse the JSON result. However the client should be generic enough to get metric and label values from all kind of nested hash and array structures contained in the JSONs. 13 | 14 | ## Compatibility 15 | The client was developed on a Fritzbox 7590 running on 07.21, other models or versions may behave differently so just test and see what works, but again the generic part of the client should still work as long as there is a JSON result. 16 | 17 | ## Translations 18 | Since the API is used to drive the UI, labels are translated and will be returned in the language configured in the Fritzbox. There seems to be a lang parameter but it looks like it is simply ignored. Having translated labels is annoying, therefore the clients also support renaming them based on regex. 19 | Currently the regex are defined for: 20 | - German 21 | 22 | If your Fritzbox is running in another language you need to adjust them or you will receive different labels, that may not work with dashboards using them for filtering! 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /luaTest-many.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "path": "data.lua", 4 | "params": "page=overview" 5 | }, 6 | { 7 | "path": "data.lua", 8 | "params": "page=ipv6" 9 | }, 10 | { 11 | "path": "data.lua", 12 | "params": "page=dnsSrv" 13 | }, 14 | { 15 | "path": "data.lua", 16 | "params": "page=kidLis" 17 | }, 18 | { 19 | "path": "data.lua", 20 | "params": "page=trafapp" 21 | }, 22 | { 23 | "path": "data.lua", 24 | "params": "page=portoverview" 25 | }, 26 | { 27 | "path": "data.lua", 28 | "params": "page=dslOv" 29 | }, 30 | { 31 | "path": "data.lua", 32 | "params": "page=dialLi" 33 | }, 34 | { 35 | "path": "data.lua", 36 | "params": "page=bookLi" 37 | }, 38 | { 39 | "path": "data.lua", 40 | "params": "page=dectSet" 41 | }, 42 | { 43 | "path": "data.lua", 44 | "params": "page=dectMon" 45 | }, 46 | { 47 | "path": "data.lua", 48 | "params": "page=homeNet" 49 | }, 50 | { 51 | "path": "data.lua", 52 | "params": "page=netDev" 53 | }, 54 | { 55 | "path": "data.lua", 56 | "params": "page=netSet" 57 | }, 58 | { 59 | "path": "data.lua", 60 | "params": "page=usbOv" 61 | }, 62 | { 63 | "path": "data.lua", 64 | "params": "page=mServSet" 65 | }, 66 | { 67 | "path": "data.lua", 68 | "params": "page=wSet" 69 | }, 70 | { 71 | "path": "data.lua", 72 | "params": "page=chan" 73 | }, 74 | { 75 | "path": "data.lua", 76 | "params": "page=sh_dev" 77 | }, 78 | { 79 | "path": "data.lua", 80 | "params": "page=energy" 81 | }, 82 | { 83 | "path": "data.lua", 84 | "params": "page=ecoStat" 85 | }, 86 | { 87 | "path": "GET:internet/inetstat_monitor.lua", 88 | "params": "action=get_graphic&useajax=1" 89 | }, 90 | { 91 | "path": "GET:internet/internet_settings.lua", 92 | "params": "multiwan_page=dsl&useajax=1" 93 | }, 94 | { 95 | "path": "GET:internet/dsl_stats_tab.lua", 96 | "params": "update=mainDiv&useajax=1" 97 | }, 98 | { 99 | "path": "GET:net/network.lua", 100 | "params": "useajax=1" 101 | }, 102 | { 103 | "path": "data.lua", 104 | "params": "page=netCnt" 105 | }, 106 | { 107 | "path": "data.lua", 108 | "params": "page=dslStat" 109 | } 110 | ] -------------------------------------------------------------------------------- /.github/workflows/docker-build-publish.yml: -------------------------------------------------------------------------------- 1 | name: build & publish docker image 2 | 3 | on: 4 | push: 5 | schedule: 6 | # every sunday at midnight 7 | - cron: '0 0 * * 0' 8 | workflow_dispatch: 9 | 10 | env: 11 | IMAGE_NAME: dotwee/fritzbox-prometheus-exporter 12 | 13 | jobs: 14 | build: 15 | runs-on: ubuntu-latest 16 | permissions: 17 | contents: read 18 | packages: write 19 | 20 | # This is used to complete the identity challenge 21 | # with sigstore/fulcio when running outside of PRs. 22 | id-token: write 23 | 24 | steps: 25 | - name: Checkout repository 26 | uses: actions/checkout@v6 27 | 28 | # Install the cosign tool except on PR 29 | # https://github.com/sigstore/cosign-installer 30 | - name: Install cosign 31 | uses: sigstore/cosign-installer@v4.0.0 32 | with: 33 | cosign-release: 'v1.6.0' 34 | 35 | - name: Setup QEMU binaries 36 | uses: docker/setup-qemu-action@v3.7.0 37 | 38 | - name: Setup Docker buildx 39 | uses: docker/setup-buildx-action@v3.11.1 40 | with: 41 | buildkitd-flags: --debug 42 | 43 | - name: Log into Docker Hub 44 | uses: docker/login-action@v3.6.0 45 | with: 46 | username: dotwee 47 | password: ${{ secrets.DOCKER_REGISTRY_TOKEN }} 48 | 49 | - name: Log into GitHub registry 50 | uses: docker/login-action@v3.6.0 51 | with: 52 | registry: ghcr.io 53 | username: ${{ github.repository_owner }} 54 | password: ${{ secrets.GITHUB_TOKEN }} 55 | 56 | # Extract metadata (tags, labels) for Docker 57 | # https://github.com/docker/metadata-action 58 | - name: Extract Docker metadata 59 | id: meta 60 | uses: docker/metadata-action@v5.10.0 61 | with: 62 | images: | 63 | ${{ env.IMAGE_NAME }} 64 | ghcr.io/${{ env.IMAGE_NAME }} 65 | tags: | 66 | type=ref,event=branch 67 | type=raw,value=latest,enable=${{ github.ref == format('refs/heads/{0}', github.event.repository.default_branch) }} 68 | flavor: | 69 | latest=true 70 | 71 | # Build and push Docker image with Buildx (don't push on PR) 72 | # https://github.com/docker/build-push-action 73 | - name: Build and push Docker image 74 | id: build-and-push 75 | uses: docker/build-push-action@v6.18.0 76 | with: 77 | build-args: REPO=${{ github.repository }} 78 | context: . 79 | platforms: linux/amd64,linux/arm64,linux/arm/v7,linux/arm/v6 80 | push: true 81 | sbom: true 82 | tags: ${{ steps.meta.outputs.tags }} 83 | labels: ${{ steps.meta.outputs.labels }} 84 | cache-from: type=gha 85 | cache-to: type=gha,mode=max 86 | 87 | # // SET IMAGE SIGNING ON ICE UNTIL DOCKERHUB'S KEYSTORE GETS FIXED // 88 | # Sign the resulting Docker image digest except on PRs. 89 | # This will only write to the public Rekor transparency log when the Docker 90 | # repository is public to avoid leaking data. If you would like to publish 91 | # transparency data even for private images, pass --force to cosign below. 92 | # https://github.com/sigstore/cosign 93 | # - name: Sign the published Docker image 94 | # env: 95 | # COSIGN_EXPERIMENTAL: "true" 96 | # # This step uses the identity token to provision an ephemeral certificate 97 | # # against the sigstore community Fulcio instance. 98 | # run: cosign sign ${{ env.IMAGE_NAME }}@${{ steps.build-and-push.outputs.digest }} 99 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Fritz!Box Upnp statistics exporter for prometheus 2 | 3 | This exporter exports some variables from an 4 | [AVM Fritzbox](http://avm.de/produkte/fritzbox/) 5 | to prometheus. 6 | 7 | This exporter is tested with a Fritzbox 7590 software version 07.12, 07.20, 07.21 and 07.25. 8 | 9 | The goal of the fork is: 10 | - [x] allow passing of username / password using evironment variable 11 | - [x] use https instead of http for communitcation with fritz.box 12 | - [x] move config of metrics to be exported to config file rather then code 13 | - [x] add config for additional metrics to collect (especially from TR-064 API) 14 | - [x] create a grafana dashboard consuming the additional metrics 15 | - [x] collect metrics from lua APIs not available in UPNP APIs 16 | 17 | Other changes: 18 | - replaced digest authentication code with own implementation 19 | - improved error messages 20 | - test mode prints details about all SOAP Actions and their parameters 21 | - collect option to directly test collection of results 22 | - additional metrics to collect details about connected hosts and DECT devices 23 | - support to use results like hostname or MAC address as labels to metrics 24 | - support for metrics from lua APIs (e.g. CPU temperature, utilization, ...) 25 | 26 | 27 | ## Building 28 | 29 | go install github.com/dotWee/docker-fritzbox-prometheus-exporter@latest 30 | 31 | ## Running 32 | 33 | In the configuration of the Fritzbox the option "Statusinformationen über UPnP übertragen" in the dialog "Heimnetz > 34 | Heimnetzübersicht > Netzwerkeinstellungen" has to be enabled. 35 | 36 | ### Using docker 37 | 38 | First you have to build the container: `docker build --tag fritzbox-prometheus-exporter:latest .` 39 | 40 | Then start the container: 41 | 42 | ```bash 43 | $ docker run -e 'USERNAME=your_fritzbox_username' \ 44 | -e 'PASSWORD=your_fritzbox_password' \ 45 | -e 'GATEWAY_URL="http://192.168.0.1:49000"' \ 46 | -e 'LISTEN_ADDRESS="0.0.0.0:9042"' \ 47 | fritzbox-prometheus-exporter:latest 48 | ``` 49 | 50 | I've you're getting `no such host` issues, define your FritzBox as DNS server for your docker container like this: 51 | 52 | ```bash 53 | $ docker run --dns YOUR_FRITZBOX_IP \ 54 | -e 'USERNAME=your_fritzbox_username' \ 55 | -e 'PASSWORD=your_fritzbox_password' \ 56 | -e 'GATEWAY_URL="http://192.168.0.1:49000"' \ 57 | -e 'LISTEN_ADDRESS="0.0.0.0:9042"' \ 58 | fritzbox-prometheus-exporter:latest 59 | ``` 60 | 61 | ### Using docker-compose 62 | 63 | Set your environment variables within the [docker-compose.yml](docker-compose.yml) file. 64 | 65 | Then start up the container using `docker-compose up -d`. 66 | 67 | ### Using the binary 68 | 69 | Usage: 70 | 71 | $GOPATH/bin/fritzbox_exporter -h 72 | Usage of ./fritzbox_exporter: 73 | -gateway-url string 74 | The URL of the FRITZ!Box (default "http://fritz.box:49000") 75 | -gateway-luaurl string 76 | The URL of the FRITZ!Box UI (default "http://fritz.box") 77 | -metrics-file string 78 | The JSON file with the metric definitions. (default "metrics.json") 79 | -lua-metrics-file string 80 | The JSON file with the lua metric definitions. (default "metrics-lua.json") 81 | -test 82 | print all available SOAP calls and their results (if call possible) to stdout 83 | -json-out string 84 | store metrics also to JSON file when running test 85 | -testLua 86 | read luaTest.json file make all contained calls and dump results 87 | -collect 88 | collect metrics once print to stdout and exit 89 | -nolua 90 | disable collecting lua metrics 91 | -username string 92 | The user for the FRITZ!Box UPnP service 93 | -password string 94 | The password for the FRITZ!Box UPnP service 95 | -listen-address string 96 | The address to listen on for HTTP requests. (default "127.0.0.1:9042") 97 | 98 | The password (needed for metrics from TR-064 API) can be passed over environment variables to test in shell: 99 | read -rs PASSWORD && export PASSWORD && ./fritzbox_exporter -username -test; unset PASSWORD 100 | 101 | ## Exported metrics 102 | 103 | start exporter and run 104 | curl -s http://127.0.0.1:9042/metrics 105 | 106 | ## Output of -test 107 | 108 | The exporter prints all available Variables to stdout when called with the -test option. 109 | These values are determined by parsing all services from http://fritz.box:49000/igddesc.xml and http://fritzbox:49000/tr64desc.xml (for TR64 username and password is needed!!!) 110 | 111 | ## Customizing metrics 112 | 113 | The metrics to collect are no longer hard coded, but have been moved to the [metrics.json](metrics.json) and [metrics-lua.json](metrics-lua.json) files, so just adjust to your needs (for cable version also see [metrics-lua_cable.json](metrics-lua_cable.json)). 114 | For a list of all the available metrics just execute the exporter with -test (username and password are needed for the TR-064 API!) 115 | For lua metrics open UI in browser and check the json files used for the various screens. 116 | 117 | For a list of all available metrics, see the dumps below (the format is the same as in the metrics.json file, so it can be used to easily add further metrics to retrieve): 118 | - [FritzBox 6591 v7.29](all_available_metrics_6591_7.29.json) 119 | - [FritzBox 7590 v7.12](all_available_metrics_7590_7.12.json) 120 | - [FritzBox 7590 v7.20](all_available_metrics_7590_7.20.json) 121 | - [FritzBox 7590 v7.25](all_available_metrics_7590_7.25.json) 122 | - [FritzBox 7590 v7.29](all_available_metrics_7590_7.29.json) 123 | ## Grafana Dashboard 124 | 125 | The dashboard is now also published on [Grafana](https://grafana.com/grafana/dashboards/12579). 126 | -------------------------------------------------------------------------------- /metrics-lua.json: -------------------------------------------------------------------------------- 1 | { 2 | "labelRenames": [ 3 | { 4 | "matchRegex": "(?i)prozessor", 5 | "renameLabel": "CPU" 6 | }, 7 | { 8 | "matchRegex": "(?i)system", 9 | "renameLabel": "System" 10 | }, 11 | { 12 | "matchRegex": "(?i)DSL", 13 | "renameLabel": "DSL" 14 | }, 15 | { 16 | "matchRegex": "(?i)FON", 17 | "renameLabel": "Phone" 18 | }, 19 | { 20 | "matchRegex": "(?i)WLAN", 21 | "renameLabel": "WLAN" 22 | }, 23 | { 24 | "matchRegex": "(?i)USB", 25 | "renameLabel": "USB" 26 | }, 27 | { 28 | "matchRegex": "(?i)Speicher.*FRITZ", 29 | "renameLabel": "Internal eStorage" 30 | } 31 | ], 32 | "metrics": [ 33 | { 34 | "path": "data.lua", 35 | "params": "page=energy", 36 | "resultPath": "data.drain.*", 37 | "resultKey": "actPerc", 38 | "promDesc": { 39 | "fqName": "gateway_data_energy_consumption", 40 | "help": "percentage of energy consumed from data.lua?page=energy", 41 | "varLabels": [ 42 | "gateway", "name" 43 | ] 44 | }, 45 | "promType": "GaugeValue", 46 | "cacheEntryTTL": 300 47 | }, 48 | { 49 | "path": "data.lua", 50 | "params": "page=energy", 51 | "resultPath": "data.drain.*.lan.*", 52 | "resultKey": "class", 53 | "okValue": "green", 54 | "promDesc": { 55 | "fqName": "gateway_data_energy_lan_status", 56 | "help": "status of LAN connection from data.lua?page=energy (1 = up)", 57 | "varLabels": [ 58 | "gateway", "name" 59 | ] 60 | }, 61 | "promType": "GaugeValue", 62 | "cacheEntryTTL": 300 63 | }, 64 | { 65 | "path": "data.lua", 66 | "params": "page=ecoStat", 67 | "resultPath": "data.cputemp.series.0", 68 | "resultKey": "-1", 69 | "promDesc": { 70 | "fqName": "gateway_data_ecostat_cputemp", 71 | "help": "cpu temperature from data.lua?page=ecoStat", 72 | "varLabels": [ 73 | "gateway" 74 | ] 75 | }, 76 | "promType": "GaugeValue", 77 | "cacheEntryTTL": 300 78 | }, 79 | { 80 | "path": "data.lua", 81 | "params": "page=ecoStat", 82 | "resultPath": "data.cpuutil.series.0", 83 | "resultKey": "-1", 84 | "promDesc": { 85 | "fqName": "gateway_data_ecostat_cpuutil", 86 | "help": "percentage of cpu utilization from data.lua?page=ecoStat", 87 | "varLabels": [ 88 | "gateway" 89 | ] 90 | }, 91 | "promType": "GaugeValue", 92 | "cacheEntryTTL": 300 93 | }, 94 | { 95 | "path": "data.lua", 96 | "params": "page=ecoStat", 97 | "resultPath": "data.ramusage.series.0", 98 | "resultKey": "-1", 99 | "promDesc": { 100 | "fqName": "gateway_data_ecostat_ramusage", 101 | "help": "percentage of RAM utilization from data.lua?page=energy", 102 | "varLabels": [ 103 | "gateway" 104 | ], 105 | "fixedLabels": { 106 | "ram_type" : "Fixed" 107 | } 108 | }, 109 | "promType": "GaugeValue", 110 | "cacheEntryTTL": 300 111 | }, 112 | { 113 | "path": "data.lua", 114 | "params": "page=ecoStat", 115 | "resultPath": "data.ramusage.series.1", 116 | "resultKey": "-1", 117 | "promDesc": { 118 | "fqName": "gateway_data_ecostat_ramusage", 119 | "help": "percentage of RAM utilization from data.lua?page=energy", 120 | "varLabels": [ 121 | "gateway" 122 | ], 123 | "fixedLabels": { 124 | "ram_type" : "Dynamic" 125 | } 126 | }, 127 | "promType": "GaugeValue", 128 | "cacheEntryTTL": 300 129 | }, 130 | { 131 | "path": "data.lua", 132 | "params": "page=ecoStat", 133 | "resultPath": "data.ramusage.series.2", 134 | "resultKey": "-1", 135 | "promDesc": { 136 | "fqName": "gateway_data_ecostat_ramusage", 137 | "help": "percentage of RAM utilization from data.lua?page=energy", 138 | "varLabels": [ 139 | "gateway" 140 | ], 141 | "fixedLabels": { 142 | "ram_type" : "Free" 143 | } 144 | }, 145 | "promType": "GaugeValue", 146 | "cacheEntryTTL": 300 147 | }, 148 | { 149 | "path": "data.lua", 150 | "params": "page=usbOv", 151 | "resultPath": "data.usbOverview.devices.*", 152 | "resultKey": "partitions.0.totalStorageInBytes", 153 | "promDesc": { 154 | "fqName": "gateway_data_usb_storage_total", 155 | "help": "total storage in bytes from data.lua?page=usbOv", 156 | "varLabels": [ 157 | "gateway", "deviceType", "deviceName" 158 | ] 159 | }, 160 | "promType": "GaugeValue", 161 | "cacheEntryTTL": 300 162 | }, 163 | { 164 | "path": "data.lua", 165 | "params": "page=usbOv", 166 | "resultPath": "data.usbOverview.devices.*", 167 | "resultKey": "partitions.0.usedStorageInBytes", 168 | "promDesc": { 169 | "fqName": "gateway_data_usb_storage_used", 170 | "help": "used storage in bytes from data.lua?page=usbOv", 171 | "varLabels": [ 172 | "gateway", "deviceType", "deviceName" 173 | ] 174 | }, 175 | "promType": "GaugeValue", 176 | "cacheEntryTTL": 300 177 | } 178 | ] 179 | } 180 | 181 | -------------------------------------------------------------------------------- /metrics-lua_cable.json: -------------------------------------------------------------------------------- 1 | { 2 | "labelRenames": [ 3 | { 4 | "matchRegex": "64QAM", 5 | "renameLabel": "64" 6 | }, 7 | { 8 | "matchRegex": "16QAM", 9 | "renameLabel": "16" 10 | }, 11 | { 12 | "matchRegex": "(?i)prozessor", 13 | "renameLabel": "CPU" 14 | }, 15 | { 16 | "matchRegex": "(?i)system", 17 | "renameLabel": "System" 18 | }, 19 | { 20 | "matchRegex": "(?i)DSL", 21 | "renameLabel": "DSL" 22 | }, 23 | { 24 | "matchRegex": "(?i)FON", 25 | "renameLabel": "Phone" 26 | }, 27 | { 28 | "matchRegex": "(?i)WLAN", 29 | "renameLabel": "WLAN" 30 | }, 31 | { 32 | "matchRegex": "(?i)USB", 33 | "renameLabel": "USB" 34 | }, 35 | { 36 | "matchRegex": "(?i)Speicher.*FRITZ", 37 | "renameLabel": "Internal eStorage" 38 | } 39 | ], 40 | "metrics": [ 41 | { 42 | "path": "data.lua", 43 | "params": "page=docInfo", 44 | "resultPath": "data.channelUs.docsis30.*", 45 | "resultKey": "powerLevel", 46 | "promDesc": { 47 | "fqName": "gateway_cable_power_upstream", 48 | "help": "docsis 3.0 power upstream from data.lua?page=docInfo", 49 | "varLabels": [ 50 | "gateway", "frequency" 51 | ] 52 | }, 53 | "promType": "GaugeValue", 54 | "cacheEntryTTL": 300 55 | }, 56 | { 57 | "path": "data.lua", 58 | "params": "page=docInfo", 59 | "resultPath": "data.channelUs.docsis30.*", 60 | "resultKey": "type", 61 | "promDesc": { 62 | "fqName": "gateway_cable_modulation_upstream", 63 | "help": "docsis 3.0 modulation upstream from data.lua?page=docInfo", 64 | "varLabels": [ 65 | "gateway", "frequency" 66 | ] 67 | }, 68 | "promType": "GaugeValue", 69 | "cacheEntryTTL": 300 70 | }, 71 | { 72 | "path": "data.lua", 73 | "params": "page=docInfo", 74 | "resultPath": "data.channelUs.docsis31.*", 75 | "resultKey": "powerLevel", 76 | "promDesc": { 77 | "fqName": "gateway_cable_power_upstream31", 78 | "help": "docsis 3.1 power upstream from data.lua?page=docInfo", 79 | "varLabels": [ 80 | "gateway", "frequency" 81 | ] 82 | }, 83 | "promType": "GaugeValue", 84 | "cacheEntryTTL": 300 85 | }, 86 | { 87 | "path": "data.lua", 88 | "params": "page=docInfo", 89 | "resultPath": "data.channelDs.docsis30.*", 90 | "resultKey": "corrErrors", 91 | "promDesc": { 92 | "fqName": "gateway_cable_correctables_downstream", 93 | "help": "docsis 3.0 correctable errors dpwnstream from data.lua?page=docInfo", 94 | "varLabels": [ 95 | "gateway", "frequency" 96 | ] 97 | }, 98 | "promType": "GaugeValue", 99 | "cacheEntryTTL": 300 100 | }, 101 | { 102 | "path": "data.lua", 103 | "params": "page=docInfo", 104 | "resultPath": "data.channelDs.docsis30.*", 105 | "resultKey": "nonCorrErrors", 106 | "promDesc": { 107 | "fqName": "gateway_cable_uncorrectables_downstream", 108 | "help": "docsis 3.0 uncorrectable errors downstream from data.lua?page=docInfo", 109 | "varLabels": [ 110 | "gateway", "frequency" 111 | ] 112 | }, 113 | "promType": "GaugeValue", 114 | "cacheEntryTTL": 300 115 | }, 116 | { 117 | "path": "data.lua", 118 | "params": "page=docInfo", 119 | "resultPath": "data.channelDs.docsis30.*", 120 | "resultKey": "mse", 121 | "promDesc": { 122 | "fqName": "gateway_cable_mse_downstream", 123 | "help": "docsis 3.0 mse downstream from data.lua?page=docInfo", 124 | "varLabels": [ 125 | "gateway", "frequency" 126 | ] 127 | }, 128 | "promType": "GaugeValue", 129 | "cacheEntryTTL": 300 130 | }, 131 | { 132 | "path": "data.lua", 133 | "params": "page=docInfo", 134 | "resultPath": "data.channelDs.docsis30.*", 135 | "resultKey": "powerLevel", 136 | "promDesc": { 137 | "fqName": "gateway_cable_power_downstream", 138 | "help": "docsis 3.0 powerlevel downstream from data.lua?page=docInfo", 139 | "varLabels": [ 140 | "gateway", "frequency" 141 | ] 142 | }, 143 | "promType": "GaugeValue", 144 | "cacheEntryTTL": 300 145 | }, 146 | { 147 | "path": "data.lua", 148 | "params": "page=docInfo", 149 | "resultPath": "data.channelDs.docsis31.*", 150 | "resultKey": "powerLevel", 151 | "promDesc": { 152 | "fqName": "gateway_cable_power_downstream31", 153 | "help": "docsis 3.1 powerlevel downstream from data.lua?page=docInfo", 154 | "varLabels": [ 155 | "gateway", "frequency" 156 | ] 157 | }, 158 | "promType": "GaugeValue", 159 | "cacheEntryTTL": 300 160 | }, 161 | { 162 | "path": "data.lua", 163 | "params": "page=energy", 164 | "resultPath": "data.drain.*", 165 | "resultKey": "actPerc", 166 | "promDesc": { 167 | "fqName": "gateway_data_energy_consumption", 168 | "help": "percentage of energy consumed from data.lua?page=energy", 169 | "varLabels": [ 170 | "gateway", "name" 171 | ] 172 | }, 173 | "promType": "GaugeValue", 174 | "cacheEntryTTL": 300 175 | }, 176 | { 177 | "path": "data.lua", 178 | "params": "page=energy", 179 | "resultPath": "data.drain.*.lan.*", 180 | "resultKey": "class", 181 | "okValue": "green", 182 | "promDesc": { 183 | "fqName": "gateway_data_energy_lan_status", 184 | "help": "status of LAN connection from data.lua?page=energy (1 = up)", 185 | "varLabels": [ 186 | "gateway", "name" 187 | ] 188 | }, 189 | "promType": "GaugeValue", 190 | "cacheEntryTTL": 300 191 | }, 192 | { 193 | "path": "data.lua", 194 | "params": "page=ecoStat", 195 | "resultPath": "data.cputemp.series.0", 196 | "resultKey": "-1", 197 | "promDesc": { 198 | "fqName": "gateway_data_ecostat_cputemp", 199 | "help": "cpu temperature from data.lua?page=ecoStat", 200 | "varLabels": [ 201 | "gateway" 202 | ] 203 | }, 204 | "promType": "GaugeValue", 205 | "cacheEntryTTL": 300 206 | }, 207 | { 208 | "path": "data.lua", 209 | "params": "page=ecoStat", 210 | "resultPath": "data.cpuutil.series.0", 211 | "resultKey": "-1", 212 | "promDesc": { 213 | "fqName": "gateway_data_ecostat_cpuutil", 214 | "help": "percentage of cpu utilization from data.lua?page=ecoStat", 215 | "varLabels": [ 216 | "gateway" 217 | ] 218 | }, 219 | "promType": "GaugeValue", 220 | "cacheEntryTTL": 300 221 | }, 222 | { 223 | "path": "data.lua", 224 | "params": "page=ecoStat", 225 | "resultPath": "data.ramusage.series.0", 226 | "resultKey": "-1", 227 | "promDesc": { 228 | "fqName": "gateway_data_ecostat_ramusage", 229 | "help": "percentage of RAM utilization from data.lua?page=energy", 230 | "varLabels": [ 231 | "gateway" 232 | ], 233 | "fixedLabels": { 234 | "ram_type" : "Fixed" 235 | } 236 | }, 237 | "promType": "GaugeValue", 238 | "cacheEntryTTL": 300 239 | }, 240 | { 241 | "path": "data.lua", 242 | "params": "page=ecoStat", 243 | "resultPath": "data.ramusage.series.1", 244 | "resultKey": "-1", 245 | "promDesc": { 246 | "fqName": "gateway_data_ecostat_ramusage", 247 | "help": "percentage of RAM utilization from data.lua?page=energy", 248 | "varLabels": [ 249 | "gateway" 250 | ], 251 | "fixedLabels": { 252 | "ram_type" : "Dynamic" 253 | } 254 | }, 255 | "promType": "GaugeValue", 256 | "cacheEntryTTL": 300 257 | }, 258 | { 259 | "path": "data.lua", 260 | "params": "page=ecoStat", 261 | "resultPath": "data.ramusage.series.2", 262 | "resultKey": "-1", 263 | "promDesc": { 264 | "fqName": "gateway_data_ecostat_ramusage", 265 | "help": "percentage of RAM utilization from data.lua?page=energy", 266 | "varLabels": [ 267 | "gateway" 268 | ], 269 | "fixedLabels": { 270 | "ram_type" : "Free" 271 | } 272 | }, 273 | "promType": "GaugeValue", 274 | "cacheEntryTTL": 300 275 | }, 276 | { 277 | "path": "data.lua", 278 | "params": "page=usbOv", 279 | "resultPath": "data.usbOverview.devices.*", 280 | "resultKey": "partitions.0.totalStorageInBytes", 281 | "promDesc": { 282 | "fqName": "gateway_data_usb_storage_total", 283 | "help": "total storage in bytes from data.lua?page=usbOv", 284 | "varLabels": [ 285 | "gateway", "deviceType", "deviceName" 286 | ] 287 | }, 288 | "promType": "GaugeValue", 289 | "cacheEntryTTL": 300 290 | }, 291 | { 292 | "path": "data.lua", 293 | "params": "page=usbOv", 294 | "resultPath": "data.usbOverview.devices.*", 295 | "resultKey": "partitions.0.usedStorageInBytes", 296 | "promDesc": { 297 | "fqName": "gateway_data_usb_storage_used", 298 | "help": "used storage in bytes from data.lua?page=usbOv", 299 | "varLabels": [ 300 | "gateway", "deviceType", "deviceName" 301 | ] 302 | }, 303 | "promType": "GaugeValue", 304 | "cacheEntryTTL": 300 305 | } 306 | ] 307 | } 308 | 309 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /fritzbox_lua/lua_client.go: -------------------------------------------------------------------------------- 1 | // Package lua_client implementes client for fritzbox lua UI API 2 | package lua_client 3 | 4 | // Copyright 2020 Andreas Krebs 5 | // 6 | // Licensed under the Apache License, Version 2.0 (the "License"); 7 | // you may not use this file except in compliance with the License. 8 | // You may obtain a copy of the License at 9 | // 10 | // http://www.apache.org/licenses/LICENSE-2.0 11 | // 12 | // Unless required by applicable law or agreed to in writing, software 13 | // distributed under the License is distributed on an "AS IS" BASIS, 14 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | // See the License for the specific language governing permissions and 16 | // limitations under the License. 17 | 18 | import ( 19 | "bytes" 20 | "crypto/md5" 21 | "encoding/json" 22 | "encoding/xml" 23 | "errors" 24 | "fmt" 25 | "io/ioutil" 26 | "net/http" 27 | "regexp" 28 | "strconv" 29 | "strings" 30 | 31 | "golang.org/x/text/encoding/unicode" 32 | "golang.org/x/text/transform" 33 | ) 34 | 35 | // SessionInfo XML from login_sid.lua 36 | type SessionInfo struct { 37 | SID string `xml:"SID"` 38 | Challenge string `xml:"Challenge"` 39 | BlockTime int `xml:"BlockTime"` 40 | Rights string `xml:"Rights"` 41 | } 42 | 43 | // LuaSession for storing connection data and SID 44 | type LuaSession struct { 45 | BaseURL string 46 | Username string 47 | Password string 48 | SID string 49 | SessionInfo SessionInfo 50 | } 51 | 52 | // LuaPage identified by path and params 53 | type LuaPage struct { 54 | Path string 55 | Params string 56 | } 57 | 58 | // LuaMetricValueDefinition definition for a single metric 59 | type LuaMetricValueDefinition struct { 60 | Path string 61 | Key string 62 | OkValue string 63 | Labels []string 64 | } 65 | 66 | // LuaMetricValue single value retrieved from lua page 67 | type LuaMetricValue struct { 68 | Name string 69 | Value float64 70 | Labels map[string]string 71 | } 72 | 73 | // LabelRename regex to replace labels to get rid of translations 74 | type LabelRename struct { 75 | Pattern regexp.Regexp 76 | Name string 77 | } 78 | 79 | // regex to remove leading/training characters from numbers 80 | var ( 81 | regexNonNumberEnd = regexp.MustCompile("\\D+$") 82 | ) 83 | 84 | func (lua *LuaSession) doLogin(response string) error { 85 | urlParams := "" 86 | if response != "" { 87 | urlParams = fmt.Sprintf("?response=%s&user=%s", response, lua.Username) 88 | } 89 | 90 | resp, err := http.Get(fmt.Sprintf("%s/login_sid.lua%s", lua.BaseURL, urlParams)) 91 | if err != nil { 92 | return fmt.Errorf("Error calling login_sid.lua: %s", err.Error()) 93 | } 94 | 95 | defer resp.Body.Close() 96 | dec := xml.NewDecoder(resp.Body) 97 | 98 | err = dec.Decode(&lua.SessionInfo) 99 | if err != nil { 100 | return fmt.Errorf("Error decoding SessionInfo: %s", err.Error()) 101 | } 102 | 103 | if lua.SessionInfo.BlockTime > 0 { 104 | return fmt.Errorf("To many failed logins, login blocked for %d seconds", lua.SessionInfo.BlockTime) 105 | } 106 | 107 | return nil 108 | } 109 | 110 | func (lmvDef *LuaMetricValueDefinition) createValue(name string, value float64) LuaMetricValue { 111 | lmv := LuaMetricValue{ 112 | Name: name, 113 | Value: value, 114 | Labels: make(map[string]string), 115 | } 116 | 117 | return lmv 118 | } 119 | 120 | // Login perform loing and get SID 121 | func (lua *LuaSession) Login() error { 122 | 123 | err := lua.doLogin("") 124 | if err != nil { 125 | return err 126 | } 127 | 128 | challenge := lua.SessionInfo.Challenge 129 | if lua.SessionInfo.SID == "0000000000000000" && challenge != "" { 130 | // no SID, but challenge so calc response 131 | hash := utf16leMd5(fmt.Sprintf("%s-%s", challenge, lua.Password)) 132 | response := fmt.Sprintf("%s-%x", challenge, hash) 133 | err := lua.doLogin(response) 134 | 135 | if err != nil { 136 | return err 137 | } 138 | } 139 | 140 | sid := lua.SessionInfo.SID 141 | if sid == "0000000000000000" || sid == "" { 142 | return errors.New("LUA login failed - no SID received - check username and password") 143 | } 144 | 145 | lua.SID = sid 146 | 147 | return nil 148 | } 149 | 150 | // LoadData load a lua bage and return content 151 | func (lua *LuaSession) LoadData(page LuaPage) ([]byte, error) { 152 | method := "POST" 153 | path := page.Path 154 | 155 | // handle method prefix 156 | pathParts := strings.SplitN(path, ":", 2) 157 | if len(pathParts) > 1 { 158 | method = pathParts[0] 159 | path = pathParts[1] 160 | } 161 | 162 | dataURL := fmt.Sprintf("%s/%s", lua.BaseURL, path) 163 | 164 | callDone := false 165 | var resp *http.Response 166 | var err error 167 | for !callDone { 168 | // perform login if no SID or previous call failed with (403) 169 | if lua.SID == "" || resp != nil { 170 | err = lua.Login() 171 | callDone = true // consider call done, since we tried login 172 | 173 | if err != nil { 174 | return nil, err 175 | } 176 | } 177 | 178 | // send by UI for data.lua: xhr=1&sid=xxxxxxx&lang=de&page=energy&xhrId=all&no_sidrenew= 179 | // but SID and page seem to be enough 180 | params := "sid=" + lua.SID 181 | if page.Params != "" { 182 | params += "&" + page.Params 183 | } 184 | 185 | if method == "POST" { 186 | resp, err = http.Post(dataURL, "application/x-www-form-urlencoded", bytes.NewBuffer([]byte(params))) 187 | } else if method == "GET" { 188 | resp, err = http.Get(dataURL + "?" + params) 189 | } else { 190 | err = fmt.Errorf("method %s is unsupported in path %s", method, page.Path) 191 | } 192 | 193 | if err != nil { 194 | return nil, err 195 | } 196 | defer resp.Body.Close() 197 | 198 | if resp.StatusCode == http.StatusOK { 199 | callDone = true 200 | } else if resp.StatusCode == http.StatusForbidden && !callDone { 201 | // we assume SID is expired, so retry login 202 | } else { 203 | return nil, fmt.Errorf("%s failed: %s", page.Path, resp.Status) 204 | } 205 | } 206 | 207 | body, err := ioutil.ReadAll(resp.Body) 208 | 209 | if err != nil { 210 | return nil, err 211 | } 212 | 213 | return body, nil 214 | } 215 | 216 | // ParseJSON generic parser for unmarshalling into map 217 | func ParseJSON(jsonData []byte) (map[string]interface{}, error) { 218 | var data map[string]interface{} 219 | 220 | // Unmarshal or Decode the JSON to the interface. 221 | json.Unmarshal(jsonData, &data) 222 | 223 | return data, nil 224 | } 225 | 226 | func getRenamedLabel(labelRenames *[]LabelRename, label string) string { 227 | if labelRenames != nil { 228 | for _, lblRen := range *labelRenames { 229 | if lblRen.Pattern.MatchString(label) { 230 | return lblRen.Name 231 | } 232 | } 233 | } 234 | 235 | return label 236 | } 237 | 238 | func getValueFromHashOrArray(mapOrArray interface{}, key string, path string) (interface{}, error) { 239 | var value interface{} 240 | 241 | switch moa := mapOrArray.(type) { 242 | case map[string]interface{}: 243 | var exists bool 244 | value, exists = moa[key] 245 | if !exists { 246 | return nil, fmt.Errorf("hash '%s' has no element '%s'", path, key) 247 | } 248 | case []interface{}: 249 | // since type is array there can't be any labels to differentiate values, so only one value supported ! 250 | index, err := strconv.Atoi(key) 251 | if err != nil { 252 | return nil, fmt.Errorf("item '%s' is an array, but index '%s' is not a number", path, key) 253 | } 254 | 255 | if index < 0 { 256 | // this is an index from the end of the values 257 | index += len(moa) 258 | } 259 | 260 | if index < 0 || index >= len(moa) { 261 | return nil, fmt.Errorf("index %d is invalid for array '%s' with length %d", index, path, len(moa)) 262 | } 263 | value = moa[index] 264 | default: 265 | return nil, fmt.Errorf("item '%s' is not a hash or array, can't get value %s", path, key) 266 | } 267 | 268 | return value, nil 269 | } 270 | 271 | // GetMetrics get metrics from parsed lua page for definition and rename labels 272 | func GetMetrics(labelRenames *[]LabelRename, data map[string]interface{}, metricDef LuaMetricValueDefinition) ([]LuaMetricValue, error) { 273 | 274 | var values []interface{} 275 | var err error 276 | if metricDef.Path != "" { 277 | pathItems := strings.Split(metricDef.Path, ".") 278 | values, err = _getValues(data, pathItems, "") 279 | if err != nil { 280 | return nil, err 281 | } 282 | } else { 283 | values = make([]interface{}, 1) 284 | values[0] = data 285 | } 286 | 287 | metrics := make([]LuaMetricValue, 0) 288 | keyItems := strings.Split(metricDef.Key, ".") 289 | 290 | VALUE: 291 | for _, pathVal := range values { 292 | valUntyped := pathVal 293 | path := metricDef.Path 294 | 295 | // now handle if key is also splitted 296 | for _, key := range keyItems { 297 | valUntyped, err = getValueFromHashOrArray(valUntyped, key, path) 298 | if err != nil { 299 | // since we may have other values, we simply continue (should we report it?) 300 | continue VALUE 301 | } 302 | 303 | if path != "" { 304 | path += "." 305 | } 306 | path += key 307 | } 308 | 309 | var sVal = toString(valUntyped) 310 | var floatVal float64 311 | if metricDef.OkValue != "" { 312 | if metricDef.OkValue == sVal { 313 | floatVal = 1 314 | } else { 315 | floatVal = 0 316 | } 317 | } else { 318 | // convert value to float, but first remove all non numbers from begin or end of value 319 | // needed if value contains unit 320 | sNum := regexNonNumberEnd.ReplaceAllString(sVal, "") 321 | 322 | floatVal, err = strconv.ParseFloat(sNum, 64) 323 | if err != nil { 324 | continue VALUE 325 | } 326 | } 327 | 328 | // create metric value 329 | lmv := metricDef.createValue(path, floatVal) 330 | 331 | // add labels if pathVal is a hash 332 | valMap, isType := pathVal.(map[string]interface{}) 333 | if isType { 334 | for _, l := range metricDef.Labels { 335 | lv, exists := valMap[l] 336 | if exists { 337 | lmv.Labels[l] = getRenamedLabel(labelRenames, toString(lv)) 338 | } 339 | } 340 | } 341 | 342 | metrics = append(metrics, lmv) 343 | } 344 | 345 | if len(metrics) == 0 { 346 | if err == nil { 347 | // normal we should already have an error, this is just a fallback 348 | err = fmt.Errorf("no value found for item '%s' with key '%s'", metricDef.Path, metricDef.Key) 349 | } 350 | return nil, err 351 | } 352 | 353 | return metrics, nil 354 | } 355 | 356 | // from https://stackoverflow.com/questions/33710672/golang-encode-string-utf16-little-endian-and-hash-with-md5 357 | func utf16leMd5(s string) []byte { 358 | enc := unicode.UTF16(unicode.LittleEndian, unicode.IgnoreBOM).NewEncoder() 359 | hasher := md5.New() 360 | t := transform.NewWriter(hasher, enc) 361 | t.Write([]byte(s)) 362 | return hasher.Sum(nil) 363 | } 364 | 365 | // helper for retrieving values from parsed JSON 366 | func _getValues(data interface{}, pathItems []string, parentPath string) ([]interface{}, error) { 367 | 368 | var err error 369 | values := make([]interface{}, 0) 370 | value := data 371 | curPath := parentPath 372 | 373 | for i, p := range pathItems { 374 | if p == "*" { 375 | // handle * case to get all values 376 | var subvals []interface{} 377 | switch vv := value.(type) { 378 | case []interface{}: 379 | for index, u := range vv { 380 | subvals, err = _getValues(u, pathItems[i+1:], fmt.Sprintf("%s.%d", curPath, index)) 381 | 382 | if subvals != nil { 383 | values = append(values, subvals...) 384 | } 385 | } 386 | case map[string]interface{}: 387 | for subK, subV := range vv { 388 | subvals, err = _getValues(subV, pathItems[i+1:], fmt.Sprintf("%s.%s", curPath, subK)) 389 | 390 | if subvals != nil { 391 | values = append(values, subvals...) 392 | } 393 | } 394 | default: 395 | err = fmt.Errorf("item '%s' is neither a hash or array", curPath) 396 | } 397 | 398 | if len(values) == 0 { 399 | if err == nil { 400 | err = fmt.Errorf("item '%s.*' has no values", curPath) 401 | } 402 | 403 | return nil, err 404 | } 405 | 406 | return values, nil 407 | } 408 | 409 | // this is a single value 410 | value, err = getValueFromHashOrArray(value, p, curPath) 411 | if err != nil { 412 | return nil, err 413 | } 414 | 415 | if curPath == "" { 416 | curPath = p 417 | } else { 418 | curPath += "." + p 419 | } 420 | } 421 | 422 | values = append(values, value) 423 | 424 | return values, nil 425 | } 426 | 427 | func toString(value interface{}) string { 428 | // should we better check or simple convert everything ???? 429 | return fmt.Sprintf("%v", value) 430 | } 431 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 2 | github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= 3 | github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= 4 | github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= 5 | github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= 6 | github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= 7 | github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= 8 | github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= 9 | github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= 10 | github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= 11 | github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY= 12 | github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 13 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 14 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 15 | github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= 16 | github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= 17 | github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= 18 | github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= 19 | github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= 20 | github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= 21 | github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= 22 | github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= 23 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 24 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 25 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 26 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= 27 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= 28 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= 29 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= 30 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= 31 | github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 32 | github.com/golang/protobuf v1.4.3 h1:JjCZWpVbqXDqFVmTfYWEVTMIYrL/NPdPSCHPJ0T/raM= 33 | github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 34 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 35 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 36 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 37 | github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 38 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 39 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 40 | github.com/heptiolabs/healthcheck v0.0.0-20211123025425-613501dd5deb h1:tsEKRC3PU9rMw18w/uAptoijhgG4EvlA5kfJPtwrMDk= 41 | github.com/heptiolabs/healthcheck v0.0.0-20211123025425-613501dd5deb/go.mod h1:NtmN9h8vrTveVQRLHcX2HQ5wIPBDCsZ351TGbZWgg38= 42 | github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= 43 | github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= 44 | github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= 45 | github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= 46 | github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= 47 | github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= 48 | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 49 | github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 50 | github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= 51 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 52 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 53 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 54 | github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= 55 | github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= 56 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 57 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 58 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 59 | github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 60 | github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= 61 | github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= 62 | github.com/namsral/flag v1.7.4-pre h1:b2ScHhoCUkbsq0d2C15Mv+VU8bl8hAXV8arnWiOHNZs= 63 | github.com/namsral/flag v1.7.4-pre/go.mod h1:OXldTctbM6SWH1K899kPZcf65KxJiD7MsceFUpB5yDo= 64 | github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 65 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 66 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 67 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 68 | github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= 69 | github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= 70 | github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= 71 | github.com/prometheus/client_golang v1.11.0 h1:HNkLOAEQMIDv/K+04rukrLx6ch7msSRwf3/SASFAGtQ= 72 | github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= 73 | github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= 74 | github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 75 | github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M= 76 | github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 77 | github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= 78 | github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= 79 | github.com/prometheus/common v0.26.0 h1:iMAkS2TDoNWnKM+Kopnx/8tnEStIfpYA0ur0xQzzhMQ= 80 | github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= 81 | github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= 82 | github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= 83 | github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= 84 | github.com/prometheus/procfs v0.6.0 h1:mxy4L2jP6qMonqmq+aTtOx1ifVWUgG/TAmntgbh3xv4= 85 | github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= 86 | github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= 87 | github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= 88 | github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= 89 | github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= 90 | github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= 91 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 92 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 93 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 94 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 95 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 96 | golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 97 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 98 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 99 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 100 | golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 101 | golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 102 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 103 | golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 104 | golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= 105 | golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 106 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 107 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 108 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 109 | golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 110 | golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 111 | golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 112 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 113 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 114 | golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 115 | golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 116 | golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 117 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 118 | golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 119 | golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 120 | golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 121 | golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40 h1:JWgyZ1qgdTaF3N3oxC+MdTV7qvEEgHo3otj+HB5CM7Q= 122 | golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 123 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 124 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 125 | golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= 126 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 127 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 128 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 129 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 130 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= 131 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 132 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= 133 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= 134 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= 135 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 136 | google.golang.org/protobuf v1.26.0-rc.1 h1:7QnIQpGRHE5RnLKnESfDoxm2dTapTZua5a0kS0A+VXQ= 137 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= 138 | gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= 139 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 140 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 141 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 142 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 143 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 144 | gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 145 | gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 146 | -------------------------------------------------------------------------------- /metrics.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "service": "urn:schemas-upnp-org:service:WANCommonInterfaceConfig:1", 4 | "action": "GetTotalPacketsReceived", 5 | "result": "TotalPacketsReceived", 6 | "promDesc": { 7 | "fqName": "gateway_traffic", 8 | "help": "traffic on gateway interface", 9 | "varLabels": [ 10 | "gateway" 11 | ], 12 | "fixedLabels": { 13 | "direction": "Received", 14 | "unit": "Packets", 15 | "interface": "WAN" 16 | } 17 | }, 18 | "promType": "CounterValue" 19 | }, 20 | { 21 | "service": "urn:schemas-upnp-org:service:WANCommonInterfaceConfig:1", 22 | "action": "GetTotalPacketsSent", 23 | "result": "TotalPacketsSent", 24 | "promDesc": { 25 | "fqName": "gateway_traffic", 26 | "help": "traffic on gateway interface", 27 | "varLabels": [ 28 | "gateway" 29 | ], 30 | "fixedLabels": { 31 | "direction": "Sent", 32 | "unit": "Packets", 33 | "interface": "WAN" 34 | } 35 | }, 36 | "promType": "CounterValue" 37 | }, 38 | { 39 | "service": "urn:schemas-upnp-org:service:WANCommonInterfaceConfig:1", 40 | "action": "GetAddonInfos", 41 | "result": "TotalBytesReceived", 42 | "promDesc": { 43 | "fqName": "gateway_traffic", 44 | "help": "traffic on gateway interface", 45 | "varLabels": [ 46 | "gateway" 47 | ], 48 | "fixedLabels": { 49 | "direction": "Received", 50 | "unit": "Bytes", 51 | "interface": "WAN" 52 | } 53 | }, 54 | "promType": "CounterValue" 55 | }, 56 | { 57 | "service": "urn:schemas-upnp-org:service:WANCommonInterfaceConfig:1", 58 | "action": "GetAddonInfos", 59 | "result": "TotalBytesSent", 60 | "promDesc": { 61 | "fqName": "gateway_traffic", 62 | "help": "traffic on gateway interface", 63 | "varLabels": [ 64 | "gateway" 65 | ], 66 | "fixedLabels": { 67 | "direction": "Sent", 68 | "unit": "Bytes", 69 | "interface": "WAN" 70 | } 71 | }, 72 | "promType": "CounterValue" 73 | }, 74 | { 75 | "service": "urn:schemas-upnp-org:service:WANCommonInterfaceConfig:1", 76 | "action": "GetAddonInfos", 77 | "result": "ByteSendRate", 78 | "promDesc": { 79 | "fqName": "gateway_traffic_rate", 80 | "help": "traffic rate on gateway interface", 81 | "varLabels": [ 82 | "gateway" 83 | ], 84 | "fixedLabels": { 85 | "direction": "Sent", 86 | "unit": "Bytes", 87 | "interface": "WAN" 88 | } 89 | }, 90 | "promType": "GaugeValue" 91 | }, 92 | { 93 | "service": "urn:schemas-upnp-org:service:WANCommonInterfaceConfig:1", 94 | "action": "GetAddonInfos", 95 | "result": "ByteReceiveRate", 96 | "promDesc": { 97 | "fqName": "gateway_traffic_rate", 98 | "help": "traffic rate on gateway interface", 99 | "varLabels": [ 100 | "gateway" 101 | ], 102 | "fixedLabels": { 103 | "direction": "Received", 104 | "unit": "Bytes", 105 | "interface": "WAN" 106 | } 107 | }, 108 | "promType": "GaugeValue" 109 | }, 110 | { 111 | "service": "urn:schemas-upnp-org:service:WANCommonInterfaceConfig:1", 112 | "action": "GetCommonLinkProperties", 113 | "result": "Layer1UpstreamMaxBitRate", 114 | "promDesc": { 115 | "fqName": "gateway_max_bitrate", 116 | "help": "max bitrate on gateway interface", 117 | "varLabels": [ 118 | "gateway" 119 | ], 120 | "fixedLabels": { 121 | "direction": "Up", 122 | "interface": "WAN" 123 | } 124 | }, 125 | "promType": "GaugeValue", 126 | "cacheEntryTTL": 60 127 | }, 128 | { 129 | "service": "urn:schemas-upnp-org:service:WANCommonInterfaceConfig:1", 130 | "action": "GetCommonLinkProperties", 131 | "result": "Layer1DownstreamMaxBitRate", 132 | "promDesc": { 133 | "fqName": "gateway_max_bitrate", 134 | "help": "max bitrate on gateway interface", 135 | "varLabels": [ 136 | "gateway" 137 | ], 138 | "fixedLabels": { 139 | "direction": "Down", 140 | "interface": "WAN" 141 | } 142 | }, 143 | "promType": "GaugeValue", 144 | "cacheEntryTTL": 60 145 | }, 146 | { 147 | "service": "urn:schemas-upnp-org:service:WANCommonInterfaceConfig:1", 148 | "action": "GetCommonLinkProperties", 149 | "result": "PhysicalLinkStatus", 150 | "okValue": "Up", 151 | "promDesc": { 152 | "fqName": "gateway_connection_status", 153 | "help": "Connection status (Connected = 1)", 154 | "varLabels": [ 155 | "gateway" 156 | ], 157 | "fixedLabels": { 158 | "connection": "Physical Link", 159 | "interface": "WAN" 160 | } 161 | }, 162 | "promType": "GaugeValue", 163 | "cacheEntryTTL": 60 164 | }, 165 | { 166 | "service": "urn:dslforum-org:service:WANCommonInterfaceConfig:1", 167 | "action": "GetTotalPacketsReceived", 168 | "result": "TotalPacketsReceived", 169 | "promDesc": { 170 | "fqName": "gateway_traffic", 171 | "help": "traffic on gateway interface", 172 | "varLabels": [ 173 | "gateway" 174 | ], 175 | "fixedLabels": { 176 | "direction": "Received", 177 | "unit": "Packets", 178 | "interface": "DSL" 179 | } 180 | }, 181 | "promType": "CounterValue" 182 | }, 183 | { 184 | "service": "urn:dslforum-org:service:WANCommonInterfaceConfig:1", 185 | "action": "GetTotalPacketsSent", 186 | "result": "TotalPacketsSent", 187 | "promDesc": { 188 | "fqName": "gateway_traffic", 189 | "help": "traffic on gateway interface", 190 | "varLabels": [ 191 | "gateway" 192 | ], 193 | "fixedLabels": { 194 | "direction": "Sent", 195 | "unit": "Packets", 196 | "interface": "DSL" 197 | } 198 | }, 199 | "promType": "CounterValue" 200 | }, 201 | { 202 | "service": "urn:dslforum-org:service:WANCommonInterfaceConfig:1", 203 | "action": "GetCommonLinkProperties", 204 | "result": "Layer1UpstreamMaxBitRate", 205 | "promDesc": { 206 | "fqName": "gateway_max_bitrate", 207 | "help": "max bitrate on gateway interface", 208 | "varLabels": [ 209 | "gateway" 210 | ], 211 | "fixedLabels": { 212 | "direction": "Up", 213 | "interface": "DSL" 214 | } 215 | }, 216 | "promType": "GaugeValue", 217 | "cacheEntryTTL": 60 218 | }, 219 | { 220 | "service": "urn:dslforum-org:service:WANCommonInterfaceConfig:1", 221 | "action": "GetCommonLinkProperties", 222 | "result": "Layer1DownstreamMaxBitRate", 223 | "promDesc": { 224 | "fqName": "gateway_max_bitrate", 225 | "help": "max bitrate on gateway interface", 226 | "varLabels": [ 227 | "gateway" 228 | ], 229 | "fixedLabels": { 230 | "direction": "Down", 231 | "interface": "DSL" 232 | } 233 | }, 234 | "promType": "GaugeValue", 235 | "cacheEntryTTL": 60 236 | }, 237 | { 238 | "service": "urn:dslforum-org:service:WANCommonInterfaceConfig:1", 239 | "action": "GetCommonLinkProperties", 240 | "result": "PhysicalLinkStatus", 241 | "okValue": "Up", 242 | "promDesc": { 243 | "fqName": "gateway_connection_status", 244 | "help": "Connection status (Connected = 1)", 245 | "varLabels": [ 246 | "gateway" 247 | ], 248 | "fixedLabels": { 249 | "connection": "Physical Link", 250 | "interface": "DSL" 251 | } 252 | }, 253 | "promType": "GaugeValue", 254 | "cacheEntryTTL": 60 255 | }, 256 | { 257 | "service": "urn:schemas-upnp-org:service:WANIPConnection:1", 258 | "action": "GetStatusInfo", 259 | "result": "ConnectionStatus", 260 | "okValue": "Connected", 261 | "promDesc": { 262 | "fqName": "gateway_connection_status", 263 | "help": "Connection status (Connected = 1)", 264 | "varLabels": [ 265 | "gateway" 266 | ], 267 | "fixedLabels": { 268 | "connection": "IP", 269 | "interface": "WAN" 270 | } 271 | }, 272 | "promType": "GaugeValue", 273 | "cacheEntryTTL": 60 274 | }, 275 | { 276 | "service": "urn:schemas-upnp-org:service:WANIPConnection:1", 277 | "action": "GetStatusInfo", 278 | "result": "Uptime", 279 | "promDesc": { 280 | "fqName": "gateway_connection_uptime_seconds", 281 | "help": "Connection uptime", 282 | "varLabels": [ 283 | "gateway" 284 | ], 285 | "fixedLabels": { 286 | "connection": "IP", 287 | "interface": "WAN" 288 | } 289 | }, 290 | "promType": "GaugeValue" 291 | }, 292 | { 293 | "service": "urn:dslforum-org:service:WANPPPConnection:1", 294 | "action": "GetInfo", 295 | "result": "ConnectionStatus", 296 | "okValue": "Connected", 297 | "promDesc": { 298 | "fqName": "gateway_wan_connection_status", 299 | "help": "WAN Connection status (Connected = 1)", 300 | "varLabels": [ 301 | "gateway", 302 | "ConnectionType", 303 | "ExternalIPAddress", 304 | "MACAddress" 305 | ], 306 | "fixedLabels": { 307 | "connection": "PPP", 308 | "interface": "WAN" 309 | } 310 | }, 311 | "promType": "GaugeValue" 312 | }, 313 | { 314 | "service": "urn:dslforum-org:service:WANPPPConnection:1", 315 | "action": "GetInfo", 316 | "result": "Uptime", 317 | "promDesc": { 318 | "fqName": "gateway_connection_uptime_seconds", 319 | "help": "Connection uptime", 320 | "varLabels": [ 321 | "gateway" 322 | ], 323 | "fixedLabels": { 324 | "connection": "PPP", 325 | "interface": "WAN" 326 | } 327 | }, 328 | "promType": "GaugeValue" 329 | }, 330 | { 331 | "service": "urn:dslforum-org:service:WANPPPConnection:1", 332 | "action": "GetInfo", 333 | "result": "UpstreamMaxBitRate", 334 | "promDesc": { 335 | "fqName": "gateway_connection_max_bitrate", 336 | "help": "connection max bitrate", 337 | "varLabels": [ 338 | "gateway" 339 | ], 340 | "fixedLabels": { 341 | "direction": "Up", 342 | "connection": "PPP", 343 | "interface": "WAN" 344 | } 345 | }, 346 | "promType": "GaugeValue" 347 | }, 348 | { 349 | "service": "urn:dslforum-org:service:WANPPPConnection:1", 350 | "action": "GetInfo", 351 | "result": "DownstreamMaxBitRate", 352 | "promDesc": { 353 | "fqName": "gateway_connection_max_bitrate", 354 | "help": "connection max bitrate", 355 | "varLabels": [ 356 | "gateway" 357 | ], 358 | "fixedLabels": { 359 | "direction": "Down", 360 | "connection": "PPP", 361 | "interface": "WAN" 362 | } 363 | }, 364 | "promType": "GaugeValue" 365 | }, 366 | { 367 | "service": "urn:dslforum-org:service:WLANConfiguration:1", 368 | "action": "GetTotalAssociations", 369 | "result": "TotalAssociations", 370 | "promDesc": { 371 | "fqName": "gateway_wlan_current_connections", 372 | "help": "current WLAN connections", 373 | "varLabels": [ 374 | "gateway" 375 | ], 376 | "fixedLabels": { 377 | "wlan": "2.4 GHz" 378 | } 379 | }, 380 | "promType": "GaugeValue" 381 | }, 382 | { 383 | "service": "urn:dslforum-org:service:WLANConfiguration:2", 384 | "action": "GetTotalAssociations", 385 | "result": "TotalAssociations", 386 | "promDesc": { 387 | "fqName": "gateway_wlan_current_connections", 388 | "help": "current WLAN connections", 389 | "varLabels": [ 390 | "gateway" 391 | ], 392 | "fixedLabels": { 393 | "wlan": "5 Ghz" 394 | } 395 | }, 396 | "promType": "GaugeValue" 397 | }, 398 | { 399 | "service": "urn:dslforum-org:service:WLANConfiguration:1", 400 | "action": "GetInfo", 401 | "result": "Status", 402 | "okValue": "Up", 403 | "promDesc": { 404 | "fqName": "gateway_wlan_status", 405 | "help": "WLAN status (Enabled = 1)", 406 | "varLabels": [ 407 | "gateway" 408 | ], 409 | "fixedLabels": { 410 | "wlan": "2.4 Ghz" 411 | } 412 | }, 413 | "promType": "GaugeValue", 414 | "cacheEntryTTL": 60 415 | }, 416 | { 417 | "service": "urn:dslforum-org:service:WLANConfiguration:2", 418 | "action": "GetInfo", 419 | "result": "Status", 420 | "okValue": "Up", 421 | "promDesc": { 422 | "fqName": "gateway_wlan_status", 423 | "help": "WLAN status (Enabled = 1)", 424 | "varLabels": [ 425 | "gateway" 426 | ], 427 | "fixedLabels": { 428 | "wlan": "5 Ghz" 429 | } 430 | }, 431 | "promType": "GaugeValue", 432 | "cacheEntryTTL": 60 433 | }, 434 | { 435 | "service": "urn:dslforum-org:service:DeviceInfo:1", 436 | "action": "GetInfo", 437 | "result": "UpTime", 438 | "promDesc": { 439 | "fqName": "gateway_uptime_seconds", 440 | "help": "gateway uptime", 441 | "varLabels": [ 442 | "gateway", 443 | "Description" 444 | ] 445 | }, 446 | "promType": "GaugeValue" 447 | }, 448 | { 449 | "service": "urn:dslforum-org:service:DeviceInfo:1", 450 | "action": "GetInfo", 451 | "result": "ModelName", 452 | "promDesc": { 453 | "fqName": "gateway_device_modelname", 454 | "help": "gateway device model name", 455 | "varLabels": [ 456 | "gateway", 457 | "Description", 458 | "ModelName", 459 | "ProductClass", 460 | "SoftwareVersion", 461 | "HardwareVersion" 462 | ] 463 | }, 464 | "promType": "GaugeValue" 465 | }, 466 | { 467 | "service": "urn:dslforum-org:service:LANEthernetInterfaceConfig:1", 468 | "action": "GetStatistics", 469 | "result": "Stats.BytesSent", 470 | "promDesc": { 471 | "fqName": "gateway_traffic", 472 | "help": "traffic on gateway interface", 473 | "varLabels": [ 474 | "gateway" 475 | ], 476 | "fixedLabels": { 477 | "direction": "Sent", 478 | "unit": "Bytes", 479 | "interface": "LAN" 480 | } 481 | }, 482 | "promType": "CounterValue" 483 | }, 484 | { 485 | "service": "urn:dslforum-org:service:LANEthernetInterfaceConfig:1", 486 | "action": "GetStatistics", 487 | "result": "Stats.BytesReceived", 488 | "promDesc": { 489 | "fqName": "gateway_traffic", 490 | "help": "traffic on gateway interface", 491 | "varLabels": [ 492 | "gateway" 493 | ], 494 | "fixedLabels": { 495 | "direction": "Received", 496 | "unit": "Bytes", 497 | "interface": "LAN" 498 | } 499 | }, 500 | "promType": "CounterValue" 501 | }, 502 | { 503 | "service": "urn:dslforum-org:service:Hosts:1", 504 | "action": "GetGenericHostEntry", 505 | "actionArgument": { 506 | "name": "NewIndex", 507 | "isIndex": true, 508 | "providerAction": "GetHostNumberOfEntries", 509 | "value": "HostNumberOfEntries" 510 | }, 511 | "result": "Active", 512 | "promDesc": { 513 | "fqName": "gateway_host_active", 514 | "help": "is host currently active", 515 | "varLabels": [ 516 | "gateway", 517 | "IPAddress", 518 | "MACAddress", 519 | "InterfaceType", 520 | "HostName" 521 | ] 522 | }, 523 | "promType": "GaugeValue", 524 | "cacheEntryTTL": 60 525 | }, 526 | { 527 | "service": "urn:dslforum-org:service:X_AVM-DE_Dect:1", 528 | "action": "GetGenericDectEntry", 529 | "actionArgument": { 530 | "name": "NewIndex", 531 | "isIndex": true, 532 | "providerAction": "GetNumberOfDectEntries", 533 | "value": "NumberOfEntries" 534 | }, 535 | "result": "Active", 536 | "promDesc": { 537 | "fqName": "gateway_dect_active", 538 | "help": "is dect device currently active", 539 | "varLabels": [ 540 | "gateway", 541 | "ID", 542 | "Name", 543 | "Model" 544 | ] 545 | }, 546 | "promType": "GaugeValue", 547 | "cacheEntryTTL": 120 548 | } 549 | ] -------------------------------------------------------------------------------- /fritzbox_upnp/service.go: -------------------------------------------------------------------------------- 1 | // Package fritzbox_upnp Query UPNP variables from Fritz!Box devices. 2 | package fritzbox_upnp 3 | 4 | // Copyright 2016 Nils Decker 5 | // 6 | // Licensed under the Apache License, Version 2.0 (the "License"); 7 | // you may not use this file except in compliance with the License. 8 | // You may obtain a copy of the License at 9 | // 10 | // http://www.apache.org/licenses/LICENSE-2.0 11 | // 12 | // Unless required by applicable law or agreed to in writing, software 13 | // distributed under the License is distributed on an "AS IS" BASIS, 14 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | // See the License for the specific language governing permissions and 16 | // limitations under the License. 17 | 18 | import ( 19 | "bytes" 20 | "crypto/md5" 21 | "crypto/rand" 22 | "crypto/tls" 23 | "encoding/xml" 24 | "errors" 25 | "fmt" 26 | "io" 27 | "net/http" 28 | "strconv" 29 | "strings" 30 | ) 31 | 32 | // curl http://fritz.box:49000/igddesc.xml 33 | // curl http://fritz.box:49000/any.xml 34 | // curl http://fritz.box:49000/igdconnSCPD.xml 35 | // curl http://fritz.box:49000/igdicfgSCPD.xml 36 | // curl http://fritz.box:49000/igddslSCPD.xml 37 | // curl http://fritz.box:49000/igd2ipv6fwcSCPD.xml 38 | 39 | const textXML = `text/xml; charset="utf-8"` 40 | 41 | var errInvalidSOAPResponse = errors.New("invalid SOAP response") 42 | 43 | // Root of the UPNP tree 44 | type Root struct { 45 | BaseURL string 46 | Username string 47 | Password string 48 | Device Device `xml:"device"` 49 | Services map[string]*Service // Map of all services indexed by .ServiceType 50 | } 51 | 52 | // Device an UPNP device 53 | type Device struct { 54 | root *Root 55 | 56 | DeviceType string `xml:"deviceType"` 57 | FriendlyName string `xml:"friendlyName"` 58 | Manufacturer string `xml:"manufacturer"` 59 | ManufacturerURL string `xml:"manufacturerURL"` 60 | ModelDescription string `xml:"modelDescription"` 61 | ModelName string `xml:"modelName"` 62 | ModelNumber string `xml:"modelNumber"` 63 | ModelURL string `xml:"modelURL"` 64 | UDN string `xml:"UDN"` 65 | 66 | Services []*Service `xml:"serviceList>service"` // Service of the device 67 | Devices []*Device `xml:"deviceList>device"` // Sub-Devices of the device 68 | 69 | PresentationURL string `xml:"presentationURL"` 70 | } 71 | 72 | // Service an UPNP Service 73 | type Service struct { 74 | Device *Device 75 | 76 | ServiceType string `xml:"serviceType"` 77 | ServiceID string `xml:"serviceId"` 78 | ControlURL string `xml:"controlURL"` 79 | EventSubURL string `xml:"eventSubURL"` 80 | SCPDUrl string `xml:"SCPDURL"` 81 | 82 | Actions map[string]*Action // All actions available on the service 83 | StateVariables []*StateVariable // All state variables available on the service 84 | } 85 | 86 | type scpdRoot struct { 87 | Actions []*Action `xml:"actionList>action"` 88 | StateVariables []*StateVariable `xml:"serviceStateTable>stateVariable"` 89 | } 90 | 91 | // Action an UPNP action on a service 92 | type Action struct { 93 | service *Service 94 | 95 | Name string `xml:"name"` 96 | Arguments []*Argument `xml:"argumentList>argument"` 97 | ArgumentMap map[string]*Argument // Map of arguments indexed by .Name 98 | } 99 | 100 | // ActionArgument an Inüut Argument to pass to an action 101 | type ActionArgument struct { 102 | Name string 103 | Value interface{} 104 | } 105 | 106 | // SoapEnvelope struct to unmarshal SOAP faults 107 | type SoapEnvelope struct { 108 | XMLName xml.Name `xml:"http://schemas.xmlsoap.org/soap/envelope/ Envelope"` 109 | Body SoapBody 110 | } 111 | 112 | // SoapBody struct 113 | type SoapBody struct { 114 | XMLName xml.Name `xml:"http://schemas.xmlsoap.org/soap/envelope/ Body"` 115 | Fault SoapFault 116 | } 117 | 118 | // SoapFault struct 119 | type SoapFault struct { 120 | XMLName xml.Name `xml:"http://schemas.xmlsoap.org/soap/envelope/ Fault"` 121 | FaultCode string `xml:"faultcode"` 122 | FaultString string `xml:"faultstring"` 123 | Detail FaultDetail `xml:"detail"` 124 | } 125 | 126 | // FaultDetail struct 127 | type FaultDetail struct { 128 | UpnpError UpnpError `xml:"UPnPError"` 129 | } 130 | 131 | // UpnpError struct 132 | type UpnpError struct { 133 | ErrorCode int `xml:"errorCode"` 134 | ErrorDescription string `xml:"errorDescription"` 135 | } 136 | 137 | // IsGetOnly Returns if the action seems to be a query for information. 138 | // This is determined by checking if the action has no input arguments and at least one output argument. 139 | func (a *Action) IsGetOnly() bool { 140 | for _, a := range a.Arguments { 141 | if a.Direction == "in" { 142 | return false 143 | } 144 | } 145 | return len(a.Arguments) > 0 146 | } 147 | 148 | // An Argument to an action 149 | type Argument struct { 150 | Name string `xml:"name"` 151 | Direction string `xml:"direction"` 152 | RelatedStateVariable string `xml:"relatedStateVariable"` 153 | StateVariable *StateVariable 154 | } 155 | 156 | // StateVariable a state variable that can be manipulated through actions 157 | type StateVariable struct { 158 | Name string `xml:"name"` 159 | DataType string `xml:"dataType"` 160 | DefaultValue string `xml:"defaultValue"` 161 | } 162 | 163 | // Result The result of a Call() contains all output arguments of the call. 164 | // The map is indexed by the name of the state variable. 165 | // The type of the value is string, uint64 or bool depending of the DataType of the variable. 166 | type Result map[string]interface{} 167 | 168 | // load the whole tree 169 | func (r *Root) load() error { 170 | igddesc, err := http.Get( 171 | fmt.Sprintf("%s/igddesc.xml", r.BaseURL), 172 | ) 173 | 174 | if err != nil { 175 | return err 176 | } 177 | 178 | defer igddesc.Body.Close() 179 | 180 | dec := xml.NewDecoder(igddesc.Body) 181 | 182 | err = dec.Decode(r) 183 | if err != nil { 184 | return err 185 | } 186 | 187 | r.Services = make(map[string]*Service) 188 | return r.Device.fillServices(r) 189 | } 190 | 191 | func (r *Root) loadTr64() error { 192 | igddesc, err := http.Get( 193 | fmt.Sprintf("%s/tr64desc.xml", r.BaseURL), 194 | ) 195 | 196 | if err != nil { 197 | return err 198 | } 199 | 200 | defer igddesc.Body.Close() 201 | 202 | dec := xml.NewDecoder(igddesc.Body) 203 | 204 | err = dec.Decode(r) 205 | if err != nil { 206 | return err 207 | } 208 | 209 | r.Services = make(map[string]*Service) 210 | return r.Device.fillServices(r) 211 | } 212 | 213 | // load all service descriptions 214 | func (d *Device) fillServices(r *Root) error { 215 | d.root = r 216 | 217 | for _, s := range d.Services { 218 | s.Device = d 219 | 220 | response, err := http.Get(r.BaseURL + s.SCPDUrl) 221 | if err != nil { 222 | return err 223 | } 224 | 225 | defer response.Body.Close() 226 | 227 | var scpd scpdRoot 228 | 229 | dec := xml.NewDecoder(response.Body) 230 | err = dec.Decode(&scpd) 231 | if err != nil { 232 | return err 233 | } 234 | 235 | s.Actions = make(map[string]*Action) 236 | for _, a := range scpd.Actions { 237 | s.Actions[a.Name] = a 238 | } 239 | s.StateVariables = scpd.StateVariables 240 | 241 | for _, a := range s.Actions { 242 | a.service = s 243 | a.ArgumentMap = make(map[string]*Argument) 244 | 245 | for _, arg := range a.Arguments { 246 | for _, svar := range s.StateVariables { 247 | if arg.RelatedStateVariable == svar.Name { 248 | arg.StateVariable = svar 249 | } 250 | } 251 | 252 | a.ArgumentMap[arg.Name] = arg 253 | } 254 | } 255 | 256 | r.Services[s.ServiceType] = s 257 | } 258 | for _, d2 := range d.Devices { 259 | err := d2.fillServices(r) 260 | if err != nil { 261 | return err 262 | } 263 | } 264 | return nil 265 | } 266 | 267 | const soapActionXML = `` + 268 | `` + 269 | `%s` + 270 | `` 271 | 272 | const soapActionParamXML = `<%s>%s` 273 | 274 | func (a *Action) createCallHTTPRequest(actionArg *ActionArgument) (*http.Request, error) { 275 | argsString := "" 276 | if actionArg != nil { 277 | var buf bytes.Buffer 278 | sValue := fmt.Sprintf("%v", actionArg.Value) 279 | xml.EscapeText(&buf, []byte(sValue)) 280 | argsString += fmt.Sprintf(soapActionParamXML, actionArg.Name, buf.String(), actionArg.Name) 281 | } 282 | bodystr := fmt.Sprintf(soapActionXML, a.Name, a.service.ServiceType, argsString, a.Name, a.service.ServiceType) 283 | 284 | url := a.service.Device.root.BaseURL + a.service.ControlURL 285 | body := strings.NewReader(bodystr) 286 | 287 | req, err := http.NewRequest("POST", url, body) 288 | if err != nil { 289 | return nil, err 290 | } 291 | 292 | action := fmt.Sprintf("%s#%s", a.service.ServiceType, a.Name) 293 | 294 | req.Header.Set("Content-Type", textXML) 295 | req.Header.Set("SOAPAction", action) 296 | 297 | return req, nil 298 | } 299 | 300 | // store auth header for reuse 301 | var authHeader = "" 302 | 303 | // Call an action with argument if given 304 | func (a *Action) Call(actionArg *ActionArgument) (Result, error) { 305 | req, err := a.createCallHTTPRequest(actionArg) 306 | 307 | if err != nil { 308 | return nil, err 309 | } 310 | 311 | // reuse prior authHeader, to avoid unnecessary authentication 312 | if authHeader != "" { 313 | req.Header.Set("Authorization", authHeader) 314 | } 315 | 316 | // first try call without auth header 317 | resp, err := http.DefaultClient.Do(req) 318 | 319 | if err != nil { 320 | return nil, err 321 | } 322 | 323 | wwwAuth := resp.Header.Get("WWW-Authenticate") 324 | if resp.StatusCode == http.StatusUnauthorized { 325 | resp.Body.Close() // close now, since we make a new request below or fail 326 | 327 | if wwwAuth != "" && a.service.Device.root.Username != "" && a.service.Device.root.Password != "" { 328 | // call failed, but we have a password so calculate header and try again 329 | authHeader, err = a.getDigestAuthHeader(wwwAuth, a.service.Device.root.Username, a.service.Device.root.Password) 330 | if err != nil { 331 | return nil, fmt.Errorf("%s: %s", a.Name, err.Error()) 332 | } 333 | 334 | req, err = a.createCallHTTPRequest(actionArg) 335 | if err != nil { 336 | return nil, fmt.Errorf("%s: %s", a.Name, err.Error()) 337 | } 338 | 339 | req.Header.Set("Authorization", authHeader) 340 | 341 | resp, err = http.DefaultClient.Do(req) 342 | 343 | if err != nil { 344 | return nil, fmt.Errorf("%s: %s", a.Name, err.Error()) 345 | } 346 | 347 | } else { 348 | return nil, fmt.Errorf("%s: Unauthorized, but no username and password given", a.Name) 349 | } 350 | } 351 | 352 | defer resp.Body.Close() 353 | 354 | if resp.StatusCode != http.StatusOK { 355 | errMsg := fmt.Sprintf("%s (%d)", http.StatusText(resp.StatusCode), resp.StatusCode) 356 | if resp.StatusCode == 500 { 357 | buf := new(strings.Builder) 358 | io.Copy(buf, resp.Body) 359 | body := buf.String() 360 | //fmt.Println(body) 361 | 362 | var soapEnv SoapEnvelope 363 | err := xml.Unmarshal([]byte(body), &soapEnv) 364 | if err != nil { 365 | errMsg = fmt.Sprintf("error decoding SOAPFault: %s", err.Error()) 366 | } else { 367 | soapFault := soapEnv.Body.Fault 368 | 369 | if soapFault.FaultString == "UPnPError" { 370 | upe := soapFault.Detail.UpnpError 371 | 372 | errMsg = fmt.Sprintf("SAOPFault: %s %d (%s)", soapFault.FaultString, upe.ErrorCode, upe.ErrorDescription) 373 | } else { 374 | errMsg = fmt.Sprintf("SAOPFault: %s", soapFault.FaultString) 375 | } 376 | } 377 | } 378 | return nil, fmt.Errorf("%s: %s", a.Name, errMsg) 379 | } 380 | 381 | return a.parseSoapResponse(resp.Body) 382 | } 383 | 384 | func (a *Action) getDigestAuthHeader(wwwAuth string, username string, password string) (string, error) { 385 | // parse www-auth header 386 | if !strings.HasPrefix(wwwAuth, "Digest ") { 387 | return "", fmt.Errorf("WWW-Authentication header is not Digest: '%s'", wwwAuth) 388 | } 389 | 390 | s := wwwAuth[7:] 391 | d := map[string]string{} 392 | for _, kv := range strings.Split(s, ",") { 393 | parts := strings.SplitN(kv, "=", 2) 394 | if len(parts) != 2 { 395 | continue 396 | } 397 | d[strings.Trim(parts[0], "\" ")] = strings.Trim(parts[1], "\" ") 398 | } 399 | 400 | if d["algorithm"] == "" { 401 | d["algorithm"] = "MD5" 402 | } else if d["algorithm"] != "MD5" { 403 | return "", fmt.Errorf("digest algorithm not supported: %s != MD5", d["algorithm"]) 404 | } 405 | 406 | if d["qop"] != "auth" { 407 | return "", fmt.Errorf("digest qop not supported: %s != auth", d["qop"]) 408 | } 409 | 410 | // calc h1 and h2 411 | ha1 := fmt.Sprintf("%x", md5.Sum([]byte(username+":"+d["realm"]+":"+password))) 412 | 413 | ha2 := fmt.Sprintf("%x", md5.Sum([]byte("POST:"+a.service.ControlURL))) 414 | 415 | cn := make([]byte, 8) 416 | rand.Read(cn) 417 | cnonce := fmt.Sprintf("%x", cn) 418 | 419 | nCounter := 1 420 | nc := fmt.Sprintf("%08x", nCounter) 421 | 422 | ds := strings.Join([]string{ha1, d["nonce"], nc, cnonce, d["qop"], ha2}, ":") 423 | response := fmt.Sprintf("%x", md5.Sum([]byte(ds))) 424 | 425 | authHeader := fmt.Sprintf("Digest username=\"%s\", realm=\"%s\", nonce=\"%s\", uri=\"%s\", cnonce=\"%s\", nc=%s, qop=%s, response=\"%s\", algorithm=%s", 426 | username, d["realm"], d["nonce"], a.service.ControlURL, cnonce, nc, d["qop"], response, d["algorithm"]) 427 | 428 | return authHeader, nil 429 | } 430 | 431 | func (a *Action) parseSoapResponse(r io.Reader) (Result, error) { 432 | res := make(Result) 433 | dec := xml.NewDecoder(r) 434 | 435 | for { 436 | t, err := dec.Token() 437 | if err == io.EOF { 438 | return res, nil 439 | } 440 | 441 | if err != nil { 442 | return nil, err 443 | } 444 | 445 | if se, ok := t.(xml.StartElement); ok { 446 | arg, ok := a.ArgumentMap[se.Name.Local] 447 | 448 | if ok { 449 | t2, err := dec.Token() 450 | if err != nil { 451 | return nil, err 452 | } 453 | 454 | var val string 455 | switch element := t2.(type) { 456 | case xml.EndElement: 457 | val = "" 458 | case xml.CharData: 459 | val = string(element) 460 | default: 461 | return nil, errInvalidSOAPResponse 462 | } 463 | 464 | converted, err := convertResult(val, arg) 465 | if err != nil { 466 | return nil, err 467 | } 468 | res[arg.StateVariable.Name] = converted 469 | } 470 | } 471 | 472 | } 473 | } 474 | 475 | func convertResult(val string, arg *Argument) (interface{}, error) { 476 | switch arg.StateVariable.DataType { 477 | case "string": 478 | return val, nil 479 | case "boolean": 480 | return bool(val == "1"), nil 481 | 482 | case "ui1", "ui2", "ui4": 483 | // type ui4 can contain values greater than 2^32! 484 | res, err := strconv.ParseUint(val, 10, 64) 485 | if err != nil { 486 | return nil, err 487 | } 488 | return uint64(res), nil 489 | case "i4": 490 | res, err := strconv.ParseInt(val, 10, 64) 491 | if err != nil { 492 | return nil, err 493 | } 494 | return int64(res), nil 495 | case "dateTime", "uuid": 496 | // data types we don't convert yet 497 | return val, nil 498 | default: 499 | return nil, fmt.Errorf("unknown datatype: %s (%s)", arg.StateVariable.DataType, val) 500 | } 501 | } 502 | 503 | // LoadServices loads the services tree from an device. 504 | func LoadServices(baseurl string, username string, password string, verifyTls bool) (*Root, error) { 505 | 506 | if !verifyTls && strings.HasPrefix(baseurl, "https://") { 507 | // disable certificate validation, since fritz.box uses self signed cert 508 | http.DefaultTransport.(*http.Transport).TLSClientConfig = &tls.Config{InsecureSkipVerify: true} 509 | } 510 | 511 | var root = &Root{ 512 | BaseURL: baseurl, 513 | Username: username, 514 | Password: password, 515 | } 516 | 517 | err := root.load() 518 | if err != nil { 519 | return nil, err 520 | } 521 | 522 | var rootTr64 = &Root{ 523 | BaseURL: baseurl, 524 | Username: username, 525 | Password: password, 526 | } 527 | 528 | err = rootTr64.loadTr64() 529 | if err != nil { 530 | return nil, err 531 | } 532 | 533 | for k, v := range rootTr64.Services { 534 | root.Services[k] = v 535 | } 536 | 537 | return root, nil 538 | } 539 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | // Copyright 2016 Nils Decker 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | 17 | import ( 18 | "bytes" 19 | "encoding/json" 20 | "fmt" 21 | "io/ioutil" 22 | "net/http" 23 | "net/url" 24 | "regexp" 25 | "sort" 26 | "strconv" 27 | "strings" 28 | "sync" 29 | "time" 30 | 31 | "github.com/namsral/flag" 32 | "github.com/prometheus/client_golang/prometheus" 33 | "github.com/prometheus/client_golang/prometheus/promhttp" 34 | "github.com/sirupsen/logrus" 35 | 36 | lua "github.com/sberk42/fritzbox_exporter/fritzbox_lua" 37 | upnp "github.com/sberk42/fritzbox_exporter/fritzbox_upnp" 38 | ) 39 | 40 | const serviceLoadRetryTime = 1 * time.Minute 41 | 42 | // minimum TTL for cached results in seconds 43 | const minCacheTTL = 30 44 | 45 | var ( 46 | flagTest = flag.Bool("test", false, "print all available metrics to stdout") 47 | flagLuaTest = flag.Bool("testLua", false, "read luaTest.json file make all contained calls and dump results") 48 | flagCollect = flag.Bool("collect", false, "print configured metrics to stdout and exit") 49 | flagJSONOut = flag.String("json-out", "", "store metrics also to JSON file when running test") 50 | 51 | flagAddr = flag.String("listen-address", "127.0.0.1:9042", "The address to listen on for HTTP requests.") 52 | flagMetricsFile = flag.String("metrics-file", "metrics.json", "The JSON file with the metric definitions.") 53 | flagDisableLua = flag.Bool("nolua", false, "disable collecting lua metrics") 54 | flagLuaMetricsFile = flag.String("lua-metrics-file", "metrics-lua.json", "The JSON file with the lua metric definitions.") 55 | 56 | flagGatewayURL = flag.String("gateway-url", "http://fritz.box:49000", "The URL of the FRITZ!Box") 57 | flagGatewayLuaURL = flag.String("gateway-luaurl", "http://fritz.box", "The URL of the FRITZ!Box UI") 58 | flagUsername = flag.String("username", "", "The user for the FRITZ!Box UPnP service") 59 | flagPassword = flag.String("password", "", "The password for the FRITZ!Box UPnP service") 60 | flagGatewayVerifyTLS = flag.Bool("verifyTls", false, "Verify the tls connection when connecting to the FRITZ!Box") 61 | flagLogLevel = flag.String("log-level", "info", "The logging level. Can be error, warn, info, debug or trace") 62 | ) 63 | 64 | var ( 65 | collectErrors = prometheus.NewCounter(prometheus.CounterOpts{ 66 | Name: "fritzbox_exporter_collectErrors", 67 | Help: "Number of collection errors.", 68 | }) 69 | ) 70 | var ( 71 | luaCollectErrors = prometheus.NewCounter(prometheus.CounterOpts{ 72 | Name: "fritzbox_exporter_luaCollectErrors", 73 | Help: "Number of lua collection errors.", 74 | }) 75 | ) 76 | var collectLuaResultsCached = prometheus.NewCounter(prometheus.CounterOpts{ 77 | Name: "fritzbox_exporter_results_cached", 78 | Help: "Number of results taken from cache.", 79 | ConstLabels: prometheus.Labels{"Cache": "LUA"}, 80 | }) 81 | var collectUpnpResultsCached = prometheus.NewCounter(prometheus.CounterOpts{ 82 | Name: "fritzbox_exporter_results_cached", 83 | Help: "Number of results taken from cache.", 84 | ConstLabels: prometheus.Labels{"Cache": "UPNP"}, 85 | }) 86 | var collectLuaResultsLoaded = prometheus.NewCounter(prometheus.CounterOpts{ 87 | Name: "fritzbox_exporter_results_loaded", 88 | Help: "Number of results loaded from fritzbox.", 89 | ConstLabels: prometheus.Labels{"Cache": "LUA"}, 90 | }) 91 | var collectUpnpResultsLoaded = prometheus.NewCounter(prometheus.CounterOpts{ 92 | Name: "fritzbox_exporter_results_loaded", 93 | Help: "Number of results loaded from fritzbox.", 94 | ConstLabels: prometheus.Labels{"Cache": "UPNP"}, 95 | }) 96 | 97 | // JSONPromDesc metric description loaded from JSON 98 | type JSONPromDesc struct { 99 | FqName string `json:"fqName"` 100 | Help string `json:"help"` 101 | VarLabels []string `json:"varLabels"` 102 | FixedLabels map[string]string `json:"fixedLabels"` 103 | fixedLabelValues string // neeeded to create uniq lookup key when reporting 104 | } 105 | 106 | // ActionArg argument for upnp action 107 | type ActionArg struct { 108 | Name string `json:"Name"` 109 | IsIndex bool `json:"IsIndex"` 110 | ProviderAction string `json:"ProviderAction"` 111 | Value string `json:"Value"` 112 | } 113 | 114 | // Metric upnp metric 115 | type Metric struct { 116 | // initialized loading JSON 117 | Service string `json:"service"` 118 | Action string `json:"action"` 119 | ActionArgument *ActionArg `json:"actionArgument"` 120 | Result string `json:"result"` 121 | OkValue string `json:"okValue"` 122 | PromDesc JSONPromDesc `json:"promDesc"` 123 | PromType string `json:"promType"` 124 | CacheEntryTTL int64 `json:"cacheEntryTTL"` 125 | 126 | // initialized at startup 127 | Desc *prometheus.Desc 128 | MetricType prometheus.ValueType 129 | } 130 | 131 | // LuaTest JSON struct for API tests 132 | type LuaTest struct { 133 | Path string `json:"path"` 134 | Params string `json:"params"` 135 | } 136 | 137 | // LuaLabelRename struct 138 | type LuaLabelRename struct { 139 | MatchRegex string `json:"matchRegex"` 140 | RenameLabel string `json:"renameLabel"` 141 | } 142 | 143 | // LuaMetric struct 144 | type LuaMetric struct { 145 | // initialized loading JSON 146 | Path string `json:"path"` 147 | Params string `json:"params"` 148 | ResultPath string `json:"resultPath"` 149 | ResultKey string `json:"resultKey"` 150 | OkValue string `json:"okValue"` 151 | PromDesc JSONPromDesc `json:"promDesc"` 152 | PromType string `json:"promType"` 153 | CacheEntryTTL int64 `json:"cacheEntryTTL"` 154 | 155 | // initialized at startup 156 | Desc *prometheus.Desc 157 | MetricType prometheus.ValueType 158 | LuaPage lua.LuaPage 159 | LuaMetricDef lua.LuaMetricValueDefinition 160 | } 161 | 162 | // LuaMetricsFile json struct 163 | type LuaMetricsFile struct { 164 | LabelRenames []LuaLabelRename `json:"labelRenames"` 165 | Metrics []*LuaMetric `json:"metrics"` 166 | } 167 | 168 | type upnpCacheEntry struct { 169 | Timestamp int64 170 | Result *upnp.Result 171 | } 172 | 173 | type luaCacheEntry struct { 174 | Timestamp int64 175 | Result *map[string]interface{} 176 | } 177 | 178 | var metrics []*Metric 179 | var luaMetrics []*LuaMetric 180 | var upnpCache map[string]*upnpCacheEntry 181 | var luaCache map[string]*luaCacheEntry 182 | 183 | // FritzboxCollector main struct 184 | type FritzboxCollector struct { 185 | URL string 186 | Gateway string 187 | Username string 188 | Password string 189 | VerifyTls bool 190 | 191 | // support for lua collector 192 | LuaSession *lua.LuaSession 193 | LabelRenames *[]lua.LabelRename 194 | 195 | sync.Mutex // protects Root 196 | Root *upnp.Root 197 | } 198 | 199 | // simple ResponseWriter to collect output 200 | type testResponseWriter struct { 201 | header http.Header 202 | statusCode int 203 | body bytes.Buffer 204 | } 205 | 206 | func (w *testResponseWriter) Header() http.Header { 207 | return w.header 208 | } 209 | 210 | func (w *testResponseWriter) Write(b []byte) (int, error) { 211 | return w.body.Write(b) 212 | } 213 | 214 | func (w *testResponseWriter) WriteHeader(statusCode int) { 215 | w.statusCode = statusCode 216 | } 217 | 218 | func (w *testResponseWriter) String() string { 219 | return w.body.String() 220 | } 221 | 222 | // LoadServices tries to load the service information. Retries until success. 223 | func (fc *FritzboxCollector) LoadServices() { 224 | for { 225 | root, err := upnp.LoadServices(fc.URL, fc.Username, fc.Password, fc.VerifyTls) 226 | if err != nil { 227 | logrus.Errorf("cannot load services: %s", err) 228 | 229 | time.Sleep(serviceLoadRetryTime) 230 | continue 231 | } 232 | 233 | logrus.Info("services loaded") 234 | 235 | fc.Lock() 236 | fc.Root = root 237 | fc.Unlock() 238 | return 239 | } 240 | } 241 | 242 | // Describe describe metric 243 | func (fc *FritzboxCollector) Describe(ch chan<- *prometheus.Desc) { 244 | for _, m := range metrics { 245 | ch <- m.Desc 246 | } 247 | } 248 | 249 | func (fc *FritzboxCollector) reportMetric(ch chan<- prometheus.Metric, m *Metric, result upnp.Result, dupCache map[string]bool) { 250 | 251 | val, ok := result[m.Result] 252 | if !ok { 253 | logrus.Debugf("%s.%s has no result %s", m.Service, m.Action, m.Result) 254 | collectErrors.Inc() 255 | return 256 | } 257 | 258 | var floatval float64 259 | switch tval := val.(type) { 260 | case uint64: 261 | floatval = float64(tval) 262 | case bool: 263 | if tval { 264 | floatval = 1 265 | } else { 266 | floatval = 0 267 | } 268 | case string: 269 | if tval == m.OkValue { 270 | floatval = 1 271 | } else { 272 | floatval = 0 273 | } 274 | default: 275 | logrus.Warnf("unknown type: %s", val) 276 | collectErrors.Inc() 277 | return 278 | } 279 | 280 | labels := make([]string, len(m.PromDesc.VarLabels)) 281 | for i, l := range m.PromDesc.VarLabels { 282 | if l == "gateway" { 283 | labels[i] = fc.Gateway 284 | } else { 285 | lval, ok := result[l] 286 | if !ok { 287 | logrus.Warnf("%s.%s has no resul for label %s", m.Service, m.Action, l) 288 | lval = "" 289 | } 290 | 291 | // convert hostname and MAC tolower to avoid problems with labels 292 | if l == "HostName" || l == "MACAddress" { 293 | labels[i] = strings.ToLower(fmt.Sprintf("%v", lval)) 294 | } else { 295 | labels[i] = fmt.Sprintf("%v", lval) 296 | } 297 | } 298 | } 299 | 300 | // check for duplicate labels to prevent collection failure 301 | key := m.PromDesc.FqName + ":" + m.PromDesc.fixedLabelValues + strings.Join(labels, ",") 302 | if dupCache[key] { 303 | logrus.Debugf("%s.%s reported before as: %s\n", m.Service, m.Action, key) 304 | collectErrors.Inc() 305 | return 306 | } 307 | dupCache[key] = true 308 | 309 | metric, err := prometheus.NewConstMetric(m.Desc, m.MetricType, floatval, labels...) 310 | if err != nil { 311 | logrus.Errorf("Can not create metric %s.%s: %s", m.Service, m.Action, err.Error()) 312 | } else { 313 | ch <- metric 314 | } 315 | } 316 | 317 | func (fc *FritzboxCollector) getActionResult(metric *Metric, actionName string, actionArg *upnp.ActionArgument) (upnp.Result, error) { 318 | 319 | key := metric.Service + "|" + actionName 320 | 321 | // for calls with argument also add argument name and value to key 322 | if actionArg != nil { 323 | key += "|" + actionArg.Name + "|" + fmt.Sprintf("%v", actionArg.Value) 324 | } 325 | 326 | now := time.Now().Unix() 327 | 328 | cacheEntry := upnpCache[key] 329 | if cacheEntry == nil { 330 | cacheEntry = &upnpCacheEntry{} 331 | upnpCache[key] = cacheEntry 332 | } else if now-cacheEntry.Timestamp > metric.CacheEntryTTL { 333 | cacheEntry.Result = nil 334 | } 335 | 336 | if cacheEntry.Result == nil { 337 | service, ok := fc.Root.Services[metric.Service] 338 | if !ok { 339 | return nil, fmt.Errorf("service %s not found", metric.Service) 340 | } 341 | 342 | action, ok := service.Actions[actionName] 343 | if !ok { 344 | return nil, fmt.Errorf("action %s not found in service %s", actionName, metric.Service) 345 | } 346 | 347 | data, err := action.Call(actionArg) 348 | 349 | if err != nil { 350 | return nil, err 351 | } 352 | 353 | cacheEntry.Timestamp = now 354 | cacheEntry.Result = &data 355 | collectUpnpResultsCached.Inc() 356 | } else { 357 | collectUpnpResultsLoaded.Inc() 358 | } 359 | 360 | return *cacheEntry.Result, nil 361 | } 362 | 363 | // Collect collect upnp metrics 364 | func (fc *FritzboxCollector) Collect(ch chan<- prometheus.Metric) { 365 | fc.Lock() 366 | root := fc.Root 367 | fc.Unlock() 368 | 369 | if root == nil { 370 | // Services not loaded yet 371 | return 372 | } 373 | 374 | // create cache for duplicate lookup, to prevent collection errors 375 | var dupCache = make(map[string]bool) 376 | 377 | for _, m := range metrics { 378 | var actArg *upnp.ActionArgument 379 | if m.ActionArgument != nil { 380 | aa := m.ActionArgument 381 | var value interface{} 382 | value = aa.Value 383 | 384 | if aa.ProviderAction != "" { 385 | provRes, err := fc.getActionResult(m, aa.ProviderAction, nil) 386 | 387 | if err != nil { 388 | logrus.Warnf("Error getting provider action %s result for %s.%s: %s", aa.ProviderAction, m.Service, m.Action, err.Error()) 389 | collectErrors.Inc() 390 | continue 391 | } 392 | 393 | var ok bool 394 | value, ok = provRes[aa.Value] // Value contains the result name for provider actions 395 | if !ok { 396 | logrus.Warnf("provider action %s for %s.%s has no result", m.Service, m.Action, aa.Value) 397 | collectErrors.Inc() 398 | continue 399 | } 400 | } 401 | 402 | if aa.IsIndex { 403 | sval := fmt.Sprintf("%v", value) 404 | count, err := strconv.Atoi(sval) 405 | if err != nil { 406 | logrus.Warn(err.Error()) 407 | collectErrors.Inc() 408 | continue 409 | } 410 | 411 | for i := 0; i < count; i++ { 412 | actArg = &upnp.ActionArgument{Name: aa.Name, Value: i} 413 | result, err := fc.getActionResult(m, m.Action, actArg) 414 | 415 | if err != nil { 416 | logrus.Error("Can not get result for %s: %s", m.Action, err) 417 | collectErrors.Inc() 418 | continue 419 | } 420 | 421 | fc.reportMetric(ch, m, result, dupCache) 422 | } 423 | 424 | continue 425 | } else { 426 | actArg = &upnp.ActionArgument{Name: aa.Name, Value: value} 427 | } 428 | } 429 | 430 | result, err := fc.getActionResult(m, m.Action, actArg) 431 | 432 | if err != nil { 433 | logrus.Warnf("can not collect metrics: %s", err) 434 | collectErrors.Inc() 435 | continue 436 | } 437 | 438 | fc.reportMetric(ch, m, result, dupCache) 439 | } 440 | 441 | // if lua is enabled now also collect metrics 442 | if fc.LuaSession != nil { 443 | fc.collectLua(ch, dupCache) 444 | } 445 | } 446 | 447 | func (fc *FritzboxCollector) collectLua(ch chan<- prometheus.Metric, dupCache map[string]bool) { 448 | // create a map for caching results 449 | now := time.Now().Unix() 450 | 451 | for _, lm := range luaMetrics { 452 | key := lm.Path + "_" + lm.Params 453 | 454 | cacheEntry := luaCache[key] 455 | if cacheEntry == nil { 456 | cacheEntry = &luaCacheEntry{} 457 | luaCache[key] = cacheEntry 458 | } else if now-cacheEntry.Timestamp > lm.CacheEntryTTL { 459 | cacheEntry.Result = nil 460 | } 461 | 462 | if cacheEntry.Result == nil { 463 | pageData, err := fc.LuaSession.LoadData(lm.LuaPage) 464 | 465 | if err != nil { 466 | logrus.Errorf("Can not load %s for %s.%s: %s", lm.Path, lm.ResultPath, lm.ResultKey, err.Error()) 467 | luaCollectErrors.Inc() 468 | fc.LuaSession.SID = "" // clear SID in case of error, so force reauthentication 469 | continue 470 | } 471 | 472 | var data map[string]interface{} 473 | data, err = lua.ParseJSON(pageData) 474 | if err != nil { 475 | logrus.Errorf("Can not parse JSON from %s for %s.%s: %s", lm.Path, lm.ResultPath, lm.ResultKey, err.Error()) 476 | luaCollectErrors.Inc() 477 | continue 478 | } 479 | 480 | cacheEntry.Result = &data 481 | cacheEntry.Timestamp = now 482 | collectLuaResultsLoaded.Inc() 483 | } else { 484 | collectLuaResultsCached.Inc() 485 | } 486 | 487 | metricVals, err := lua.GetMetrics(fc.LabelRenames, *cacheEntry.Result, lm.LuaMetricDef) 488 | 489 | if err != nil { 490 | logrus.Errorf("Can not get metric values for %s.%s: %s", lm.ResultPath, lm.ResultKey, err.Error()) 491 | luaCollectErrors.Inc() 492 | cacheEntry.Result = nil // don't use invalid results for cache 493 | continue 494 | } 495 | 496 | for _, mv := range metricVals { 497 | fc.reportLuaMetric(ch, lm, mv, dupCache) 498 | } 499 | } 500 | } 501 | 502 | func (fc *FritzboxCollector) reportLuaMetric(ch chan<- prometheus.Metric, lm *LuaMetric, value lua.LuaMetricValue, dupCache map[string]bool) { 503 | 504 | labels := make([]string, len(lm.PromDesc.VarLabels)) 505 | for i, l := range lm.PromDesc.VarLabels { 506 | if l == "gateway" { 507 | labels[i] = fc.Gateway 508 | } else { 509 | lval, ok := value.Labels[l] 510 | if !ok { 511 | logrus.Warnf("%s.%s from %s?%s has no resul for label %s", lm.ResultPath, lm.ResultKey, lm.Path, lm.Params, l) 512 | lval = "" 513 | } 514 | 515 | // convert hostname and MAC tolower to avoid problems with labels 516 | if l == "HostName" || l == "MACAddress" { 517 | labels[i] = strings.ToLower(fmt.Sprintf("%v", lval)) 518 | } else { 519 | labels[i] = fmt.Sprintf("%v", lval) 520 | } 521 | } 522 | } 523 | 524 | // check for duplicate labels to prevent collection failure 525 | key := lm.PromDesc.FqName + ":" + lm.PromDesc.fixedLabelValues + strings.Join(labels, ",") 526 | if dupCache[key] { 527 | logrus.Errorf("%s.%s reported before as: %s\n", lm.ResultPath, lm.ResultPath, key) 528 | luaCollectErrors.Inc() 529 | return 530 | } 531 | dupCache[key] = true 532 | 533 | metric, err := prometheus.NewConstMetric(lm.Desc, lm.MetricType, value.Value, labels...) 534 | if err != nil { 535 | logrus.Errorf("Can not create metric %s.%s: %s", lm.ResultPath, lm.ResultPath, err.Error()) 536 | } else { 537 | ch <- metric 538 | } 539 | } 540 | 541 | func test() { 542 | root, err := upnp.LoadServices(*flagGatewayURL, *flagUsername, *flagPassword, *flagGatewayVerifyTLS) 543 | if err != nil { 544 | panic(err) 545 | } 546 | 547 | var newEntry = false 548 | var json bytes.Buffer 549 | json.WriteString("[\n") 550 | 551 | serviceKeys := []string{} 552 | for k := range root.Services { 553 | serviceKeys = append(serviceKeys, k) 554 | } 555 | sort.Strings(serviceKeys) 556 | for _, k := range serviceKeys { 557 | s := root.Services[k] 558 | logrus.Infof("Service: %s (Url: %s)\n", k, s.ControlURL) 559 | 560 | actionKeys := []string{} 561 | for l := range s.Actions { 562 | actionKeys = append(actionKeys, l) 563 | } 564 | sort.Strings(actionKeys) 565 | for _, l := range actionKeys { 566 | a := s.Actions[l] 567 | logrus.Debugf("%s - arguments: variable [direction] (soap name, soap type)", a.Name) 568 | for _, arg := range a.Arguments { 569 | sv := arg.StateVariable 570 | logrus.Debugf("%s [%s] (%s, %s)", arg.RelatedStateVariable, arg.Direction, arg.Name, sv.DataType) 571 | } 572 | 573 | if !a.IsGetOnly() { 574 | logrus.Debugf("%s - not calling, since arguments required or no output", a.Name) 575 | continue 576 | } 577 | 578 | // only create JSON for Get 579 | // TODO also create JSON templates for input actionParams 580 | for _, arg := range a.Arguments { 581 | // create new json entry 582 | if newEntry { 583 | json.WriteString(",\n") 584 | } else { 585 | newEntry = true 586 | } 587 | 588 | json.WriteString("\t{\n\t\t\"service\": \"") 589 | json.WriteString(k) 590 | json.WriteString("\",\n\t\t\"action\": \"") 591 | json.WriteString(a.Name) 592 | json.WriteString("\",\n\t\t\"result\": \"") 593 | json.WriteString(arg.RelatedStateVariable) 594 | json.WriteString("\"\n\t}") 595 | } 596 | 597 | logrus.Debugf("%s - calling - results: variable: value", a.Name) 598 | res, err := a.Call(nil) 599 | 600 | if err != nil { 601 | logrus.Warnf("FAILED:%s", err) 602 | continue 603 | } 604 | 605 | for _, arg := range a.Arguments { 606 | logrus.Debugf("%s: %v", arg.RelatedStateVariable, res[arg.StateVariable.Name]) 607 | } 608 | } 609 | } 610 | 611 | json.WriteString("\n]") 612 | 613 | if *flagJSONOut != "" { 614 | err := ioutil.WriteFile(*flagJSONOut, json.Bytes(), 0644) 615 | if err != nil { 616 | logrus.Warnf("Failed writing JSON file '%s': %s\n", *flagJSONOut, err.Error()) 617 | } 618 | } 619 | } 620 | 621 | func testLua() { 622 | 623 | jsonData, err := ioutil.ReadFile("luaTest.json") 624 | if err != nil { 625 | logrus.Error("Can not read luaTest.json: ", err) 626 | return 627 | } 628 | 629 | var luaTests []LuaTest 630 | err = json.Unmarshal(jsonData, &luaTests) 631 | if err != nil { 632 | logrus.Error("Can not parse luaTest JSON: ", err) 633 | return 634 | } 635 | 636 | // create session struct and init params 637 | luaSession := lua.LuaSession{BaseURL: *flagGatewayLuaURL, Username: *flagUsername, Password: *flagPassword} 638 | 639 | for _, test := range luaTests { 640 | page := lua.LuaPage{Path: test.Path, Params: test.Params} 641 | pageData, err := luaSession.LoadData(page) 642 | 643 | if err != nil { 644 | logrus.Errorf("Testing %s (%s) failed: %s", test.Path, test.Params, err.Error()) 645 | } else { 646 | logrus.Infof("Testing %s(%s) successful: %s", test.Path, test.Params, string(pageData)) 647 | } 648 | } 649 | } 650 | 651 | func getValueType(vt string) prometheus.ValueType { 652 | switch vt { 653 | case "CounterValue": 654 | return prometheus.CounterValue 655 | case "GaugeValue": 656 | return prometheus.GaugeValue 657 | case "UntypedValue": 658 | return prometheus.UntypedValue 659 | } 660 | 661 | return prometheus.UntypedValue 662 | } 663 | 664 | func main() { 665 | flag.Parse() 666 | level, e := logrus.ParseLevel(*flagLogLevel) 667 | if e != nil { 668 | logrus.Warnf("Can not parse log level: %s use INFO", e) 669 | level = logrus.InfoLevel 670 | } 671 | logrus.SetLevel(level) 672 | 673 | u, err := url.Parse(*flagGatewayURL) 674 | if err != nil { 675 | logrus.Errorf("invalid URL:", err) 676 | return 677 | } 678 | 679 | if *flagTest { 680 | test() 681 | return 682 | } 683 | 684 | if *flagLuaTest { 685 | testLua() 686 | return 687 | } 688 | 689 | // read metrics 690 | jsonData, err := ioutil.ReadFile(*flagMetricsFile) 691 | if err != nil { 692 | logrus.Errorf("error reading metric file:", err) 693 | return 694 | } 695 | 696 | err = json.Unmarshal(jsonData, &metrics) 697 | if err != nil { 698 | logrus.Errorf("error parsing JSON:", err) 699 | return 700 | } 701 | 702 | // create a map for caching results 703 | upnpCache = make(map[string]*upnpCacheEntry) 704 | 705 | var luaSession *lua.LuaSession 706 | var luaLabelRenames *[]lua.LabelRename 707 | if !*flagDisableLua { 708 | jsonData, err := ioutil.ReadFile(*flagLuaMetricsFile) 709 | if err != nil { 710 | logrus.Error("error reading lua metric file:", err) 711 | return 712 | } 713 | 714 | var lmf *LuaMetricsFile 715 | err = json.Unmarshal(jsonData, &lmf) 716 | if err != nil { 717 | logrus.Error("error parsing lua JSON:", err) 718 | return 719 | } 720 | 721 | // create a map for caching results 722 | luaCache = make(map[string]*luaCacheEntry) 723 | 724 | // init label renames 725 | lblRen := make([]lua.LabelRename, 0) 726 | for _, ren := range lmf.LabelRenames { 727 | regex, err := regexp.Compile(ren.MatchRegex) 728 | 729 | if err != nil { 730 | logrus.Error("error compiling lua rename regex:", err) 731 | return 732 | } 733 | 734 | lblRen = append(lblRen, lua.LabelRename{Pattern: *regex, Name: ren.RenameLabel}) 735 | } 736 | luaLabelRenames = &lblRen 737 | 738 | // init metrics 739 | luaMetrics = lmf.Metrics 740 | for _, lm := range luaMetrics { 741 | pd := &lm.PromDesc 742 | 743 | // make labels lower case 744 | labels := make([]string, len(pd.VarLabels)) 745 | for i, l := range pd.VarLabels { 746 | labels[i] = strings.ToLower(l) 747 | } 748 | 749 | // create fixed labels values 750 | pd.fixedLabelValues = "" 751 | for _, flv := range pd.FixedLabels { 752 | pd.fixedLabelValues += flv + "," 753 | } 754 | 755 | lm.Desc = prometheus.NewDesc(pd.FqName, pd.Help, labels, pd.FixedLabels) 756 | lm.MetricType = getValueType(lm.PromType) 757 | 758 | lm.LuaPage = lua.LuaPage{ 759 | Path: lm.Path, 760 | Params: lm.Params, 761 | } 762 | 763 | lm.LuaMetricDef = lua.LuaMetricValueDefinition{ 764 | Path: lm.ResultPath, 765 | Key: lm.ResultKey, 766 | OkValue: lm.OkValue, 767 | Labels: pd.VarLabels, 768 | } 769 | 770 | // init TTL 771 | if lm.CacheEntryTTL < minCacheTTL { 772 | lm.CacheEntryTTL = minCacheTTL 773 | } 774 | } 775 | 776 | luaSession = &lua.LuaSession{ 777 | BaseURL: *flagGatewayLuaURL, 778 | Username: *flagUsername, 779 | Password: *flagPassword, 780 | } 781 | } 782 | 783 | // init metrics 784 | for _, m := range metrics { 785 | pd := &m.PromDesc 786 | 787 | // make labels lower case 788 | labels := make([]string, len(pd.VarLabels)) 789 | for i, l := range pd.VarLabels { 790 | labels[i] = strings.ToLower(l) 791 | } 792 | 793 | // create fixed labels values 794 | pd.fixedLabelValues = "" 795 | for _, flv := range pd.FixedLabels { 796 | pd.fixedLabelValues += flv + "," 797 | } 798 | 799 | m.Desc = prometheus.NewDesc(pd.FqName, pd.Help, labels, pd.FixedLabels) 800 | m.MetricType = getValueType(m.PromType) 801 | 802 | // init TTL 803 | if m.CacheEntryTTL < minCacheTTL { 804 | m.CacheEntryTTL = minCacheTTL 805 | } 806 | } 807 | 808 | collector := &FritzboxCollector{ 809 | URL: *flagGatewayURL, 810 | Gateway: u.Hostname(), 811 | Username: *flagUsername, 812 | Password: *flagPassword, 813 | VerifyTls: *flagGatewayVerifyTLS, 814 | 815 | LuaSession: luaSession, 816 | LabelRenames: luaLabelRenames, 817 | } 818 | 819 | if *flagCollect { 820 | collector.LoadServices() 821 | 822 | prometheus.MustRegister(collector) 823 | prometheus.MustRegister(collectErrors) 824 | if luaSession != nil { 825 | prometheus.MustRegister(luaCollectErrors) 826 | } 827 | 828 | logrus.Infof("collecting metrics via http") 829 | 830 | // simulate HTTP request without starting actual http server 831 | writer := testResponseWriter{header: http.Header{}} 832 | request := http.Request{} 833 | promhttp.Handler().ServeHTTP(&writer, &request) 834 | 835 | logrus.Infof("Response:\n\n%s", writer.String()) 836 | 837 | return 838 | } 839 | 840 | go collector.LoadServices() 841 | 842 | prometheus.MustRegister(collector) 843 | prometheus.MustRegister(collectErrors) 844 | prometheus.MustRegister(collectUpnpResultsCached) 845 | prometheus.MustRegister(collectUpnpResultsLoaded) 846 | 847 | if luaSession != nil { 848 | prometheus.MustRegister(luaCollectErrors) 849 | prometheus.MustRegister(collectLuaResultsCached) 850 | prometheus.MustRegister(collectLuaResultsLoaded) 851 | } 852 | 853 | healthChecks := createHealthChecks(*flagGatewayURL) 854 | 855 | http.Handle("/metrics", promhttp.Handler()) 856 | logrus.Infof("metrics available at http://%s/metrics", *flagAddr) 857 | http.HandleFunc("/ready", healthChecks.ReadyEndpoint) 858 | logrus.Infof("readyness check available at http://%s/ready", *flagAddr) 859 | http.HandleFunc("/live", healthChecks.LiveEndpoint) 860 | logrus.Infof("liveness check available at http://%s/live", *flagAddr) 861 | 862 | logrus.Error(http.ListenAndServe(*flagAddr, nil)) 863 | } 864 | -------------------------------------------------------------------------------- /grafana/Dashboard_for_grafana6.json: -------------------------------------------------------------------------------- 1 | { 2 | "__inputs": [ 3 | { 4 | "name": "DS_PROMETHEUS", 5 | "label": "Prometheus", 6 | "description": "", 7 | "type": "datasource", 8 | "pluginId": "prometheus", 9 | "pluginName": "Prometheus" 10 | } 11 | ], 12 | "__requires": [ 13 | { 14 | "type": "panel", 15 | "id": "gauge", 16 | "name": "Gauge", 17 | "version": "" 18 | }, 19 | { 20 | "type": "grafana", 21 | "id": "grafana", 22 | "name": "Grafana", 23 | "version": "6.7.4" 24 | }, 25 | { 26 | "type": "panel", 27 | "id": "graph", 28 | "name": "Graph", 29 | "version": "" 30 | }, 31 | { 32 | "type": "datasource", 33 | "id": "prometheus", 34 | "name": "Prometheus", 35 | "version": "1.0.0" 36 | }, 37 | { 38 | "type": "panel", 39 | "id": "singlestat", 40 | "name": "Singlestat", 41 | "version": "" 42 | }, 43 | { 44 | "type": "panel", 45 | "id": "table", 46 | "name": "Table", 47 | "version": "" 48 | } 49 | ], 50 | "annotations": { 51 | "list": [ 52 | { 53 | "$$hashKey": "object:41", 54 | "builtIn": 1, 55 | "datasource": "-- Grafana --", 56 | "enable": true, 57 | "hide": true, 58 | "iconColor": "rgba(0, 211, 255, 1)", 59 | "name": "Annotations & Alerts", 60 | "type": "dashboard" 61 | } 62 | ] 63 | }, 64 | "description": "Monitor FRITZ!Box routers.", 65 | "editable": true, 66 | "gnetId": 713, 67 | "graphTooltip": 2, 68 | "id": null, 69 | "links": [], 70 | "panels": [ 71 | { 72 | "cacheTimeout": null, 73 | "colorBackground": true, 74 | "colorValue": false, 75 | "colors": [ 76 | "rgba(245, 54, 54, 0.9)", 77 | "rgba(237, 129, 40, 0.89)", 78 | "#1f78c1" 79 | ], 80 | "datasource": "${DS_PROMETHEUS}", 81 | "format": "none", 82 | "gauge": { 83 | "maxValue": 100, 84 | "minValue": 0, 85 | "show": false, 86 | "thresholdLabels": false, 87 | "thresholdMarkers": true 88 | }, 89 | "id": 10, 90 | "interval": null, 91 | "links": [], 92 | "mappingType": 1, 93 | "mappingTypes": [ 94 | { 95 | "name": "value to text", 96 | "value": 1 97 | }, 98 | { 99 | "name": "range to text", 100 | "value": 2 101 | } 102 | ], 103 | "maxDataPoints": 100, 104 | "nullPointMode": "connected", 105 | "nullText": null, 106 | "postfix": "", 107 | "postfixFontSize": "50%", 108 | "prefix": "", 109 | "prefixFontSize": "50%", 110 | "rangeMaps": [ 111 | { 112 | "from": "null", 113 | "text": "N/A", 114 | "to": "null" 115 | } 116 | ], 117 | "sparkline": { 118 | "fillColor": "rgba(31, 118, 189, 0.18)", 119 | "full": false, 120 | "lineColor": "rgb(31, 120, 193)", 121 | "show": false 122 | }, 123 | "tableColumn": "", 124 | "targets": [ 125 | { 126 | "dsType": "prometheus", 127 | "expr": "gateway_wan_connection_status", 128 | "groupBy": [ 129 | { 130 | "params": [ 131 | "$__interval" 132 | ], 133 | "type": "time" 134 | } 135 | ], 136 | "interval": "", 137 | "legendFormat": "", 138 | "measurement": "fritzbox_value", 139 | "orderByTime": "ASC", 140 | "policy": "default", 141 | "refId": "A", 142 | "resultFormat": "time_series", 143 | "select": [ 144 | [ 145 | { 146 | "params": [ 147 | "value" 148 | ], 149 | "type": "field" 150 | }, 151 | { 152 | "params": [], 153 | "type": "last" 154 | } 155 | ] 156 | ], 157 | "tags": [ 158 | { 159 | "key": "type_instance", 160 | "operator": "=", 161 | "value": "constatus" 162 | } 163 | ], 164 | "target": "" 165 | } 166 | ], 167 | "thresholds": "1,1", 168 | "title": "WAN Connection Status", 169 | "type": "singlestat", 170 | "valueFontSize": "80%", 171 | "valueMaps": [ 172 | { 173 | "op": "=", 174 | "text": "N/A", 175 | "value": "null" 176 | }, 177 | { 178 | "op": "=", 179 | "text": "Disconnected", 180 | "value": "0" 181 | }, 182 | { 183 | "op": "=", 184 | "text": "Connected", 185 | "value": "1" 186 | } 187 | ], 188 | "valueName": "current" 189 | }, 190 | { 191 | "cacheTimeout": null, 192 | "colorBackground": true, 193 | "colorValue": false, 194 | "colors": [ 195 | "rgba(245, 54, 54, 0.9)", 196 | "rgba(237, 129, 40, 0.89)", 197 | "#1f78c1" 198 | ], 199 | "datasource": "${DS_PROMETHEUS}", 200 | "format": "none", 201 | "gauge": { 202 | "maxValue": 100, 203 | "minValue": 0, 204 | "show": false, 205 | "thresholdLabels": false, 206 | "thresholdMarkers": true 207 | }, 208 | "gridPos": { 209 | "h": 3, 210 | "w": 6, 211 | "x": 6, 212 | "y": 0 213 | }, 214 | "id": 9, 215 | "interval": null, 216 | "links": [], 217 | "mappingType": 1, 218 | "mappingTypes": [ 219 | { 220 | "name": "value to text", 221 | "value": 1 222 | }, 223 | { 224 | "name": "range to text", 225 | "value": 2 226 | } 227 | ], 228 | "maxDataPoints": 100, 229 | "nullPointMode": "connected", 230 | "nullText": null, 231 | "postfix": "", 232 | "postfixFontSize": "50%", 233 | "prefix": "", 234 | "prefixFontSize": "50%", 235 | "rangeMaps": [ 236 | { 237 | "from": "null", 238 | "text": "N/A", 239 | "to": "null" 240 | } 241 | ], 242 | "sparkline": { 243 | "fillColor": "rgba(31, 118, 189, 0.18)", 244 | "full": false, 245 | "lineColor": "rgb(31, 120, 193)", 246 | "show": false 247 | }, 248 | "tableColumn": "", 249 | "targets": [ 250 | { 251 | "dsType": "prometheus", 252 | "expr": "gateway_wan_layer1_link_status", 253 | "groupBy": [ 254 | { 255 | "params": [ 256 | "$__interval" 257 | ], 258 | "type": "time" 259 | } 260 | ], 261 | "interval": "", 262 | "legendFormat": "", 263 | "measurement": "fritzbox_value", 264 | "orderByTime": "ASC", 265 | "policy": "default", 266 | "refId": "A", 267 | "resultFormat": "time_series", 268 | "select": [ 269 | [ 270 | { 271 | "params": [ 272 | "value" 273 | ], 274 | "type": "field" 275 | }, 276 | { 277 | "params": [], 278 | "type": "last" 279 | } 280 | ] 281 | ], 282 | "tags": [ 283 | { 284 | "key": "type_instance", 285 | "operator": "=", 286 | "value": "dslstatus" 287 | } 288 | ], 289 | "target": "" 290 | } 291 | ], 292 | "thresholds": "1,1", 293 | "title": "DSL Link Status", 294 | "type": "singlestat", 295 | "valueFontSize": "80%", 296 | "valueMaps": [ 297 | { 298 | "op": "=", 299 | "text": "N/A", 300 | "value": "null" 301 | }, 302 | { 303 | "op": "=", 304 | "text": "Disconnected", 305 | "value": "0" 306 | }, 307 | { 308 | "op": "=", 309 | "text": "Connected", 310 | "value": "1" 311 | } 312 | ], 313 | "valueName": "current" 314 | }, 315 | { 316 | "cacheTimeout": null, 317 | "datasource": "${DS_PROMETHEUS}", 318 | "gridPos": { 319 | "h": 6, 320 | "w": 6, 321 | "x": 12, 322 | "y": 0 323 | }, 324 | "id": 11, 325 | "links": [], 326 | "options": { 327 | "fieldOptions": { 328 | "calcs": [ 329 | "lastNotNull" 330 | ], 331 | "defaults": { 332 | "color": { 333 | "mode": "thresholds" 334 | }, 335 | "decimals": 1, 336 | "mappings": [ 337 | { 338 | "id": 0, 339 | "op": "=", 340 | "text": "N/A", 341 | "type": 1, 342 | "value": "null" 343 | } 344 | ], 345 | "max": 116000000, 346 | "min": 0, 347 | "nullValueMode": "connected", 348 | "thresholds": { 349 | "mode": "absolute", 350 | "steps": [ 351 | { 352 | "color": "green", 353 | "value": null 354 | }, 355 | { 356 | "color": "rgba(237, 129, 40, 0.89)", 357 | "value": 50000000 358 | }, 359 | { 360 | "color": "red", 361 | "value": 100000000 362 | } 363 | ] 364 | }, 365 | "unit": "bps" 366 | }, 367 | "overrides": [], 368 | "values": false 369 | }, 370 | "orientation": "horizontal", 371 | "showThresholdLabels": false, 372 | "showThresholdMarkers": true 373 | }, 374 | "pluginVersion": "6.7.4", 375 | "targets": [ 376 | { 377 | "dsType": "prometheus", 378 | "expr": "gateway_wan_bytes_receive_rate", 379 | "groupBy": [ 380 | { 381 | "params": [ 382 | "$__interval" 383 | ], 384 | "type": "time" 385 | } 386 | ], 387 | "interval": "", 388 | "legendFormat": "", 389 | "measurement": "fritzbox_value", 390 | "orderByTime": "ASC", 391 | "policy": "default", 392 | "refId": "A", 393 | "resultFormat": "time_series", 394 | "select": [ 395 | [ 396 | { 397 | "params": [ 398 | "value" 399 | ], 400 | "type": "field" 401 | }, 402 | { 403 | "params": [], 404 | "type": "last" 405 | } 406 | ] 407 | ], 408 | "tags": [ 409 | { 410 | "key": "type_instance", 411 | "operator": "=", 412 | "value": "receiverate" 413 | } 414 | ], 415 | "target": "" 416 | } 417 | ], 418 | "title": "Current Download", 419 | "type": "gauge" 420 | }, 421 | { 422 | "cacheTimeout": null, 423 | "datasource": "${DS_PROMETHEUS}", 424 | "gridPos": { 425 | "h": 6, 426 | "w": 6, 427 | "x": 18, 428 | "y": 0 429 | }, 430 | "id": 12, 431 | "links": [], 432 | "options": { 433 | "fieldOptions": { 434 | "calcs": [ 435 | "lastNotNull" 436 | ], 437 | "defaults": { 438 | "color": { 439 | "mode": "thresholds" 440 | }, 441 | "decimals": 1, 442 | "mappings": [ 443 | { 444 | "id": 0, 445 | "op": "=", 446 | "text": "N/A", 447 | "type": 1, 448 | "value": "null" 449 | } 450 | ], 451 | "max": 40000000, 452 | "min": 0, 453 | "nullValueMode": "connected", 454 | "thresholds": { 455 | "mode": "absolute", 456 | "steps": [ 457 | { 458 | "color": "blue", 459 | "value": null 460 | }, 461 | { 462 | "color": "green", 463 | "value": 5000000 464 | }, 465 | { 466 | "color": "rgba(237, 129, 40, 0.89)", 467 | "value": 10000000 468 | }, 469 | { 470 | "color": "#e24d42", 471 | "value": 30000000 472 | } 473 | ] 474 | }, 475 | "unit": "bps" 476 | }, 477 | "overrides": [], 478 | "values": false 479 | }, 480 | "orientation": "horizontal", 481 | "showThresholdLabels": false, 482 | "showThresholdMarkers": true 483 | }, 484 | "pluginVersion": "6.7.4", 485 | "targets": [ 486 | { 487 | "dsType": "prometheus", 488 | "expr": "gateway_wan_bytes_send_rate", 489 | "groupBy": [ 490 | { 491 | "params": [ 492 | "$__interval" 493 | ], 494 | "type": "time" 495 | } 496 | ], 497 | "interval": "", 498 | "legendFormat": "", 499 | "measurement": "fritzbox_value", 500 | "orderByTime": "ASC", 501 | "policy": "default", 502 | "refId": "A", 503 | "resultFormat": "time_series", 504 | "select": [ 505 | [ 506 | { 507 | "params": [ 508 | "value" 509 | ], 510 | "type": "field" 511 | }, 512 | { 513 | "params": [], 514 | "type": "last" 515 | } 516 | ] 517 | ], 518 | "tags": [ 519 | { 520 | "key": "type_instance", 521 | "operator": "=", 522 | "value": "sendrate" 523 | } 524 | ], 525 | "target": "" 526 | } 527 | ], 528 | "title": "Current Upload", 529 | "type": "gauge" 530 | }, 531 | { 532 | "cacheTimeout": null, 533 | "colorBackground": false, 534 | "colorValue": false, 535 | "colors": [ 536 | "rgba(245, 54, 54, 0.9)", 537 | "rgba(237, 129, 40, 0.89)", 538 | "rgba(50, 172, 45, 0.97)" 539 | ], 540 | "datasource": "${DS_PROMETHEUS}", 541 | "decimals": 1, 542 | "format": "dtdurations", 543 | "gauge": { 544 | "maxValue": 100, 545 | "minValue": 0, 546 | "show": false, 547 | "thresholdLabels": false, 548 | "thresholdMarkers": true 549 | }, 550 | "gridPos": { 551 | "h": 3, 552 | "w": 6, 553 | "x": 0, 554 | "y": 3 555 | }, 556 | "id": 21, 557 | "interval": null, 558 | "links": [], 559 | "mappingType": 1, 560 | "mappingTypes": [ 561 | { 562 | "name": "value to text", 563 | "value": 1 564 | }, 565 | { 566 | "name": "range to text", 567 | "value": 2 568 | } 569 | ], 570 | "maxDataPoints": 100, 571 | "nullPointMode": "connected", 572 | "nullText": null, 573 | "postfix": "", 574 | "postfixFontSize": "50%", 575 | "prefix": "", 576 | "prefixFontSize": "50%", 577 | "rangeMaps": [ 578 | { 579 | "from": "null", 580 | "text": "N/A", 581 | "to": "null" 582 | } 583 | ], 584 | "sparkline": { 585 | "fillColor": "rgba(31, 118, 189, 0.18)", 586 | "full": false, 587 | "lineColor": "rgb(31, 120, 193)", 588 | "show": false 589 | }, 590 | "tableColumn": "", 591 | "targets": [ 592 | { 593 | "dsType": "prometheus", 594 | "expr": "gateway_uptime_seconds", 595 | "groupBy": [ 596 | { 597 | "params": [ 598 | "$__interval" 599 | ], 600 | "type": "time" 601 | } 602 | ], 603 | "interval": "", 604 | "legendFormat": "", 605 | "measurement": "fritzbox_value", 606 | "orderByTime": "ASC", 607 | "policy": "default", 608 | "refId": "A", 609 | "resultFormat": "time_series", 610 | "select": [ 611 | [ 612 | { 613 | "params": [ 614 | "value" 615 | ], 616 | "type": "field" 617 | }, 618 | { 619 | "params": [], 620 | "type": "last" 621 | } 622 | ] 623 | ], 624 | "tags": [ 625 | { 626 | "key": "type", 627 | "operator": "=", 628 | "value": "uptime" 629 | } 630 | ], 631 | "target": "" 632 | } 633 | ], 634 | "thresholds": "1,1", 635 | "title": "Fritzbox Uptime", 636 | "type": "singlestat", 637 | "valueFontSize": "80%", 638 | "valueMaps": [ 639 | { 640 | "op": "=", 641 | "text": "N/A", 642 | "value": "null" 643 | }, 644 | { 645 | "op": "=", 646 | "text": "Disconnected", 647 | "value": "0" 648 | }, 649 | { 650 | "op": "=", 651 | "text": "Connected", 652 | "value": "1" 653 | } 654 | ], 655 | "valueName": "current" 656 | }, 657 | { 658 | "cacheTimeout": null, 659 | "colorBackground": false, 660 | "colorValue": false, 661 | "colors": [ 662 | "rgba(245, 54, 54, 0.9)", 663 | "rgba(237, 129, 40, 0.89)", 664 | "rgba(50, 172, 45, 0.97)" 665 | ], 666 | "datasource": "${DS_PROMETHEUS}", 667 | "decimals": 1, 668 | "format": "dtdurations", 669 | "gauge": { 670 | "maxValue": 100, 671 | "minValue": 0, 672 | "show": false, 673 | "thresholdLabels": false, 674 | "thresholdMarkers": true 675 | }, 676 | "gridPos": { 677 | "h": 3, 678 | "w": 6, 679 | "x": 6, 680 | "y": 3 681 | }, 682 | "id": 13, 683 | "interval": null, 684 | "links": [], 685 | "mappingType": 1, 686 | "mappingTypes": [ 687 | { 688 | "name": "value to text", 689 | "value": 1 690 | }, 691 | { 692 | "name": "range to text", 693 | "value": 2 694 | } 695 | ], 696 | "maxDataPoints": 100, 697 | "nullPointMode": "connected", 698 | "nullText": null, 699 | "postfix": "", 700 | "postfixFontSize": "50%", 701 | "prefix": "", 702 | "prefixFontSize": "50%", 703 | "rangeMaps": [ 704 | { 705 | "from": "null", 706 | "text": "N/A", 707 | "to": "null" 708 | } 709 | ], 710 | "sparkline": { 711 | "fillColor": "rgba(31, 118, 189, 0.18)", 712 | "full": false, 713 | "lineColor": "rgb(31, 120, 193)", 714 | "show": false 715 | }, 716 | "tableColumn": "", 717 | "targets": [ 718 | { 719 | "dsType": "prometheus", 720 | "expr": "gateway_wan_connection_uptime_seconds", 721 | "groupBy": [ 722 | { 723 | "params": [ 724 | "$__interval" 725 | ], 726 | "type": "time" 727 | } 728 | ], 729 | "interval": "", 730 | "legendFormat": "", 731 | "measurement": "fritzbox_value", 732 | "orderByTime": "ASC", 733 | "policy": "default", 734 | "refId": "A", 735 | "resultFormat": "time_series", 736 | "select": [ 737 | [ 738 | { 739 | "params": [ 740 | "value" 741 | ], 742 | "type": "field" 743 | }, 744 | { 745 | "params": [], 746 | "type": "last" 747 | } 748 | ] 749 | ], 750 | "tags": [ 751 | { 752 | "key": "type_instance", 753 | "operator": "=", 754 | "value": "uptime" 755 | } 756 | ], 757 | "target": "" 758 | } 759 | ], 760 | "thresholds": "1,1", 761 | "title": "Connection Uptime", 762 | "type": "singlestat", 763 | "valueFontSize": "80%", 764 | "valueMaps": [ 765 | { 766 | "op": "=", 767 | "text": "N/A", 768 | "value": "null" 769 | } 770 | ], 771 | "valueName": "current" 772 | }, 773 | { 774 | "cacheTimeout": null, 775 | "colorBackground": false, 776 | "colorValue": false, 777 | "colors": [ 778 | "rgba(245, 54, 54, 0.9)", 779 | "rgba(237, 129, 40, 0.89)", 780 | "rgba(50, 172, 45, 0.97)" 781 | ], 782 | "datasource": "${DS_PROMETHEUS}", 783 | "decimals": 1, 784 | "format": "decbytes", 785 | "gauge": { 786 | "maxValue": 100, 787 | "minValue": 0, 788 | "show": false, 789 | "thresholdLabels": false, 790 | "thresholdMarkers": true 791 | }, 792 | "gridPos": { 793 | "h": 4, 794 | "w": 6, 795 | "x": 0, 796 | "y": 6 797 | }, 798 | "id": 3, 799 | "interval": null, 800 | "links": [], 801 | "mappingType": 1, 802 | "mappingTypes": [ 803 | { 804 | "name": "value to text", 805 | "value": 1 806 | }, 807 | { 808 | "name": "range to text", 809 | "value": 2 810 | } 811 | ], 812 | "maxDataPoints": 100, 813 | "nullPointMode": "connected", 814 | "nullText": null, 815 | "postfix": "", 816 | "postfixFontSize": "50%", 817 | "prefix": "", 818 | "prefixFontSize": "50%", 819 | "rangeMaps": [ 820 | { 821 | "from": "null", 822 | "text": "N/A", 823 | "to": "null" 824 | } 825 | ], 826 | "sparkline": { 827 | "fillColor": "rgba(31, 118, 189, 0.18)", 828 | "full": false, 829 | "lineColor": "rgb(31, 120, 193)", 830 | "show": true 831 | }, 832 | "tableColumn": "", 833 | "targets": [ 834 | { 835 | "dsType": "prometheus", 836 | "expr": "gateway_wan_bytes_received", 837 | "groupBy": [], 838 | "interval": "", 839 | "legendFormat": "", 840 | "measurement": "fritzbox_value", 841 | "orderByTime": "ASC", 842 | "policy": "default", 843 | "query": "SELECT cumulative_sum(non_negative_difference(last(\"value\"))) FROM \"fritzbox_value\" WHERE (\"type_instance\" = 'totalbytesreceived') AND $timeFilter GROUP BY time($__interval)", 844 | "rawQuery": false, 845 | "refId": "A", 846 | "resultFormat": "time_series", 847 | "select": [ 848 | [ 849 | { 850 | "params": [ 851 | "value" 852 | ], 853 | "type": "field" 854 | }, 855 | { 856 | "params": [ 857 | "10s" 858 | ], 859 | "type": "non_negative_derivative" 860 | } 861 | ] 862 | ], 863 | "tags": [ 864 | { 865 | "key": "type_instance", 866 | "operator": "=", 867 | "value": "totalbytesreceived" 868 | } 869 | ], 870 | "target": "" 871 | } 872 | ], 873 | "thresholds": "", 874 | "title": "Total Download", 875 | "type": "singlestat", 876 | "valueFontSize": "100%", 877 | "valueMaps": [ 878 | { 879 | "op": "=", 880 | "text": "N/A", 881 | "value": "null" 882 | } 883 | ], 884 | "valueName": "total" 885 | }, 886 | { 887 | "cacheTimeout": null, 888 | "colorBackground": false, 889 | "colorPrefix": true, 890 | "colorValue": false, 891 | "colors": [ 892 | "rgba(245, 54, 54, 0.9)", 893 | "rgba(237, 129, 40, 0.89)", 894 | "rgba(50, 172, 45, 0.97)" 895 | ], 896 | "datasource": "${DS_PROMETHEUS}", 897 | "decimals": 1, 898 | "format": "decbytes", 899 | "gauge": { 900 | "maxValue": 100, 901 | "minValue": 0, 902 | "show": false, 903 | "thresholdLabels": false, 904 | "thresholdMarkers": true 905 | }, 906 | "gridPos": { 907 | "h": 4, 908 | "w": 6, 909 | "x": 6, 910 | "y": 6 911 | }, 912 | "id": 8, 913 | "interval": null, 914 | "links": [], 915 | "mappingType": 1, 916 | "mappingTypes": [ 917 | { 918 | "name": "value to text", 919 | "value": 1 920 | }, 921 | { 922 | "name": "range to text", 923 | "value": 2 924 | } 925 | ], 926 | "maxDataPoints": 100, 927 | "nullPointMode": "connected", 928 | "nullText": null, 929 | "postfix": "", 930 | "postfixFontSize": "50%", 931 | "prefix": "", 932 | "prefixFontSize": "50%", 933 | "rangeMaps": [ 934 | { 935 | "from": "null", 936 | "text": "N/A", 937 | "to": "null" 938 | } 939 | ], 940 | "sparkline": { 941 | "fillColor": "rgba(137, 15, 2, 0.18)", 942 | "full": false, 943 | "lineColor": "#e24d42", 944 | "show": true 945 | }, 946 | "tableColumn": "", 947 | "targets": [ 948 | { 949 | "dsType": "prometheus", 950 | "expr": "gateway_wan_bytes_sent", 951 | "groupBy": [], 952 | "interval": "", 953 | "legendFormat": "", 954 | "measurement": "fritzbox_value", 955 | "orderByTime": "ASC", 956 | "policy": "default", 957 | "query": "SELECT cumulative_sum(non_negative_difference(last(\"value\"))) FROM \"fritzbox_value\" WHERE (\"type_instance\" = 'totalbytessent') AND $timeFilter GROUP BY time($__interval)", 958 | "rawQuery": false, 959 | "refId": "A", 960 | "resultFormat": "time_series", 961 | "select": [ 962 | [ 963 | { 964 | "params": [ 965 | "value" 966 | ], 967 | "type": "field" 968 | }, 969 | { 970 | "params": [], 971 | "type": "non_negative_difference" 972 | } 973 | ] 974 | ], 975 | "tags": [ 976 | { 977 | "key": "type_instance", 978 | "operator": "=", 979 | "value": "totalbytessent" 980 | } 981 | ], 982 | "target": "" 983 | } 984 | ], 985 | "thresholds": "", 986 | "title": "Total Upload", 987 | "type": "singlestat", 988 | "valueFontSize": "100%", 989 | "valueMaps": [ 990 | { 991 | "op": "=", 992 | "text": "N/A", 993 | "value": "null" 994 | } 995 | ], 996 | "valueName": "total" 997 | }, 998 | { 999 | "cacheTimeout": null, 1000 | "colorBackground": false, 1001 | "colorValue": false, 1002 | "colors": [ 1003 | "rgba(245, 54, 54, 0.9)", 1004 | "rgba(237, 129, 40, 0.89)", 1005 | "rgba(50, 172, 45, 0.97)" 1006 | ], 1007 | "datasource": "${DS_PROMETHEUS}", 1008 | "decimals": 1, 1009 | "format": "decbytes", 1010 | "gauge": { 1011 | "maxValue": 100, 1012 | "minValue": 0, 1013 | "show": false, 1014 | "thresholdLabels": false, 1015 | "thresholdMarkers": true 1016 | }, 1017 | "gridPos": { 1018 | "h": 4, 1019 | "w": 6, 1020 | "x": 12, 1021 | "y": 6 1022 | }, 1023 | "hideTimeOverride": false, 1024 | "id": 22, 1025 | "interval": "", 1026 | "links": [], 1027 | "mappingType": 1, 1028 | "mappingTypes": [ 1029 | { 1030 | "name": "value to text", 1031 | "value": 1 1032 | }, 1033 | { 1034 | "name": "range to text", 1035 | "value": 2 1036 | } 1037 | ], 1038 | "maxDataPoints": 100, 1039 | "nullPointMode": "connected", 1040 | "nullText": null, 1041 | "postfix": "", 1042 | "postfixFontSize": "50%", 1043 | "prefix": "", 1044 | "prefixFontSize": "50%", 1045 | "rangeMaps": [ 1046 | { 1047 | "from": "null", 1048 | "text": "N/A", 1049 | "to": "null" 1050 | } 1051 | ], 1052 | "sparkline": { 1053 | "fillColor": "rgba(31, 118, 189, 0.18)", 1054 | "full": false, 1055 | "lineColor": "rgb(31, 120, 193)", 1056 | "show": true 1057 | }, 1058 | "tableColumn": "", 1059 | "targets": [ 1060 | { 1061 | "dsType": "prometheus", 1062 | "expr": "gateway_wan_bytes_received", 1063 | "groupBy": [], 1064 | "interval": "", 1065 | "legendFormat": "", 1066 | "measurement": "fritzbox_value", 1067 | "orderByTime": "ASC", 1068 | "policy": "default", 1069 | "query": "SELECT cumulative_sum(max(\"value\")) FROM \"fritzbox_value\" WHERE (\"type_instance\" = 'totalbytesreceived') AND $timeFilter GROUP BY time($__interval) tz('Europe/Berlin')", 1070 | "rawQuery": false, 1071 | "refId": "A", 1072 | "resultFormat": "time_series", 1073 | "select": [ 1074 | [ 1075 | { 1076 | "params": [ 1077 | "value" 1078 | ], 1079 | "type": "field" 1080 | }, 1081 | { 1082 | "params": [], 1083 | "type": "non_negative_difference" 1084 | } 1085 | ] 1086 | ], 1087 | "tags": [ 1088 | { 1089 | "key": "type_instance", 1090 | "operator": "=", 1091 | "value": "totalbytesreceived" 1092 | } 1093 | ], 1094 | "target": "" 1095 | } 1096 | ], 1097 | "thresholds": "", 1098 | "timeFrom": "1d", 1099 | "timeShift": null, 1100 | "title": "Last 24h Download", 1101 | "type": "singlestat", 1102 | "valueFontSize": "100%", 1103 | "valueMaps": [ 1104 | { 1105 | "op": "=", 1106 | "text": "N/A", 1107 | "value": "null" 1108 | } 1109 | ], 1110 | "valueName": "range" 1111 | }, 1112 | { 1113 | "cacheTimeout": null, 1114 | "colorBackground": false, 1115 | "colorValue": false, 1116 | "colors": [ 1117 | "rgba(245, 54, 54, 0.9)", 1118 | "rgba(237, 129, 40, 0.89)", 1119 | "rgba(50, 172, 45, 0.97)" 1120 | ], 1121 | "datasource": "${DS_PROMETHEUS}", 1122 | "decimals": 1, 1123 | "format": "decbytes", 1124 | "gauge": { 1125 | "maxValue": 100, 1126 | "minValue": 0, 1127 | "show": false, 1128 | "thresholdLabels": false, 1129 | "thresholdMarkers": true 1130 | }, 1131 | "gridPos": { 1132 | "h": 4, 1133 | "w": 6, 1134 | "x": 18, 1135 | "y": 6 1136 | }, 1137 | "id": 16, 1138 | "interval": null, 1139 | "links": [], 1140 | "mappingType": 1, 1141 | "mappingTypes": [ 1142 | { 1143 | "name": "value to text", 1144 | "value": 1 1145 | }, 1146 | { 1147 | "name": "range to text", 1148 | "value": 2 1149 | } 1150 | ], 1151 | "maxDataPoints": 100, 1152 | "nullPointMode": "connected", 1153 | "nullText": null, 1154 | "postfix": "", 1155 | "postfixFontSize": "50%", 1156 | "prefix": "", 1157 | "prefixFontSize": "50%", 1158 | "rangeMaps": [ 1159 | { 1160 | "from": "null", 1161 | "text": "N/A", 1162 | "to": "null" 1163 | } 1164 | ], 1165 | "sparkline": { 1166 | "fillColor": "rgba(137, 15, 2, 0.18)", 1167 | "full": false, 1168 | "lineColor": "#e24d42", 1169 | "show": true 1170 | }, 1171 | "tableColumn": "", 1172 | "targets": [ 1173 | { 1174 | "dsType": "prometheus", 1175 | "expr": "gateway_wan_bytes_sent", 1176 | "groupBy": [], 1177 | "interval": "", 1178 | "legendFormat": "", 1179 | "measurement": "fritzbox_value", 1180 | "orderByTime": "ASC", 1181 | "policy": "default", 1182 | "query": "SELECT cumulative_sum(non_negative_difference(last(\"value\"))) FROM \"fritzbox_value\" WHERE (\"type_instance\" = 'totalbytessent') AND $timeFilter GROUP BY time($__interval) tz('Europe/Berlin')", 1183 | "rawQuery": false, 1184 | "refId": "A", 1185 | "resultFormat": "time_series", 1186 | "select": [ 1187 | [ 1188 | { 1189 | "params": [ 1190 | "value" 1191 | ], 1192 | "type": "field" 1193 | }, 1194 | { 1195 | "params": [], 1196 | "type": "non_negative_difference" 1197 | } 1198 | ] 1199 | ], 1200 | "tags": [ 1201 | { 1202 | "key": "type_instance", 1203 | "operator": "=", 1204 | "value": "totalbytessent" 1205 | } 1206 | ], 1207 | "target": "" 1208 | } 1209 | ], 1210 | "thresholds": "", 1211 | "timeFrom": "1d", 1212 | "title": "Last 24h Upload", 1213 | "type": "singlestat", 1214 | "valueFontSize": "100%", 1215 | "valueMaps": [ 1216 | { 1217 | "op": "=", 1218 | "text": "N/A", 1219 | "value": "null" 1220 | } 1221 | ], 1222 | "valueName": "range" 1223 | }, 1224 | { 1225 | "aliasColors": {}, 1226 | "bars": false, 1227 | "dashLength": 10, 1228 | "dashes": false, 1229 | "datasource": "${DS_PROMETHEUS}", 1230 | "fill": 1, 1231 | "fillGradient": 0, 1232 | "gridPos": { 1233 | "h": 8, 1234 | "w": 12, 1235 | "x": 0, 1236 | "y": 10 1237 | }, 1238 | "hiddenSeries": false, 1239 | "id": 24, 1240 | "legend": { 1241 | "avg": false, 1242 | "current": false, 1243 | "max": false, 1244 | "min": false, 1245 | "show": true, 1246 | "total": false, 1247 | "values": false 1248 | }, 1249 | "lines": true, 1250 | "linewidth": 1, 1251 | "nullPointMode": "null", 1252 | "options": { 1253 | "dataLinks": [] 1254 | }, 1255 | "percentage": false, 1256 | "pointradius": 2, 1257 | "points": false, 1258 | "renderer": "flot", 1259 | "seriesOverrides": [], 1260 | "spaceLength": 10, 1261 | "stack": false, 1262 | "steppedLine": false, 1263 | "targets": [ 1264 | { 1265 | "expr": "gateway_wlan_current_connections", 1266 | "interval": "", 1267 | "legendFormat": "2.4 GHz", 1268 | "refId": "A" 1269 | }, 1270 | { 1271 | "expr": "gateway_wlan2_current_connections", 1272 | "interval": "", 1273 | "legendFormat": "5 GHz", 1274 | "refId": "B" 1275 | }, 1276 | { 1277 | "expr": "gateway_wlan_current_connections+gateway_wlan2_current_connections", 1278 | "interval": "", 1279 | "legendFormat": "Total", 1280 | "refId": "C" 1281 | } 1282 | ], 1283 | "thresholds": [], 1284 | "timeFrom": null, 1285 | "timeRegions": [], 1286 | "timeShift": null, 1287 | "title": "WLAN Connections", 1288 | "tooltip": { 1289 | "shared": true, 1290 | "sort": 0, 1291 | "value_type": "individual" 1292 | }, 1293 | "type": "graph", 1294 | "xaxis": { 1295 | "buckets": null, 1296 | "mode": "time", 1297 | "name": null, 1298 | "show": true, 1299 | "values": [] 1300 | }, 1301 | "yaxes": [ 1302 | { 1303 | "format": "short", 1304 | "label": null, 1305 | "logBase": 1, 1306 | "max": null, 1307 | "min": null, 1308 | "show": true 1309 | }, 1310 | { 1311 | "format": "short", 1312 | "label": null, 1313 | "logBase": 1, 1314 | "max": null, 1315 | "min": null, 1316 | "show": true 1317 | } 1318 | ], 1319 | "yaxis": { 1320 | "align": false, 1321 | "alignLevel": null 1322 | } 1323 | }, 1324 | { 1325 | "aliasColors": { 1326 | "downstream": "#1F78C1", 1327 | "downstream max": "#0A437C", 1328 | "upstream": "#EA6460", 1329 | "upstream max": "#890F02" 1330 | }, 1331 | "annotate": { 1332 | "enable": false 1333 | }, 1334 | "bars": false, 1335 | "dashLength": 10, 1336 | "dashes": false, 1337 | "datasource": "${DS_PROMETHEUS}", 1338 | "editable": true, 1339 | "fill": 1, 1340 | "fillGradient": 0, 1341 | "grid": {}, 1342 | "gridPos": { 1343 | "h": 7, 1344 | "w": 6, 1345 | "x": 12, 1346 | "y": 10 1347 | }, 1348 | "hiddenSeries": false, 1349 | "id": 18, 1350 | "legend": { 1351 | "alignAsTable": false, 1352 | "avg": false, 1353 | "current": true, 1354 | "max": false, 1355 | "min": false, 1356 | "rightSide": false, 1357 | "show": true, 1358 | "total": false, 1359 | "values": true 1360 | }, 1361 | "lines": true, 1362 | "linewidth": 1, 1363 | "links": [], 1364 | "nullPointMode": "connected", 1365 | "options": { 1366 | "dataLinks": [] 1367 | }, 1368 | "percentage": false, 1369 | "pointradius": 5, 1370 | "points": false, 1371 | "renderer": "flot", 1372 | "resolution": 100, 1373 | "scale": 1, 1374 | "seriesOverrides": [], 1375 | "spaceLength": 10, 1376 | "stack": false, 1377 | "steppedLine": false, 1378 | "targets": [ 1379 | { 1380 | "alias": "downstream", 1381 | "dsType": "prometheus", 1382 | "expr": "gateway_wan_layer1_downstream_max_bitrate", 1383 | "fields": [ 1384 | { 1385 | "func": "mean", 1386 | "name": "value" 1387 | } 1388 | ], 1389 | "groupBy": [ 1390 | { 1391 | "params": [ 1392 | "$__interval" 1393 | ], 1394 | "type": "time" 1395 | }, 1396 | { 1397 | "params": [ 1398 | "null" 1399 | ], 1400 | "type": "fill" 1401 | } 1402 | ], 1403 | "groupByTags": [], 1404 | "interval": "", 1405 | "legendFormat": "Downstream", 1406 | "measurement": "fritzbox_value", 1407 | "orderByTime": "ASC", 1408 | "policy": "default", 1409 | "query": "SELECT mean(value) FROM \"fritzbox_value\" WHERE \"type_instance\" = 'receiverate' AND $timeFilter GROUP BY time($interval)", 1410 | "refId": "A", 1411 | "resultFormat": "time_series", 1412 | "select": [ 1413 | [ 1414 | { 1415 | "params": [ 1416 | "value" 1417 | ], 1418 | "type": "field" 1419 | }, 1420 | { 1421 | "params": [], 1422 | "type": "mean" 1423 | } 1424 | ] 1425 | ], 1426 | "tags": [ 1427 | { 1428 | "key": "type_instance", 1429 | "operator": "=", 1430 | "value": "receiverate" 1431 | } 1432 | ], 1433 | "target": "alias(collectd.squirrel.fritzbox.bitrate-receiverate,'downstream')" 1434 | }, 1435 | { 1436 | "alias": "downstream max", 1437 | "dsType": "prometheus", 1438 | "expr": "gateway_wan_layer1_upstream_max_bitrate", 1439 | "fields": [ 1440 | { 1441 | "func": "mean", 1442 | "name": "value" 1443 | } 1444 | ], 1445 | "groupBy": [ 1446 | { 1447 | "params": [ 1448 | "$__interval" 1449 | ], 1450 | "type": "time" 1451 | }, 1452 | { 1453 | "params": [ 1454 | "null" 1455 | ], 1456 | "type": "fill" 1457 | } 1458 | ], 1459 | "groupByTags": [], 1460 | "interval": "", 1461 | "legendFormat": "Upstream", 1462 | "measurement": "fritzbox_value", 1463 | "orderByTime": "ASC", 1464 | "policy": "default", 1465 | "query": "SELECT mean(value) FROM \"fritzbox_value\" WHERE \"type_instance\" = 'downstreammax' AND $timeFilter GROUP BY time($interval)", 1466 | "refId": "B", 1467 | "resultFormat": "time_series", 1468 | "select": [ 1469 | [ 1470 | { 1471 | "params": [ 1472 | "value" 1473 | ], 1474 | "type": "field" 1475 | }, 1476 | { 1477 | "params": [], 1478 | "type": "mean" 1479 | } 1480 | ] 1481 | ], 1482 | "tags": [ 1483 | { 1484 | "key": "type_instance", 1485 | "operator": "=", 1486 | "value": "downstreammax" 1487 | } 1488 | ], 1489 | "target": "alias(collectd.squirrel.fritzbox.bitrate-downstreammax,'downstream max')" 1490 | }, 1491 | { 1492 | "alias": "upstream", 1493 | "dsType": "prometheus", 1494 | "expr": "gateway_wan_bytes_receive_rate", 1495 | "fields": [ 1496 | { 1497 | "func": "mean", 1498 | "name": "value" 1499 | } 1500 | ], 1501 | "groupBy": [ 1502 | { 1503 | "params": [ 1504 | "$__interval" 1505 | ], 1506 | "type": "time" 1507 | }, 1508 | { 1509 | "params": [ 1510 | "null" 1511 | ], 1512 | "type": "fill" 1513 | } 1514 | ], 1515 | "groupByTags": [], 1516 | "interval": "", 1517 | "legendFormat": "Download", 1518 | "measurement": "fritzbox_value", 1519 | "orderByTime": "ASC", 1520 | "policy": "default", 1521 | "query": "SELECT mean(value) FROM \"fritzbox_value\" WHERE \"type_instance\" = 'sendrate' AND $timeFilter GROUP BY time($interval)", 1522 | "refId": "C", 1523 | "resultFormat": "time_series", 1524 | "select": [ 1525 | [ 1526 | { 1527 | "params": [ 1528 | "value" 1529 | ], 1530 | "type": "field" 1531 | }, 1532 | { 1533 | "params": [], 1534 | "type": "mean" 1535 | } 1536 | ] 1537 | ], 1538 | "tags": [ 1539 | { 1540 | "key": "type_instance", 1541 | "operator": "=", 1542 | "value": "sendrate" 1543 | } 1544 | ], 1545 | "target": "alias(collectd.squirrel.fritzbox.bitrate-sendrate,'upstream')" 1546 | }, 1547 | { 1548 | "alias": "upstream max", 1549 | "dsType": "prometheus", 1550 | "expr": "gateway_wan_bytes_send_rate", 1551 | "fields": [ 1552 | { 1553 | "func": "mean", 1554 | "name": "value" 1555 | } 1556 | ], 1557 | "groupBy": [ 1558 | { 1559 | "params": [ 1560 | "$__interval" 1561 | ], 1562 | "type": "time" 1563 | }, 1564 | { 1565 | "params": [ 1566 | "null" 1567 | ], 1568 | "type": "fill" 1569 | } 1570 | ], 1571 | "groupByTags": [], 1572 | "interval": "", 1573 | "legendFormat": "Upload", 1574 | "measurement": "fritzbox_value", 1575 | "orderByTime": "ASC", 1576 | "policy": "default", 1577 | "query": "SELECT mean(value) FROM \"fritzbox_value\" WHERE \"type_instance\" = 'upstreammax' AND $timeFilter GROUP BY time($interval)", 1578 | "refId": "D", 1579 | "resultFormat": "time_series", 1580 | "select": [ 1581 | [ 1582 | { 1583 | "params": [ 1584 | "value" 1585 | ], 1586 | "type": "field" 1587 | }, 1588 | { 1589 | "params": [], 1590 | "type": "mean" 1591 | } 1592 | ] 1593 | ], 1594 | "tags": [ 1595 | { 1596 | "key": "type_instance", 1597 | "operator": "=", 1598 | "value": "upstreammax" 1599 | } 1600 | ], 1601 | "target": "alias(collectd.squirrel.fritzbox.bitrate-upstreammax,'upstream max')" 1602 | } 1603 | ], 1604 | "thresholds": [], 1605 | "timeFrom": null, 1606 | "timeRegions": [], 1607 | "timeShift": null, 1608 | "title": "Bandwidth", 1609 | "tooltip": { 1610 | "msResolution": false, 1611 | "query_as_alias": true, 1612 | "shared": true, 1613 | "sort": 0, 1614 | "value_type": "cumulative" 1615 | }, 1616 | "type": "graph", 1617 | "xaxis": { 1618 | "buckets": null, 1619 | "mode": "time", 1620 | "name": null, 1621 | "show": true, 1622 | "values": [] 1623 | }, 1624 | "yaxes": [ 1625 | { 1626 | "decimals": null, 1627 | "format": "bps", 1628 | "label": "", 1629 | "logBase": 1, 1630 | "max": "116000000", 1631 | "min": "0", 1632 | "show": true 1633 | }, 1634 | { 1635 | "format": "short", 1636 | "logBase": 1, 1637 | "max": null, 1638 | "min": null, 1639 | "show": true 1640 | } 1641 | ], 1642 | "yaxis": { 1643 | "align": false, 1644 | "alignLevel": null 1645 | }, 1646 | "zerofill": true 1647 | }, 1648 | { 1649 | "aliasColors": { 1650 | "download": "#1F78C1", 1651 | "upload": "#EA6460" 1652 | }, 1653 | "annotate": { 1654 | "enable": false 1655 | }, 1656 | "bars": false, 1657 | "dashLength": 10, 1658 | "dashes": false, 1659 | "datasource": "${DS_PROMETHEUS}", 1660 | "editable": true, 1661 | "fill": 0, 1662 | "fillGradient": 0, 1663 | "grid": {}, 1664 | "gridPos": { 1665 | "h": 7, 1666 | "w": 6, 1667 | "x": 18, 1668 | "y": 10 1669 | }, 1670 | "hiddenSeries": false, 1671 | "id": 17, 1672 | "interval": "1h", 1673 | "legend": { 1674 | "avg": false, 1675 | "current": true, 1676 | "max": false, 1677 | "min": false, 1678 | "show": true, 1679 | "total": false, 1680 | "values": true 1681 | }, 1682 | "lines": true, 1683 | "linewidth": 1, 1684 | "links": [], 1685 | "nullPointMode": "connected", 1686 | "options": { 1687 | "dataLinks": [] 1688 | }, 1689 | "percentage": false, 1690 | "pointradius": 5, 1691 | "points": false, 1692 | "renderer": "flot", 1693 | "repeat": null, 1694 | "resolution": 100, 1695 | "scale": 1, 1696 | "seriesOverrides": [], 1697 | "spaceLength": 10, 1698 | "stack": false, 1699 | "steppedLine": true, 1700 | "targets": [ 1701 | { 1702 | "alias": "download", 1703 | "dsType": "prometheus", 1704 | "expr": "delta(gateway_wan_bytes_received[1m])/60", 1705 | "fields": [ 1706 | { 1707 | "func": "mean", 1708 | "name": "value" 1709 | } 1710 | ], 1711 | "groupBy": [ 1712 | { 1713 | "params": [ 1714 | "$interval" 1715 | ], 1716 | "type": "time" 1717 | }, 1718 | { 1719 | "params": [ 1720 | "null" 1721 | ], 1722 | "type": "fill" 1723 | } 1724 | ], 1725 | "groupByTags": [], 1726 | "interval": "", 1727 | "legendFormat": "Download", 1728 | "measurement": "fritzbox_value", 1729 | "orderByTime": "ASC", 1730 | "policy": "default", 1731 | "query": "SELECT mean(\"value\") FROM \"fritzbox_value\" WHERE (\"type_instance\" = 'totalbytesreceived') AND $timeFilter GROUP BY time($interval) fill(null)", 1732 | "rawQuery": true, 1733 | "refId": "A", 1734 | "resultFormat": "time_series", 1735 | "select": [ 1736 | [ 1737 | { 1738 | "params": [ 1739 | "value" 1740 | ], 1741 | "type": "field" 1742 | }, 1743 | { 1744 | "params": [], 1745 | "type": "mean" 1746 | } 1747 | ] 1748 | ], 1749 | "tags": [ 1750 | { 1751 | "key": "type_instance", 1752 | "operator": "=", 1753 | "value": "totalbytesreceived" 1754 | } 1755 | ], 1756 | "target": "alias(summarize(nonNegativeDerivative(collectd.squirrel.fritzbox.bytes-totalbytesreceived, 0), '1h', 'sum'), 'download')" 1757 | }, 1758 | { 1759 | "alias": "upload", 1760 | "dsType": "prometheus", 1761 | "expr": "delta(gateway_wan_bytes_sent[1m])/60", 1762 | "fields": [ 1763 | { 1764 | "func": "mean", 1765 | "name": "value" 1766 | } 1767 | ], 1768 | "fill": "null", 1769 | "groupBy": [ 1770 | { 1771 | "params": [ 1772 | "$interval" 1773 | ], 1774 | "type": "time" 1775 | }, 1776 | { 1777 | "params": [ 1778 | "null" 1779 | ], 1780 | "type": "fill" 1781 | } 1782 | ], 1783 | "groupByTags": [], 1784 | "hide": false, 1785 | "interval": "", 1786 | "legendFormat": "Upload", 1787 | "measurement": "fritzbox_value", 1788 | "orderByTime": "ASC", 1789 | "policy": "default", 1790 | "query": "SELECT non_negative_difference(last(cumulative_sum)) FROM (\nSELECT cumulative_sum(non_negative_difference(last(\"value\"))) FROM \"fritzbox_value\" WHERE (\"type_instance\" = 'totalbytessent') AND $timeFilter GROUP BY time($__interval)\n) WHERE $timeFilter GROUP BY time($__interval) tz('Europe/Berlin')", 1791 | "rawQuery": true, 1792 | "refId": "B", 1793 | "resultFormat": "time_series", 1794 | "select": [ 1795 | [ 1796 | { 1797 | "params": [ 1798 | "value" 1799 | ], 1800 | "type": "field" 1801 | }, 1802 | { 1803 | "params": [], 1804 | "type": "mean" 1805 | } 1806 | ] 1807 | ], 1808 | "tags": [ 1809 | { 1810 | "key": "type_instance", 1811 | "operator": "=", 1812 | "value": "totalbytessent" 1813 | } 1814 | ], 1815 | "target": "alias(summarize(nonNegativeDerivative(collectd.squirrel.fritzbox.bytes-totalbytessent,0),'1h','sum'),'upload')" 1816 | } 1817 | ], 1818 | "thresholds": [], 1819 | "timeFrom": null, 1820 | "timeRegions": [], 1821 | "timeShift": null, 1822 | "title": "Current Traffic", 1823 | "tooltip": { 1824 | "msResolution": false, 1825 | "query_as_alias": true, 1826 | "shared": true, 1827 | "sort": 0, 1828 | "value_type": "cumulative" 1829 | }, 1830 | "type": "graph", 1831 | "xaxis": { 1832 | "buckets": null, 1833 | "mode": "time", 1834 | "name": null, 1835 | "show": true, 1836 | "values": [] 1837 | }, 1838 | "yaxes": [ 1839 | { 1840 | "format": "Bps", 1841 | "logBase": 1, 1842 | "max": null, 1843 | "min": 0, 1844 | "show": true 1845 | }, 1846 | { 1847 | "format": "Bps", 1848 | "logBase": 1, 1849 | "max": null, 1850 | "min": null, 1851 | "show": true 1852 | } 1853 | ], 1854 | "yaxis": { 1855 | "align": false, 1856 | "alignLevel": null 1857 | }, 1858 | "zerofill": true 1859 | }, 1860 | { 1861 | "aliasColors": { 1862 | "Download": "#1f78c1", 1863 | "Upload": "#e24d42", 1864 | "fritzbox_value.non_negative_derivative": "#ba43a9", 1865 | "fritzbox_value.non_negative_difference": "#e24d42" 1866 | }, 1867 | "bars": true, 1868 | "dashLength": 10, 1869 | "dashes": false, 1870 | "datasource": "${DS_PROMETHEUS}", 1871 | "fill": 1, 1872 | "fillGradient": 0, 1873 | "gridPos": { 1874 | "h": 7, 1875 | "w": 12, 1876 | "x": 0, 1877 | "y": 18 1878 | }, 1879 | "hiddenSeries": false, 1880 | "id": 2, 1881 | "interval": "10s", 1882 | "legend": { 1883 | "alignAsTable": false, 1884 | "avg": false, 1885 | "current": false, 1886 | "hideEmpty": false, 1887 | "hideZero": false, 1888 | "max": false, 1889 | "min": false, 1890 | "rightSide": false, 1891 | "show": true, 1892 | "total": false, 1893 | "values": false 1894 | }, 1895 | "lines": false, 1896 | "linewidth": 1, 1897 | "links": [], 1898 | "nullPointMode": "null", 1899 | "options": { 1900 | "dataLinks": [] 1901 | }, 1902 | "percentage": false, 1903 | "pointradius": 5, 1904 | "points": false, 1905 | "renderer": "flot", 1906 | "seriesOverrides": [], 1907 | "spaceLength": 10, 1908 | "stack": false, 1909 | "steppedLine": false, 1910 | "targets": [ 1911 | { 1912 | "alias": "Download", 1913 | "dsType": "prometheus", 1914 | "expr": "gateway_wan_bytes_received", 1915 | "format": "time_series", 1916 | "groupBy": [ 1917 | { 1918 | "params": [ 1919 | "24h" 1920 | ], 1921 | "type": "time" 1922 | } 1923 | ], 1924 | "hide": false, 1925 | "instant": false, 1926 | "interval": "", 1927 | "legendFormat": "Download", 1928 | "measurement": "fritzbox_value", 1929 | "orderByTime": "ASC", 1930 | "policy": "default", 1931 | "query": "SELECT non_negative_difference(last(cumulative_sum)) FROM (\nSELECT cumulative_sum(non_negative_difference(\"value\")) FROM \"fritzbox_value\" WHERE (\"type_instance\" = 'totalbytesreceived') AND $timeFilter\n) WHERE $timeFilter GROUP BY time(1d) tz('Europe/Berlin')", 1932 | "rawQuery": true, 1933 | "refId": "F", 1934 | "resultFormat": "time_series", 1935 | "select": [ 1936 | [ 1937 | { 1938 | "params": [ 1939 | "value" 1940 | ], 1941 | "type": "field" 1942 | }, 1943 | { 1944 | "params": [], 1945 | "type": "max" 1946 | }, 1947 | { 1948 | "params": [ 1949 | "10s" 1950 | ], 1951 | "type": "non_negative_derivative" 1952 | } 1953 | ] 1954 | ], 1955 | "tags": [ 1956 | { 1957 | "key": "type_instance", 1958 | "operator": "=", 1959 | "value": "totalbytesreceived" 1960 | } 1961 | ], 1962 | "target": "" 1963 | }, 1964 | { 1965 | "alias": "Upload", 1966 | "dsType": "prometheus", 1967 | "expr": "gateway_wan_bytes_sent", 1968 | "groupBy": [ 1969 | { 1970 | "params": [ 1971 | "24h" 1972 | ], 1973 | "type": "time" 1974 | } 1975 | ], 1976 | "hide": false, 1977 | "interval": "", 1978 | "legendFormat": "Upload", 1979 | "measurement": "fritzbox_value", 1980 | "orderByTime": "ASC", 1981 | "policy": "default", 1982 | "query": "SELECT non_negative_difference(last(cumulative_sum)) FROM (\nSELECT cumulative_sum(non_negative_difference(\"value\")) FROM \"fritzbox_value\" WHERE (\"type_instance\" = 'totalbytessent') AND $timeFilter \n) WHERE $timeFilter GROUP BY time(1d) tz('Europe/Berlin')", 1983 | "rawQuery": true, 1984 | "refId": "A", 1985 | "resultFormat": "time_series", 1986 | "select": [ 1987 | [ 1988 | { 1989 | "params": [ 1990 | "value" 1991 | ], 1992 | "type": "field" 1993 | }, 1994 | { 1995 | "params": [], 1996 | "type": "max" 1997 | }, 1998 | { 1999 | "params": [ 2000 | "10s" 2001 | ], 2002 | "type": "non_negative_derivative" 2003 | } 2004 | ] 2005 | ], 2006 | "tags": [ 2007 | { 2008 | "key": "type_instance", 2009 | "operator": "=", 2010 | "value": "totalbytesreceived" 2011 | } 2012 | ], 2013 | "target": "" 2014 | } 2015 | ], 2016 | "thresholds": [], 2017 | "timeFrom": null, 2018 | "timeRegions": [], 2019 | "timeShift": null, 2020 | "title": "Daily Traffic", 2021 | "tooltip": { 2022 | "shared": true, 2023 | "sort": 0, 2024 | "value_type": "individual" 2025 | }, 2026 | "type": "graph", 2027 | "xaxis": { 2028 | "buckets": null, 2029 | "mode": "time", 2030 | "name": null, 2031 | "show": true, 2032 | "values": [] 2033 | }, 2034 | "yaxes": [ 2035 | { 2036 | "decimals": 0, 2037 | "format": "decbytes", 2038 | "label": "", 2039 | "logBase": 1, 2040 | "max": null, 2041 | "min": null, 2042 | "show": true 2043 | }, 2044 | { 2045 | "format": "decbytes", 2046 | "label": null, 2047 | "logBase": 1, 2048 | "max": null, 2049 | "min": null, 2050 | "show": true 2051 | } 2052 | ], 2053 | "yaxis": { 2054 | "align": false, 2055 | "alignLevel": null 2056 | } 2057 | }, 2058 | { 2059 | "columns": [], 2060 | "datasource": "${DS_PROMETHEUS}", 2061 | "fontSize": "100%", 2062 | "gridPos": { 2063 | "h": 8, 2064 | "w": 12, 2065 | "x": 0, 2066 | "y": 25 2067 | }, 2068 | "id": 14, 2069 | "links": [], 2070 | "pageSize": null, 2071 | "scroll": true, 2072 | "showHeader": true, 2073 | "sort": { 2074 | "col": 0, 2075 | "desc": true 2076 | }, 2077 | "styles": [ 2078 | { 2079 | "alias": "Time", 2080 | "align": "auto", 2081 | "dateFormat": "MMMM D, YYYY LT", 2082 | "pattern": "Time", 2083 | "type": "date" 2084 | }, 2085 | { 2086 | "alias": "", 2087 | "align": "auto", 2088 | "colorMode": null, 2089 | "colors": [ 2090 | "rgba(245, 54, 54, 0.9)", 2091 | "rgba(237, 129, 40, 0.89)", 2092 | "rgba(50, 172, 45, 0.97)" 2093 | ], 2094 | "decimals": 2, 2095 | "pattern": "/.*/", 2096 | "thresholds": [], 2097 | "type": "number", 2098 | "unit": "decbytes" 2099 | } 2100 | ], 2101 | "targets": [ 2102 | { 2103 | "alias": "Download", 2104 | "dsType": "prometheus", 2105 | "expr": "gateway_wan_bytes_received", 2106 | "groupBy": [ 2107 | { 2108 | "params": [ 2109 | "1d" 2110 | ], 2111 | "type": "time" 2112 | } 2113 | ], 2114 | "interval": "", 2115 | "legendFormat": "Download", 2116 | "measurement": "fritzbox_value", 2117 | "orderByTime": "ASC", 2118 | "policy": "default", 2119 | "query": "SELECT non_negative_difference(last(cumulative_sum)) FROM (\nSELECT cumulative_sum(non_negative_difference(\"value\")) FROM \"fritzbox_value\" WHERE (\"type_instance\" = 'totalbytesreceived') AND $timeFilter \n) WHERE $timeFilter GROUP BY time(1d) tz('Europe/Berlin')", 2120 | "rawQuery": true, 2121 | "refId": "A", 2122 | "resultFormat": "time_series", 2123 | "select": [ 2124 | [ 2125 | { 2126 | "params": [ 2127 | "value" 2128 | ], 2129 | "type": "field" 2130 | }, 2131 | { 2132 | "params": [], 2133 | "type": "max" 2134 | }, 2135 | { 2136 | "params": [ 2137 | "10s" 2138 | ], 2139 | "type": "non_negative_derivative" 2140 | } 2141 | ] 2142 | ], 2143 | "tags": [ 2144 | { 2145 | "key": "type_instance", 2146 | "operator": "=", 2147 | "value": "totalbytesreceived" 2148 | } 2149 | ], 2150 | "target": "" 2151 | }, 2152 | { 2153 | "alias": "Upload", 2154 | "dsType": "prometheus", 2155 | "expr": "gateway_wan_bytes_sent", 2156 | "groupBy": [ 2157 | { 2158 | "params": [ 2159 | "1d" 2160 | ], 2161 | "type": "time" 2162 | } 2163 | ], 2164 | "interval": "", 2165 | "legendFormat": "Upload", 2166 | "measurement": "fritzbox_value", 2167 | "orderByTime": "ASC", 2168 | "policy": "default", 2169 | "query": "SELECT non_negative_difference(last(cumulative_sum)) FROM (\nSELECT cumulative_sum(non_negative_difference(\"value\")) FROM \"fritzbox_value\" WHERE (\"type_instance\" = 'totalbytessent') AND $timeFilter\n) WHERE $timeFilter GROUP BY time(1d) tz('Europe/Berlin')", 2170 | "rawQuery": true, 2171 | "refId": "B", 2172 | "resultFormat": "time_series", 2173 | "select": [ 2174 | [ 2175 | { 2176 | "params": [ 2177 | "value" 2178 | ], 2179 | "type": "field" 2180 | }, 2181 | { 2182 | "params": [], 2183 | "type": "max" 2184 | }, 2185 | { 2186 | "params": [ 2187 | "10s" 2188 | ], 2189 | "type": "non_negative_derivative" 2190 | } 2191 | ] 2192 | ], 2193 | "tags": [ 2194 | { 2195 | "key": "type_instance", 2196 | "operator": "=", 2197 | "value": "totalbytesreceived" 2198 | } 2199 | ], 2200 | "target": "" 2201 | } 2202 | ], 2203 | "title": "Daily Traffic", 2204 | "transform": "timeseries_to_columns", 2205 | "type": "table" 2206 | } 2207 | ], 2208 | "refresh": false, 2209 | "schemaVersion": 22, 2210 | "style": "dark", 2211 | "tags": [], 2212 | "templating": { 2213 | "list": [] 2214 | }, 2215 | "time": { 2216 | "from": "now-7d", 2217 | "to": "now" 2218 | }, 2219 | "timepicker": { 2220 | "refresh_intervals": [ 2221 | "5s", 2222 | "10s", 2223 | "30s", 2224 | "1m", 2225 | "5m", 2226 | "15m", 2227 | "30m", 2228 | "1h", 2229 | "2h", 2230 | "1d" 2231 | ], 2232 | "time_options": [ 2233 | "5m", 2234 | "15m", 2235 | "1h", 2236 | "6h", 2237 | "12h", 2238 | "24h", 2239 | "2d", 2240 | "7d", 2241 | "30d" 2242 | ] 2243 | }, 2244 | "timezone": "", 2245 | "title": "FRITZ!Box Status", 2246 | "uid": "000000013", 2247 | "variables": { 2248 | "list": [] 2249 | }, 2250 | "version": 18 2251 | } --------------------------------------------------------------------------------