Sensors for Telekom Speedport Smart 4

UPDATE 2023-10-16:

Please have a look at this post where @Andre0512 introduces his marvellous HACS add-on!
You can also find it on GitHub.

original post:

Hi,

I was looking for a solution to integrate my Telekom Speedport Smart 4 router into HomeAssistant.
Unfortunately, I had to realize that scraping doesn’t work (javascript) and that the router’s Status.json is encrypted (wtf!?)

Luckily, I found the dsl-monitoring project on GitHub: https://github.com/aaronk6/dsl-monitoring
This project implements a decryption solution for the router’s Status.json .

Many thanks to its fantastic founder aaronk6 - when I asked him if it would be possible to save the raw decrypted json output, he altered his script to do so.

I was able to use my linux server and the smart4-vdsl script together with a cronjob to read and decrypt the router’s Status.json every 5 minutes; afterwards, it is written to config/www/json on my HomeAssistant server (mounted via Samba to my linux server running the script). Being placed there, the json file can be read via the link given below.

My problem:
I am still looking for a way to integrate the smart4-vdsl script into HomeAssistant in order to circumvent the second linux server - either via integrating the python script directly into HomeAssistant or by writing a custom integration.

configurations:
syntax for smart4-vdsl script:

/usr/scripts/smart4-vdsl --hostname 192.168.1.1 --format raw >> /path/to/write/new_status.json

config/configuration.yaml:

...
sensor: !include_dir_merge_list includes/sensors/
...

config/secrets.yaml:

...
# Speedport Smart 4-JSON-Adresse
Speedport_Smart_4_json_link: https://homeassistant.<domain>.tld/local/json/Speedport_Smart_4_Status.json
...

config/includes/sensors/Speedport_Smart_4.yaml:

# Telekom Speedport Smart 4-Sensoren

  - platform: rest
    name: Speedport Smart 4 - Firmware-Version
    resource: !secret Speedport_Smart_4_json_link
    scan_interval: 300
    value_template: "{{ value_json.60.varvalue }}"

  - platform: rest
    name: Speedport Smart 4 - DSL-Link-Status
    resource: !secret Speedport_Smart_4_json_link
    scan_interval: 300
    value_template: "{{ value_json.11.varvalue }}"

  - platform: rest
    name: Speedport Smart 4 - Hybrid-Tunnel
    resource: !secret Speedport_Smart_4_json_link
    scan_interval: 300
    value_template: "{{ value_json.35.varvalue }}"

  - platform: rest
    name: Speedport Smart 4 - DSL-Downstream
    resource: !secret Speedport_Smart_4_json_link
    scan_interval: 300
    value_template: "{{ value_json.62.varvalue | float/1000000 }}"
    unit_of_measurement: "MBit/s"

  - platform: rest
    name: Speedport Smart 4 - DSL-Upstream
    resource: !secret Speedport_Smart_4_json_link
    scan_interval: 300
    value_template: "{{ value_json.63.varvalue | float/1000000 }}"
    unit_of_measurement: "MBit/s"

  - platform: rest
    name: Speedport Smart 4 - DSL-PoP
    resource: !secret Speedport_Smart_4_json_link
    scan_interval: 300
    value_template: "{{ value_json.66.varvalue }}"

Any ideas how to do this?

Thanks in advance and best regards,
David

Okay, I finally found a working solution.

1. Install https://github.com/AlexxIT/PythonScriptsPro (see also https://smarterkram.de/2524/) and restart HomeAssistant.

2. Add “sensor include” and requirements to configuration.yaml:

...
sensor: !include_dir_merge_list includes/sensors/
...
# Python Scripts Pro (HACS)
python_script:
  requirements:
  - influxdb==5.3.1
  - lxml==4.9.2
  - pycryptodome==3.17
  - requests==2.28.2

3. Create folders /config/www/json/ and /config/python_scripts/ if necessary.

4. Copy the dsl-monitoring-script (thanks again @aaronk6!) and save it to /config/python_scripts/ as “smart4-vdsl.py”:

#!/usr/bin/env python3

