From 0341ac02022a5601b67a872739ed679fee46f073 Mon Sep 17 00:00:00 2001 From: Jeroen Oudshoorn Date: Sat, 8 Mar 2025 09:08:52 +0100 Subject: [PATCH] Changed TOML format and parser Signed-off-by: Jeroen Oudshoorn --- pwnagotchi/cli.py | 35 ++-- pwnagotchi/defaults.toml | 352 +++++++++++++++++++++----------------- pwnagotchi/plugins/cmd.py | 7 +- pwnagotchi/utils.py | 65 ++----- pyproject.toml | 2 +- 5 files changed, 230 insertions(+), 231 deletions(-) diff --git a/pwnagotchi/cli.py b/pwnagotchi/cli.py index 0fff5719..b2b1bb12 100644 --- a/pwnagotchi/cli.py +++ b/pwnagotchi/cli.py @@ -3,7 +3,7 @@ import argparse import time import signal import sys -import toml +import tomlkit import requests import os import re @@ -14,7 +14,7 @@ from pwnagotchi.google import cmd as google_cmd from pwnagotchi.plugins import cmd as plugins_cmd from pwnagotchi import log from pwnagotchi import fs -from pwnagotchi.utils import DottedTomlEncoder, parse_version as version_to_tuple +from pwnagotchi.utils import parse_version as version_to_tuple def pwnagotchi_cli(): @@ -187,12 +187,14 @@ def pwnagotchi_cli(): if pwn_name == "": pwn_name = "Pwnagotchi" print("I shall go by Pwnagotchi from now on!") - pwn_name = f"main.name = \"{pwn_name}\"\n" + pwn_name = (f"[main]\n" + f"name = \"{pwn_name}\"\n") f.write(pwn_name) else: if is_valid_hostname(pwn_name): print(f"I shall go by {pwn_name} from now on!") - pwn_name = f"main.name = \"{pwn_name}\"\n" + pwn_name = (f"[main]\n" + f"name = \"{pwn_name}\"\n") f.write(pwn_name) else: print("You have chosen an invalid name. Please start over.") @@ -203,7 +205,7 @@ def pwnagotchi_cli(): "Be sure to use digits as your answer.\n\n" "Amount of networks: ") if int(pwn_whitelist) > 0: - f.write("main.whitelist = [\n") + f.write("whitelist = [\n") for x in range(int(pwn_whitelist)): ssid = input("SSID (Name): ") bssid = input("BSSID (MAC): ") @@ -215,42 +217,45 @@ def pwnagotchi_cli(): pwn_bluetooth = input("Do you want to enable BT-Tether?\n\n" "[Y/N] ") if pwn_bluetooth.lower() in ('y', 'yes'): - f.write("main.plugins.bt-tether.enabled = true\n\n") + f.write("[main.plugins.bt-tether]\n" + "enabled = true\n\n") pwn_bluetooth_phone_name = input("What name uses your phone, check settings?\n\n") if pwn_bluetooth_phone_name != "": - f.write(f"main.plugins.bt-tether.phone-name = \"{pwn_bluetooth_phone_name}\"\n") + f.write(f"phone-name = \"{pwn_bluetooth_phone_name}\"\n") pwn_bluetooth_device = input("What device do you use? android or ios?\n\n" "Device: ") if pwn_bluetooth_device != "": if pwn_bluetooth_device != "android" and pwn_bluetooth_device != "ios": print("You have chosen an invalid device. Please start over.") exit() - f.write(f"main.plugins.bt-tether.phone = \"{pwn_bluetooth_device.lower()}\"\n") + f.write(f"phone = \"{pwn_bluetooth_device.lower()}\"\n") if pwn_bluetooth_device == "android": - f.write("main.plugins.bt-tether.ip = \"192.168.44.44\"\n") + f.write("ip = \"192.168.44.44\"\n") elif pwn_bluetooth_device == "ios": - f.write("main.plugins.bt-tether.ip = \"172.20.10.6\"\n") + f.write("ip = \"172.20.10.6\"\n") pwn_bluetooth_mac = input("What is the bluetooth MAC of your device?\n\n" "MAC: ") if pwn_bluetooth_mac != "": - f.write(f"main.plugins.bt-tether.mac = \"{pwn_bluetooth_mac}\"\n") + f.write(f"mac = \"{pwn_bluetooth_mac}\"\n") # set up display settings pwn_display_enabled = input("Do you want to enable a display?\n\n" "[Y/N]: ") if pwn_display_enabled.lower() in ('y', 'yes'): - f.write("ui.display.enabled = true\n") + f.write("[ui.display]\n" + "enabled = true\n") pwn_display_type = input("What display do you use?\n\n" "Be sure to check for the correct display type @ \n" "https://github.com/jayofelony/pwnagotchi/blob/master/pwnagotchi/utils.py#L240-L501\n\n" "Display type: ") if pwn_display_type != "": - f.write(f"ui.display.type = \"{pwn_display_type}\"\n") + f.write(f"type = \"{pwn_display_type}\"\n") pwn_display_invert = input("Do you want to invert the display colors?\n" "N = Black background\n" "Y = White background\n\n" "[Y/N]: ") if pwn_display_invert.lower() in ('y', 'yes'): - f.write("ui.invert = true\n") + f.write("[ui]\n" + "invert = true\n") f.close() if pwn_bluetooth.lower() in ('y', 'yes'): if pwn_bluetooth_device.lower == "android": @@ -300,7 +305,7 @@ def pwnagotchi_cli(): config = utils.load_config(args) if args.print_config: - print(toml.dumps(config, encoder=DottedTomlEncoder())) + print(tomlkit.dumps(config)) sys.exit(0) from pwnagotchi.identity import KeyPair diff --git a/pwnagotchi/defaults.toml b/pwnagotchi/defaults.toml index cb111515..cc3663cb 100644 --- a/pwnagotchi/defaults.toml +++ b/pwnagotchi/defaults.toml @@ -1,22 +1,25 @@ -main.name = "pwnagotchi" -main.lang = "en" -main.whitelist = [ +[main] +name = "pwnagotchi" +lang = "en" +whitelist = [ "EXAMPLE_NETWORK", "ANOTHER_EXAMPLE_NETWORK", "fo:od:ba:be:fo:od", "fo:od:ba" ] -main.confd = "/etc/pwnagotchi/conf.d/" -main.custom_plugin_repos = [ +confd = "/etc/pwnagotchi/conf.d/" +custom_plugin_repos = [ "https://github.com/jayofelony/pwnagotchi-torch-plugins/archive/master.zip", "https://github.com/Sniffleupagus/pwnagotchi_plugins/archive/master.zip", "https://github.com/NeonLightning/pwny/archive/master.zip", "https://github.com/marbasec/UPSLite_Plugin_1_3/archive/master.zip", "https://github.com/wpa-2/Pwnagotchi-Plugins/archive/master.zip", - "https://github.com/cyberartemio/wardriver-pwnagotchi-plugin/archive/main.zip", + "https://github.com/cyberartemio/wardriver-pwnagotchi-plugin/archive/main.zip" ] +custom_plugins = "/usr/local/share/pwnagotchi/custom-plugins/" -main.custom_plugins = "/usr/local/share/pwnagotchi/custom-plugins/" +[main.plugins.auto-tune] +enabled = true main.plugins.auto_backup.enabled = true main.plugins.auto_backup.interval = "daily" # or "hourly", or a number (minutes) @@ -41,172 +44,203 @@ main.plugins.auto_backup.files = [ main.plugins.auto_backup.exclude = [ "/etc/pwnagotchi/logs/*",] main.plugins.auto_backup.commands = [ "tar cf {backup_file} {files}",] -main.plugins.auto-tune.enabled = true +[main.plugins.auto-update] +enabled = true +install = true +interval = 1 +token = "" # Create a personal access token (classic) with scope set to public_repo to use the GitHub API -main.plugins.auto-update.enabled = true -main.plugins.auto-update.install = true -main.plugins.auto-update.interval = 1 +[main.plugins.bt-tether] +enabled = false +phone-name = "" # name as shown on the phone i.e. "Pwnagotchi's Phone" +mac = "" +phone = "" # android or ios +ip = "" # optional, default : 192.168.44.2 if android or 172.20.10.2 if ios +dns = "8.8.8.8 1.1.1.1" # optional, default (google): "8.8.8.8 1.1.1.1". Consider using anonymous DNS like OpenNic :-) -main.plugins.bt-tether.enabled = false -main.plugins.bt-tether.phone-name = "" # name as shown on the phone i.e. "Pwnagotchi's Phone" -main.plugins.bt-tether.mac = "" -main.plugins.bt-tether.phone = "" # android or ios -main.plugins.bt-tether.ip = "" # optional, default : 192.168.44.2 if android or 172.20.10.2 if ios -main.plugins.bt-tether.dns = "" # optional, default (google): "8.8.8.8 1.1.1.1". Consider using anonymous DNS like OpenNic :-) +[main.plugins.fix_services] +enabled = true -main.plugins.fix_services.enabled = true +[main.plugins.cache] +enabled = true -main.plugins.cache.enabled = true +[main.plugins.gdrivesync] +enabled = false +backupfiles = [""] +backup_folder = "PwnagotchiBackups" +interval = 1 -main.plugins.gdrivesync.enabled = false -main.plugins.gdrivesync.backupfiles = [''] -main.plugins.gdrivesync.backup_folder = "PwnagotchiBackups" -main.plugins.gdrivesync.interval = 1 +[main.plugins.gpio_buttons] +enabled = false -main.plugins.gpio_buttons.enabled = false +[main.plugins.gps] +enabled = false +speed = 19200 +device = "/dev/ttyUSB0" # for GPSD: "localhost:2947" -main.plugins.gps.enabled = false -main.plugins.gps.speed = 19200 -main.plugins.gps.device = "/dev/ttyUSB0" # for GPSD: "localhost:2947" +[main.plugins.gps_listener] +enabled = false -main.plugins.gps_listener.enabled = false +[main.plugins.grid] +enabled = true +report = true -main.plugins.grid.enabled = true -main.plugins.grid.report = true +[main.plugins.logtail] +enabled = false +max-lines = 10000 -main.plugins.logtail.enabled = false -main.plugins.logtail.max-lines = 10000 +[main.plugins.memtemp] +enabled = false +scale = "celsius" +orientation = "horizontal" -main.plugins.memtemp.enabled = false -main.plugins.memtemp.scale = "celsius" -main.plugins.memtemp.orientation = "horizontal" +[main.plugins.ohcapi] +enabled = false +api_key = "sk_your_api_key_here" +receive_email = "yes" -main.plugins.ohcapi.enabled = false -main.plugins.ohcapi.api_key = "sk_your_api_key_here" -main.plugins.ohcapi.receive_email = "yes" +[main.plugins.pwndroid] +enabled = false +display = false # show coords on display +display_altitude = false # show altitude on display -main.plugins.pwndroid.enabled = false -main.plugins.pwndroid.display = false # show coords on display -main.plugins.pwndroid.display_altitude = false # show altitude on display +[main.plugins.pisugarx] +enabled = false +rotation = false +default_display = "percentage" +lowpower_shutdown = true +lowpower_shutdown_level = 10 # battery percent at which the device will turn off +max_charge_voltage_protection = false #It will limit the battery voltage to about 80% to extend battery life -main.plugins.pisugarx.enabled = false -main.plugins.pisugarx.rotation = false -main.plugins.pisugarx.default_display = "percentage" -main.plugins.pisugarx.lowpower_shutdown = true -main.plugins.pisugarx.lowpower_shutdown_level = 10 # battery percent at which the device will turn off -main.plugins.pisugarx.max_charge_voltage_protection = false #It will limit the battery voltage to about 80% to extend battery life +[main.plugins.session-stats] +enabled = false +save_directory = "/var/tmp/pwnagotchi/sessions/" -main.plugins.session-stats.enabled = false -main.plugins.session-stats.save_directory = "/var/tmp/pwnagotchi/sessions/" +[main.plugins.ups_hat_c] +enabled = false +label_on = true # show BAT label or just percentage +shutdown = 5 # battery percent at which the device will turn off +bat_x_coord = 140 +bat_y_coord = 0 -main.plugins.ups_hat_c.enabled = false -main.plugins.ups_hat_c.label_on = true # show BAT label or just percentage -main.plugins.ups_hat_c.shutdown = 5 # battery percent at which the device will turn off -main.plugins.ups_hat_c.bat_x_coord = 140 -main.plugins.ups_hat_c.bat_y_coord = 0 +[main.plugins.ups_lite] +enabled = false +shutdown = 2 -main.plugins.ups_lite.enabled = false -main.plugins.ups_lite.shutdown = 2 +[main.plugins.webcfg] +enabled = true -main.plugins.webcfg.enabled = true +[main.plugins.webgpsmap] +enabled = false -main.plugins.webgpsmap.enabled = false +[main.plugins.wigle] +enabled = false +api_key = "" # mandatory +cvs_dir = "/tmp" # optionnal, is set, the CVS is written to this directory +donate = false # default: off +timeout = 30 # default: 30 +position = [7, 85] # optionnal -main.plugins.wigle.enabled = false -main.plugins.wigle.api_key = "" # mandatory -main.plugins.wigle.cvs_dir = "/tmp" # optionnal, is set, the CVS is written to this directory -main.plugins.wigle.donate = false # default: off -main.plugins.wigle.timeout = 30 # default: 30 -main.plugins.wigle.position = [7, 85] # optionnal +[main.plugins.wpa-sec] +enabled = false +api_key = "" +api_url = "https://wpa-sec.stanev.org" +download_results = false +show_pwd = false -main.plugins.wpa-sec.enabled = false -main.plugins.wpa-sec.api_key = "" -main.plugins.wpa-sec.api_url = "https://wpa-sec.stanev.org" -main.plugins.wpa-sec.download_results = false -main.plugins.wpa-sec.show_pwd = false +iface = "wlan0mon" +mon_start_cmd = "/usr/bin/monstart" +mon_stop_cmd = "/usr/bin/monstop" +mon_max_blind_epochs = 5 +no_restart = false -main.iface = "wlan0mon" -main.mon_start_cmd = "/usr/bin/monstart" -main.mon_stop_cmd = "/usr/bin/monstop" -main.mon_max_blind_epochs = 5 -main.no_restart = false +[main.log] +path = "/etc/pwnagotchi/log/pwnagotchi.log" +path-debug = "/etc/pwnagotchi/log/pwnagotchi-debug.log" -main.log.path = "/etc/pwnagotchi/log/pwnagotchi.log" -main.log.path-debug = "/etc/pwnagotchi/log/pwnagotchi-debug.log" -main.log.rotation.enabled = true -main.log.rotation.size = "10M" +[main.log.rotation] +enabled = true +size = "10M" -personality.advertise = true -personality.deauth = true -personality.associate = true -personality.channels = [] -personality.min_rssi = -200 -personality.ap_ttl = 120 -personality.sta_ttl = 300 -personality.recon_time = 30 -personality.max_inactive_scale = 2 -personality.recon_inactive_multiplier = 2 -personality.hop_recon_time = 10 -personality.min_recon_time = 5 -personality.max_interactions = 3 -personality.max_misses_for_recon = 5 -personality.excited_num_epochs = 10 -personality.bored_num_epochs = 15 -personality.sad_num_epochs = 25 -personality.bond_encounters_factor = 20000 -personality.throttle_a = 0.4 -personality.throttle_d = 0.9 +[personality] +advertise = true +deauth = true +associate = true +channels = [] +min_rssi = -200 +ap_ttl = 120 +sta_ttl = 300 +recon_time = 30 +max_inactive_scale = 2 +recon_inactive_multiplier = 2 +hop_recon_time = 10 +min_recon_time = 5 +max_interactions = 3 +max_misses_for_recon = 5 +excited_num_epochs = 10 +bored_num_epochs = 15 +sad_num_epochs = 25 +bond_encounters_factor = 20000 +throttle_a = 0.4 +throttle_d = 0.9 -ui.invert = false # false = black background, true = white background -ui.cursor = true -ui.fps = 0.0 -ui.font.name = "DejaVuSansMono" # for japanese: fonts-japanese-gothic -ui.font.size_offset = 0 # will be added to the font size +[ui] +invert = false # false = black background, true = white background +cursor = true +fps = 0.0 -ui.faces.look_r = "( ⚆_⚆)" -ui.faces.look_l = "(☉_☉ )" -ui.faces.look_r_happy = "( ◕‿◕)" -ui.faces.look_l_happy = "(◕‿◕ )" -ui.faces.sleep = "(⇀‿‿↼)" -ui.faces.sleep2 = "(≖‿‿≖)" -ui.faces.awake = "(◕‿‿◕)" -ui.faces.bored = "(-__-)" -ui.faces.intense = "(°▃▃°)" -ui.faces.cool = "(⌐■_■)" -ui.faces.happy = "(•‿‿•)" -ui.faces.excited = "(ᵔ◡◡ᵔ)" -ui.faces.grateful = "(^‿‿^)" -ui.faces.motivated = "(☼‿‿☼)" -ui.faces.demotivated = "(≖__≖)" -ui.faces.smart = "(✜‿‿✜)" -ui.faces.lonely = "(ب__ب)" -ui.faces.sad = "(╥☁╥ )" -ui.faces.angry = "(-_-')" -ui.faces.friend = "(♥‿‿♥)" -ui.faces.broken = "(☓‿‿☓)" -ui.faces.debug = "(#__#)" -ui.faces.upload = "(1__0)" -ui.faces.upload1 = "(1__1)" -ui.faces.upload2 = "(0__1)" -ui.faces.png = false -ui.faces.position_x = 0 -ui.faces.position_y = 34 +[ui.font] +name = "DejaVuSansMono" # for japanese: fonts-japanese-gothic +size_offset = 0 # will be added to the font size -ui.web.enabled = true -ui.web.address = "::" # listening on both ipv4 and ipv6 - switch to 0.0.0.0 to listen on just ipv4 -ui.web.auth = false -ui.web.username = "changeme" # if auth is true -ui.web.password = "changeme" # if auth is true -ui.web.origin = "" -ui.web.port = 8080 -ui.web.on_frame = "" +[ui.faces] +look_r = "( ⚆_⚆)" +look_l = "(☉_☉ )" +look_r_happy = "( ◕‿◕)" +look_l_happy = "(◕‿◕ )" +sleep = "(⇀‿‿↼)" +sleep2 = "(≖‿‿≖)" +awake = "(◕‿‿◕)" +bored = "(-__-)" +intense = "(°▃▃°)" +cool = "(⌐■_■)" +happy = "(•‿‿•)" +excited = "(ᵔ◡◡ᵔ)" +grateful = "(^‿‿^)" +motivated = "(☼‿‿☼)" +demotivated = "(≖__≖)" +smart = "(✜‿‿✜)" +lonely = "(ب__ب)" +sad = "(╥☁╥ )" +angry = "(-_-')" +friend = "(♥‿‿♥)" +broken = "(☓‿‿☓)" +debug = "(#__#)" +upload = "(1__0)" +upload1 = "(1__1)" +upload2 = "(0__1)" +png = false +position_x = 0 +position_y = 34 -ui.display.enabled = false -ui.display.rotation = 180 -ui.display.type = "waveshare_4" +[ui.web] +enabled = true +address = "::" # listening on both ipv4 and ipv6 - switch to 0.0.0.0 to listen on just ipv4 +auth = false +username = "changeme" # if auth is true +password = "changeme" # if auth is true +origin = "" +port = 8080 +on_frame = "" -bettercap.handshakes = "/home/pi/handshakes" -bettercap.silence = [ +[ui.display] +enabled = false +rotation = 180 +type = "waveshare_4" + +[bettercap] +handshakes = "/home/pi/handshakes" +silence = [ "ble.device.new", "ble.device.lost", "ble.device.service.discovered", @@ -222,17 +256,21 @@ bettercap.silence = [ "mod.started" ] -fs.memory.enabled = true -fs.memory.mounts.log.enabled = true -fs.memory.mounts.log.mount = "/etc/pwnagotchi/log/" -fs.memory.mounts.log.size = "50M" -fs.memory.mounts.log.sync = 60 -fs.memory.mounts.log.zram = true -fs.memory.mounts.log.rsync = true +[fs.memory] +enabled = true -fs.memory.mounts.data.enabled = true -fs.memory.mounts.data.mount = "/var/tmp/pwnagotchi" -fs.memory.mounts.data.size = "10M" -fs.memory.mounts.data.sync = 3600 -fs.memory.mounts.data.zram = true -fs.memory.mounts.data.rsync = true +[fs.memory.mounts.log] +enabled = true +mount = "/etc/pwnagotchi/log/" +size = "50M" +sync = 60 +zram = true +rsync = true + +[fs.memory.mounts.data] +enabled = true +mount = "/var/tmp/pwnagotchi" +size = "10M" +sync = 3600 +zram = true +rsync = true \ No newline at end of file diff --git a/pwnagotchi/plugins/cmd.py b/pwnagotchi/plugins/cmd.py index 92c3bcb6..ddbd494f 100644 --- a/pwnagotchi/plugins/cmd.py +++ b/pwnagotchi/plugins/cmd.py @@ -106,20 +106,19 @@ def edit(args, config): plugin_config = {'main': {'plugins': {plugin: config['main']['plugins'][plugin]}}} - import toml + import tomlkit from subprocess import call from tempfile import NamedTemporaryFile - from pwnagotchi.utils import DottedTomlEncoder new_plugin_config = None with NamedTemporaryFile(suffix=".tmp", mode='r+t') as tmp: - tmp.write(toml.dumps(plugin_config, encoder=DottedTomlEncoder())) + tmp.write(tomlkit.dumps(plugin_config)) tmp.flush() rc = call([editor, tmp.name]) if rc != 0: return rc tmp.seek(0) - new_plugin_config = toml.load(tmp) + new_plugin_config = tomlkit.load(tmp) config['main']['plugins'][plugin] = new_plugin_config['main']['plugins'][plugin] save_config(config, args.user_config) diff --git a/pwnagotchi/utils.py b/pwnagotchi/utils.py index 26f507a8..bce1e3d4 100644 --- a/pwnagotchi/utils.py +++ b/pwnagotchi/utils.py @@ -5,61 +5,13 @@ import subprocess import json import shutil -import toml import sys -import re +import tomlkit -from toml.encoder import TomlEncoder, _dump_str from zipfile import ZipFile from datetime import datetime from enum import Enum - -class DottedTomlEncoder(TomlEncoder): - """ - Dumps the toml into the dotted-key format - """ - - def __init__(self, _dict=dict): - super(DottedTomlEncoder, self).__init__(_dict) - - def dump_list(self, v): - retval = "[" - # 1 line if its just 1 item; therefore no newline - if len(v) > 1: - retval += "\n" - for u in v: - retval += " " + str(self.dump_value(u)) + ",\n" - # 1 line if its just 1 item; remove newline - if len(v) <= 1: - retval = retval.rstrip("\n") - retval += "]" - return retval - - def dump_sections(self, o, sup): - retstr = "" - pre = "" - - if sup: - pre = sup + "." - - for section, value in o.items(): - section = str(section) - qsection = section - if not re.match(r'^[A-Za-z0-9_-]+$', section): - qsection = _dump_str(section) - if value is not None: - if isinstance(value, dict): - toadd, _ = self.dump_sections(value, pre + qsection) - retstr += toadd - # separte sections - if not retstr.endswith('\n\n'): - retstr += '\n' - else: - retstr += (pre + qsection + " = " + str(self.dump_value(value)) + '\n') - return retstr, self._dict() - - def parse_version(version): """ Converts a version str to tuple, so that versions can be compared @@ -150,7 +102,8 @@ def keys_to_str(data): def save_config(config, target): with open(target, 'wt') as fp: - fp.write(toml.dumps(config, encoder=DottedTomlEncoder())) + fp.write(tomlkit.dumps(config)) + #fp.write(toml.dumps(config, encoder=DottedTomlEncoder())) return True @@ -200,7 +153,8 @@ def load_config(args): # load the defaults with open(args.config) as fp: - config = toml.load(fp) + config = tomlkit.load(fp) + #config = toml.load(fp) # load the user config try: @@ -216,10 +170,12 @@ def load_config(args): # convert int/float keys to str user_config = keys_to_str(user_config) # convert to toml but use loaded yaml - toml.dump(user_config, toml_file) + # toml.dump(user_config, toml_file) + tomlkit.dump(user_config, toml_file) elif os.path.exists(args.user_config): with open(args.user_config) as toml_file: - user_config = toml.load(toml_file) + # user_config = toml.load(toml_file) + user_config = tomlkit.load(toml_file) if user_config: config = merge_config(user_config, config) @@ -233,7 +189,8 @@ def load_config(args): dropin += '*.toml' if dropin.endswith('/') else '/*.toml' # only toml here; yaml is no more for conf in glob.glob(dropin): with open(conf) as toml_file: - additional_config = toml.load(toml_file) + # additional_config = toml.load(toml_file) + additional_config = tomlkit.load(toml_file) config = merge_config(additional_config, config) # the very first step is to normalize the display name, so we don't need dozens of if/elif around diff --git a/pyproject.toml b/pyproject.toml index eafe25f1..24d04df7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,7 +9,7 @@ dependencies = [ "PyYAML", "dbus-python", "file-read-backwards", "flask", "flask-cors", "flask-wtf", "gast", "gpiozero", "inky", "numpy", "pycryptodome", "pydrive2", "python-dateutil", "requests", "rpi-lgpio", "rpi_hardware_pwm", "scapy", "setuptools", "shimmy", "smbus", "smbus2", - "spidev", "toml", "tweepy", "websockets", "pisugar", + "spidev", "tomlkit", "tweepy", "websockets", "pisugar", ] requires-python = ">=3.11"