import json
import sys
import os
import argparse
import requests
import datetime
from time import sleep
from influxdb import line_protocol
from Crypto.Cipher import AES

FORMAT_JSON = 'json'
FORMAT_INFLUXDB = 'influxdb'
FORMAT_RAW = 'raw'

OUTPUT_FORMATS = [ FORMAT_JSON, FORMAT_INFLUXDB, FORMAT_RAW ]

DEFAULT_HOSTNAME = '169.254.2.1'
DEFAULT_PORT = 80
DEFAULT_KEY = 'cdc0cac1280b516e674f0057e4929bca84447cca8425007e33a88a5cf598a190'
DEFAULT_FORMAT = FORMAT_JSON
STATUS_ROUTE = '/data/Status.json'

HTTP_TIMEOUT = 5
MAX_RETRIES = 3
RETRY_WAIT = 3

MEASUREMENT_NAME = "smart4_vdsl_status"
REPORT_FIELDS = [ 'dsl_link_status', 'dsl_downstream', 'dsl_upstream', 'firmware_version' ]
# Fields are strings by default. Cast these to integers:
INTEGER_FIELDS = [ 'dsl_downstream', 'dsl_upstream' ]

def http_get_encrypted_json(encryptionKey, url, params={}):
    res = None

    for i in range(MAX_RETRIES+1):
        try:
            headers = { 'Accept': 'application/json' }
            response = requests.get(url, params=params, headers=headers, timeout=HTTP_TIMEOUT)

            try:
                res = response.json()
            except ValueError:
                try:
                    decrypted = decrypt_response(encryptionKey, response.text)
                    res = json.loads(decrypted)
                except ValueError:
                    eprint("Decryption or JSON parsing failed")
                    continue

        except Exception as e:
            eprint("Error: %s" % e)
            if i < MAX_RETRIES:
                eprint("Will do %i. retry in %i sec(s)..." % (i+1, RETRY_WAIT ))
                sleep(RETRY_WAIT)
            else:
                eprint("Maximum number of retries exceeded, giving up")
            continue
        break

    return res

def decrypt_response(keyHex, data):
    # thanks to https://stackoverflow.com/a/69054338/1387396

    key = bytes.fromhex(keyHex)
    nonce = bytes.fromhex(keyHex)[:8]

    ciphertextTag = bytes.fromhex(data)
    ciphertext = ciphertextTag[:-16]
    tag = ciphertextTag[-16:]

    cipher = AES.new(key, AES.MODE_CCM, nonce)
    decrypted = cipher.decrypt_and_verify(ciphertext, tag)
    return decrypted.decode('utf-8')

def get_field(report, name):
    field = next((x for x in report if x['varid'] == name), None)
    if name in INTEGER_FIELDS:
        return int(field['varvalue'])
    return field['varvalue']

def get_vdsl_status(hostname, port, key, raw=False):
    url = "http://%s:%i%s" % ( hostname, port, STATUS_ROUTE )
    report = http_get_encrypted_json(key, url)

    if raw: return report

    status = {}
    for field in REPORT_FIELDS:
        status[field] = get_field(report, field)

    return status

def get_data_points(vdsl_status):
    data = { "points": [] }
    time = get_current_utc_time()

    data["points"].append({
        "measurement": MEASUREMENT_NAME,
        "fields": vdsl_status,
        "time": time
    })
    return data

def get_formatted_output(status, format):
    if (format == FORMAT_INFLUXDB):
        return line_protocol.make_lines(get_data_points(status))
    elif (format in [ FORMAT_JSON, FORMAT_RAW ]):
        return json.dumps(status)
    else:
        eprint("Unknown format %s" % format)
        return ''

def get_current_utc_time():
    return datetime.datetime.utcnow().replace(microsecond=0).isoformat()

def eprint(*args, **kwargs):
    print(*args, file=sys.stderr, **kwargs)

def main():
    parser = argparse.ArgumentParser()
    parser.add_argument("--hostname", default=DEFAULT_HOSTNAME,
                        help="Specify hostname or IP address (defaults to %s)" % DEFAULT_HOSTNAME)
    parser.add_argument("--port", default=DEFAULT_PORT, type=int,
                        help="Specify port (defaults to %i" % DEFAULT_PORT)
    parser.add_argument("--key", default=DEFAULT_KEY,
                        help="Specify key for AES decryption (defaults to %s)" % DEFAULT_KEY)
    parser.add_argument("--format", default=DEFAULT_FORMAT,
                        help="Specify the output format (one of %s; defaults to %s)" % ( ', '.join(OUTPUT_FORMATS), DEFAULT_FORMAT ) )

    params = parser.parse_args()

    status = get_vdsl_status(params.hostname, params.port, params.key, params.format == FORMAT_RAW)
    output = get_formatted_output(status, params.format)

    if(output == ''): exit(1)

    print(output)

if __name__ == '__main__':
    main()

5. Add the following code to you secrets.yaml:

...
# Speedport Smart 4-JSON-Adresse
Speedport_Smart_4_json_link: https://homeassistant.<your_domain>.<your_tld>/local/json/Speedport_Smart_4_Status.json
...

6. Save the following code to /config/includes/sensors/Speedport_Smart_4.yaml:

# Python-Aufruf

  - platform: command_line
    name: Speedport Smart 4 - Python-Script-Aufruf
    command: "python3 /config/python_scripts/smart4-vdsl.py --hostname 192.168.1.1 --format raw > /config/www/json/Speedport_Smart_4_Status.json"
    scan_interval: 45


# Telekom Speedport Smart 4-Sensoren

  - platform: rest
    name: Speedport Smart 4 - Router-Name
    resource: !secret Speedport_Smart_4_json_link
    scan_interval: 60
    value_template: "{{ value_json | selectattr('varid', 'eq', 'device_name') | map(attribute='varvalue') | first | default }}"

  - platform: rest
    name: Speedport Smart 4 - Router-Status
    resource: !secret Speedport_Smart_4_json_link
    scan_interval: 60
    value_template: "{{ value_json | selectattr('varid', 'eq', 'router_state') | map(attribute='varvalue') | first | default }}"

  - platform: rest
    name: Speedport Smart 4 - DSL-Link-Status
    resource: !secret Speedport_Smart_4_json_link
    scan_interval: 60
    value_template: "{{ value_json | selectattr('varid', 'eq', 'dsl_link_status') | map(attribute='varvalue') | first | default }}"

  - platform: rest
    name: Speedport Smart 4 - Online-Status
    resource: !secret Speedport_Smart_4_json_link
    scan_interval: 60
    value_template: "{{ value_json | selectattr('varid', 'eq', 'onlinestatus') | map(attribute='varvalue') | first | default }}"

  - platform: rest
    name: Speedport Smart 4 - Tage online
    resource: !secret Speedport_Smart_4_json_link
    scan_interval: 60
    value_template: "{{ value_json | selectattr('varid', 'eq', 'days_online') | map(attribute='varvalue') | first | default }}"

  - platform: rest
    name: Speedport Smart 4 - Zeit online
    resource: !secret Speedport_Smart_4_json_link
    scan_interval: 60
    value_template: "{{ value_json | selectattr('varid', 'eq', 'time_online') | map(attribute='varvalue') | first | default }}"

  - platform: rest
    name: Speedport Smart 4 - Internet online seit
    resource: !secret Speedport_Smart_4_json_link
    scan_interval: 60
    value_template: "{{ value_json | selectattr('varid', 'eq', 'inet_uptime') | map(attribute='varvalue') | first | default }}"

  - platform: rest
    name: Speedport Smart 4 - Dual-Stack-Modus
    resource: !secret Speedport_Smart_4_json_link
    scan_interval: 60
    value_template: >
       {% if (value_json | selectattr('varid', 'eq', 'dualstack') | map(attribute='varvalue') | first | default) == "0" %}
         nein
       {% elif (value_json | selectattr('varid', 'eq', 'dualstack') | map(attribute='varvalue') | first | default) == "1" %}
         ja
       {% else %}
         "{{ value_json | selectattr('varid', 'eq', 'dualstack') | map(attribute='varvalue') | first | default }}"
       {% endif %}

  - platform: rest
    name: Speedport Smart 4 - DSL-Tunnel
    resource: !secret Speedport_Smart_4_json_link
    scan_interval: 60
    value_template: >
       {% if (value_json | selectattr('varid', 'eq', 'dsl_tunnel') | map(attribute='varvalue') | first | default) == "0" %}
         offline
       {% elif (value_json | selectattr('varid', 'eq', 'dsl_tunnel') | map(attribute='varvalue') | first | default) == "1" %}
         online
       {% else %}
         "{{ value_json | selectattr('varid', 'eq', 'dsl_tunnel') | map(attribute='varvalue') | first | default }}"
       {% endif %}

  - platform: rest
    name: Speedport Smart 4 - LTE-Tunnel
    resource: !secret Speedport_Smart_4_json_link
    scan_interval: 60
    value_template: >
       {% if (value_json | selectattr('varid', 'eq', 'lte_tunnel') | map(attribute='varvalue') | first | default) == "0" %}
         offline
       {% elif (value_json | selectattr('varid', 'eq', 'lte_tunnel') | map(attribute='varvalue') | first | default) == "1" %}
         online
       {% else %}
         "{{ value_json | selectattr('varid', 'eq', 'lte_tunnel') | map(attribute='varvalue') | first | default }}"
       {% endif %}

  - platform: rest
    name: Speedport Smart 4 - Hybrid-Tunnel
    resource: !secret Speedport_Smart_4_json_link
    scan_interval: 60
    value_template: >
       {% if (value_json | selectattr('varid', 'eq', 'hybrid_tunnel') | map(attribute='varvalue') | first | default) == "0" %}
         offline
       {% elif (value_json | selectattr('varid', 'eq', 'hybrid_tunnel') | map(attribute='varvalue') | first | default) == "1" %}
         online
       {% else %}
         "{{ value_json | selectattr('varid', 'eq', 'hybrid_tunnel') | map(attribute='varvalue') | first | default }}"
       {% endif %}

  - platform: rest
    name: Speedport Smart 4 - Domain-Name
    resource: !secret Speedport_Smart_4_json_link
    scan_interval: 60
    value_template: "{{ value_json | selectattr('varid', 'eq', 'domain_name') | map(attribute='varvalue') | first | default }}"

  - platform: rest
    name: Speedport Smart 4 - Status
    resource: !secret Speedport_Smart_4_json_link
    scan_interval: 60
    value_template: "{{ value_json | selectattr('varid', 'eq', 'status') | map(attribute='varvalue') | first | default }}"

  - platform: rest
    name: Speedport Smart 4 - Datum + Zeit
    resource: !secret Speedport_Smart_4_json_link
    scan_interval: 60
    value_template: "{{ value_json | selectattr('varid', 'eq', 'datetime') | map(attribute='varvalue') | first | default }}"

  - platform: rest
    name: Speedport Smart 4 - Firmware-Version
    resource: !secret Speedport_Smart_4_json_link
    scan_interval: 60
    value_template: "{{ value_json | selectattr('varid', 'eq', 'firmware_version') | map(attribute='varvalue') | first | default }}"

  - platform: rest
    name: Speedport Smart 4 - Serien-Nummer
    resource: !secret Speedport_Smart_4_json_link
    scan_interval: 60
    value_template: "{{ value_json | selectattr('varid', 'eq', 'serial_number') | map(attribute='varvalue') | first | default }}"

  - platform: rest
    name: Speedport Smart 4 - DSL-Downstream
    resource: !secret Speedport_Smart_4_json_link
    scan_interval: 60
    value_template: "{{ value_json | selectattr('varid', 'eq', 'dsl_downstream') | map(attribute='varvalue') | first | default | float/1000000 }}"
    unit_of_measurement: "MBit/s"

  - platform: rest
    name: Speedport Smart 4 - DSL-Upstream
    resource: !secret Speedport_Smart_4_json_link
    scan_interval: 60
    value_template: "{{ value_json | selectattr('varid', 'eq', 'dsl_upstream') | map(attribute='varvalue') | first | default | float/1000000 }}"
    unit_of_measurement: "MBit/s"

  - platform: rest
    name: Speedport Smart 4 - DSL-PoP
    resource: !secret Speedport_Smart_4_json_link
    scan_interval: 60
    value_template: "{{ value_json | selectattr('varid', 'eq', 'dsl_pop') | map(attribute='varvalue') | first | default }}"

  - platform: rest
    name: Speedport Smart 4 - WLAN-SSID (2,4 GHz)
    resource: !secret Speedport_Smart_4_json_link
    scan_interval: 60
    value_template: "{{ value_json | selectattr('varid', 'eq', 'wlan_ssid') | map(attribute='varvalue') | first | default }}"

  - platform: rest
    name: Speedport Smart 4 - WLAN-SSID (5 GHz)
    resource: !secret Speedport_Smart_4_json_link
    scan_interval: 60
    value_template: "{{ value_json | selectattr('varid', 'eq', 'wlan_5ghz_ssid') | map(attribute='varvalue') | first | default }}"

7. Restart HomeAssistant - your sensors should now be ready.

There are even more sensors available than configured here - I will configure them as soon as my hybrid 5G-DSL-internet access has been installed in May.

Best regards,
David

EDIT 17.04.:
Changed the way the JSON values are read out in order to make sensor output more reliable.

1 Like

Happy to see that my script is helpful! It shoudn’t be necessary though to modify the script. Instead, you can call it like this and use my unmodified version:

command: "/config/python_scripts/smart4-vdsl.py --hostname 192.168.1.1 --format raw > /config/www/json/Speedport_Smart_4_Status.json"

Let me know if this works!

1 Like

Aaron, thanks for your hint - you’re correct, there is no need to modify your script!
I’ve modified my guide posted above.

Still, I needed to do a small modification to your syntax in order to get it working:

command: "python3 /config/python_scripts/smart4-vdsl.py --hostname 192.168.1.1 --format raw > /config/www/json/Speedport_Smart_4_Status.json"

Without “python3”, the json file is generated, but its size stays at 0 kb and the file is empty.
With “python3”, it gets a size of 10 kb and its content is correct.

@aaronk :
Is there also a possibility to reboot the router via script?

Is the script executable? If not, you can make it executable with chmod +x /config/python_scripts/smart4-vdsl.py. Then it shoudn’t be necessary to invoke it with python3 (thanks to the so-called she-bang in the script).

Is there also a possibility to reboot the router via script?

In theory, yes, but the script would need to authenticate with the router’s credentials. I don’t have any plans to implement that as I power my Speedport via PoE and can just power-cycle the PoE port on the switch if needed. :slight_smile: Out of curiosity, when would you need to restart the router via script?

Thanks for the hint - I will check that later and report back!

I have a DSL line that offers ~ 70 MBit/s and is of poor quality - the speed is reduced when it’s raining outside (no, I’m not kidding…). My FRITZ!Box 7590 AX was able to re-negotiate the speed on its own when it realized that the line values became better again.

In contrast, the Speedport Smart 4 reduces the DSL sync but doesn’t re-negotiate automatically to obtain faster syncs (although the “Attainable Data Rate” seen here has become better).

Rebooting the Speedport solves the problem as it is then forced to re-sync.

The FRITZ! plugin for Home Assistant offers a button to reboot FRITZ! devices - I was just curious if there is a similar possibility for the Speedport Smart 4 :smiley:

Sorry, haven’t been able to test it earlier.
Logged in via SSH, made the script executable.

Same effect - a new file is created, but its size stay at 0 kb and the file is empty.

You could check if there are any errors shown in the Home Assistant log. Or log in via SSH and execute the script manually and see if it gives you an error message.

But it really doesn’t matter after all, it doesn’t hurt to just call it via python3. :slight_smile:

hi @DFS-90! I still have issues to get the sensor working.

Just done all the steps which you described but still receiving “Unbekannt” in the sensor card.

to make this clear:

In the python script I need to adjust the DEFAULT_HOSTNAME with my router IP correct?
Is the DEFAULT_KEY also the same?

Also in the Speedport_Smart_4.yaml, do I need to change the --hostname in the command as well to my router IP?

Last one: for the secret.yaml: can I use https://homeassistant.local:8123/local etc. as domain or is this uncorrect?

sorry for so many question, still a newbie to HA :sweat_smile:

thanks in advance!

I added a few sensors specifically for the 5G Hybrid option. It would also be nice to get the cell id but I haven’t found an entry for that.

  - platform: rest
    name: Speedport Smart 4 - 5G Signal
    resource: !secret Speedport_Smart_4_json_link
    scan_interval: 60
    value_template: "{{ value_json | selectattr('varid', 'eq', 'ex5g_signal_5g') | map(attribute='varvalue') | first | default }}"
    unit_of_measurement: "dBm"
    
  - platform: rest
    name: Speedport Smart 4 - 5G Frequenz
    resource: !secret Speedport_Smart_4_json_link
    scan_interval: 60
    value_template: "{{ value_json | selectattr('varid', 'eq', 'ex5g_freq_5g') | map(attribute='varvalue') | first | default }}"
    
  - platform: rest
    name: Speedport Smart 4 - LTE Signal
    resource: !secret Speedport_Smart_4_json_link
    scan_interval: 60
    value_template: "{{ value_json | selectattr('varid', 'eq', 'ex5g_signal_lte') | map(attribute='varvalue') | first | default }}"
    unit_of_measurement: "dBm"
    
  - platform: rest
    name: Speedport Smart 4 - LTE Frequenz
    resource: !secret Speedport_Smart_4_json_link
    scan_interval: 60
    value_template: "{{ value_json | selectattr('varid', 'eq', 'ex5g_freq_lte') | map(attribute='varvalue') | first | default }}"

Hi @FabianKn7 ,

sorry for my late reply.

You don’t need to modify the script at all.
Just enter your router IP in this step (see above):

# Python-Aufruf

  - platform: command_line
    name: Speedport Smart 4 - Python-Script-Aufruf
    command: "python3 /config/python_scripts/smart4-vdsl.py --hostname 192.168.1.1 --format raw > /config/www/json/Speedport_Smart_4_Status.json"
    scan_interval: 45

You need to choose a domain name that can be found in your local network - this depends e. g. on your router / your dns system.

In my case, I decided to make my server publicly available - that’s why I entered my external domain here.
In theory, it should also work if you enter https://YOUR_HOME_ASSISTANT_IP_ADDRESS:8123/local etc.

(But then it might be necessary to add the parameter “verify_ssl: false” .)

Best regards,
David

Hello There,

I had some trouble getting the Speedport_Smart_4_Status.json filled with data.
It was created but stayed empty.

Searching for the reason showed up the timeout for the http request was slightly to low with 5 seconds.
Changing that to 10 Seconds did the trick.

Speedport Smart 4
Delivered Oct 23
Firmware 010139.3.3.001.2

Thank you so much for this.
I just had to switch from a FritzBox to the Speedport because of the Hybrid possibility and the worse internet around here and I’m happy to be able to still see my beloved data :wink:

Hi, I created a new custom integration for Telekom Speedport: GitHub - Andre0512/speedport: Home Assistant integration for Telekom Speedport
Would be glad if someone can test it :slightly_smiling_face:

Hi Andre,
just installed it this morning - thanks for your amazing work! :smiley:
I found some issues that I already reported in your GitHub repo.

I will update my first post and place a link to your project as installing a HACS add-on is so much easier than configuring sensors… :wink:

Maybe you can enhance your add-on by adding sensors for the 5G receiver also?

Best regards,
David

Hi David, thanks for your feedback and your support! :smiley:
I have added the 5G sensors, check out v0.3.3

I just tried to use the custom integration. But I have troubles connecting.
My Speedport smart 4 has IP 192.168.1.1 and SSL enabled.
The config flow is successful with these values.
But when I then check the integration it says: Error when creating the integration.
In the debug logs I can see that the integration is trying to connect on port 80 and this fails. Shouldnt it connect on port 443, since I checked https box?