diff --git a/config.py b/config.py
index 1f4799f8f4f96f5e918c48944ab0c97aabdbb96f..a318d9cebe2210655aee510d8955cf2906a0720c 100644
--- a/config.py
+++ b/config.py
@@ -7,9 +7,10 @@ PROP_WINDOW_FULLSCREEN = False
 PROP_WINDOW_SCALE = 3
 
 PROP_AVAILABLE_WIDGETS = [
+    "IPShowWidget",
+    "TestWidget",
     "AnalogClockWidget",
-    "DigitalClockWidget",
-    "TestWidget"
+    "DigitalClockWidget"
 ]
 
 PROP_AVAILABLE_PATTERNS = [
diff --git a/main.py b/main.py
index 4c94a397f4995a55573f607cccf9e47e1737bc14..83394fe7398a83d020a0814f2adb2c90b77edd7b 100644
--- a/main.py
+++ b/main.py
@@ -36,19 +36,24 @@ if os.path.exists("module_config.json"):
 with open("module_config.json", "w") as f:
     json.dump(module_config, f, indent=4)
 
+mainthread_queue = queue.Queue()
 api_queue = queue.Queue()
 led_queue = queue.Queue()
 
 @app.route("/api/available_fonts")
-def available_fonts():
+def wr_available_fonts():
     return Response(json.dumps(pygame.font.get_fonts()), mimetype="application/json")
 
 @app.route("/api/available_widgets")
-def available_widgets():
+def wr_available_widgets():
     return Response(json.dumps(PROP_AVAILABLE_WIDGETS), mimetype="application/json")
 
+@app.route("/api/available_patterns")
+def wr_available_patterns():
+    return Response(json.dumps(PROP_AVAILABLE_PATTERNS), mimetype="application/json")
+
 @app.route("/api/set_widget", methods=["POST"])
-def set_widget():
+def wr_set_widget():
     widget_name = request.args.get("widget")
     if widget_name is None:
         return "No widget provided", 400
@@ -60,6 +65,23 @@ def set_widget():
 
     return "OK"
 
+@app.route("/api/set_pattern", methods=["POST"])
+def wr_set_pattern():
+    pattern_name = request.args.get("pattern")
+    if pattern_name is None:
+        return "No pattern provided", 400
+
+    if pattern_name not in PROP_AVAILABLE_PATTERNS:
+        return f"Pattern {pattern_name} is not available", 400
+
+    led_queue.put("SET_PATTERN:" + pattern_name)
+
+    return "OK"
+
+@app.route("/api/get_config")
+def wr_get_config():
+    return Response(json.dumps(module_config), mimetype="application/json")
+
 def load_widget(widget_name: str):
     # Get the widget properties from the config file
     widget_props = module_config.get(widget_name, {}).copy()
@@ -122,6 +144,9 @@ def run_screen():
         # Update the screen
         pygame.display.flip()
 
+    # Send exit message to main thread
+    mainthread_queue.put("EXIT")
+
     # Quit Pygame
     pygame.quit()
 
@@ -149,6 +174,8 @@ def run_leds():
                     current_pattern = load_pattern(pattern_name, leds)
                     pattern_timer = module_config.get(pattern_name, {}).get("tick_rate", -1)
                     current_pattern.tick()
+            elif message == "EXIT":
+                break
         except queue.Empty:
             pass
 
@@ -175,7 +202,16 @@ if __name__ == "__main__":
     # Wait for the threads to finish
     try:
         while True:
+            try:
+                message = mainthread_queue.get_nowait()
+                if message == "EXIT":
+                    api_queue.put("EXIT")
+                    led_queue.put("EXIT")
+                    break
+            except queue.Empty:
+                pass
             # Keep main thread alive
-            time.sleep(1000)
+            time.sleep(1)
     except KeyboardInterrupt:
-        exit()
\ No newline at end of file
+        exit()
+    exit()
\ No newline at end of file
diff --git a/module_defaults.json b/module_defaults.json
index de3b8f70d7d863e5e5d598b5347f5e40de692cf1..c25c3ba950faa9a18c7f7da670d72aa6bb6a82f1 100644
--- a/module_defaults.json
+++ b/module_defaults.json
@@ -197,6 +197,63 @@
             "type": "filepath"
         }
     },
+    "IPShowWidget": {
+        "scalable_font_size": {
+            "name": "Font size",
+            "description": "The font size of the clock.",
+            "default": 20,
+            "type": "integer"
+        },
+        "font_color": {
+            "name": "Font colour",
+            "description": "The colour of the font.",
+            "default": "#cccccc",
+            "type": "color"
+        },
+        "font_name": {
+            "name": "Font name",
+            "description": "The name of the font to use.",
+            "default": null,
+            "type": "font"
+        },
+        "font_bold": {
+            "name": "Font bold",
+            "description": "Whether the font should be bold.",
+            "default": false,
+            "type": "boolean"
+        },
+        "font_italic": {
+            "name": "Font italic",
+            "description": "Whether the font should be italic.",
+            "default": false,
+            "type": "boolean"
+        },
+        "draw_background": {
+            "name": "Draw background",
+            "description": "Draw the background of the widget.",
+            "default": true,
+            "type": "boolean"
+        },
+        "background_type": {
+            "name": "Background type",
+            "description": "The type of background to use (`color` or `image`).",
+            "default": "color",
+            "type": "enum",
+            "allowed_values": ["color", "image"]
+        },
+        "background_color": {
+            "name": "Background colour",
+            "description": "The colour of the background.",
+            "default": "#000000",
+            "type": "color"
+        },
+        "background_image": {
+            "name": "Background image",
+            "description": "The image to use as the background.",
+            "default": "",
+            "type": "filepath"
+        }
+    },
     "TestWidget": {
         "color": {
             "name": "Colour",
diff --git a/num_utils.py b/num_utils.py
index 96699966e25bee7a3d9119976a9dbd05ec2327ff..cc177288631b36392789486ecfda2506ca3d73e9 100644
--- a/num_utils.py
+++ b/num_utils.py
@@ -1,5 +1,5 @@
 from config import *
-
+import socket
 
 def get_screen_centre():
     return (PROP_SCREEN_WIDTH * PROP_WINDOW_SCALE) / 2, (PROP_SCREEN_HEIGHT * PROP_WINDOW_SCALE) / 2
@@ -15,3 +15,17 @@ def color_hex_to_tuple(hx: str) -> tuple[int, int, int]:
     """
     hx = hx.removeprefix("#")
     return tuple(int(hx[i:i + 2], 16) for i in (0, 2, 4))
+
+
+def get_ip():
+    s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
+    s.settimeout(0)
+    try:
+        # doesn't even have to be reachable
+        s.connect(('10.254.254.254', 1))
+        ip_addr = s.getsockname()[0]
+    except Exception:
+        ip_addr = '127.0.0.1'
+    finally:
+        s.close()
+    return ip_addr
diff --git a/screenwidgets.py b/screenwidgets.py
index a660703409395e4ab77c3d7e7f14775459096a72..212ba41a4d17c6dd29882388c67437ea1c6eee1a 100644
--- a/screenwidgets.py
+++ b/screenwidgets.py
@@ -2,7 +2,7 @@ import math
 from datetime import datetime
 
 from config import PROP_WINDOW_SCALE, PROP_SCREEN_WIDTH, PROP_SCREEN_HEIGHT
-from num_utils import get_screen_centre
+from num_utils import get_screen_centre, get_ip
 
 
 class IWidgetMeta(type):
@@ -197,3 +197,51 @@ class AnalogClockWidget(IWidget):
         self.clock_hand(screen, get_screen_centre(), self.second_hand_length, second * 6, self.second_hand_thickness, self.second_hand_color)
         self.clock_hand(screen, get_screen_centre(), self.minute_hand_length, minute * 6, self.minute_hand_thickness, self.minute_hand_color)
         self.clock_hand(screen, get_screen_centre(), self.hour_hand_length, hour * 30, self.hour_hand_thickness, self.hour_hand_color)
+
+class IPShowWidget(IWidget):
+    def __init__(self, config: dict):
+        super().__init__(config)
+        self.pygame = config['pygame']
+
+        # Backgrounds
+        self.background_color = config.get("background_color", "black")
+        self.draw_background = config.get("draw_background", False)
+        self.draw_clock_background = config.get("draw_clock_background", True)
+        self.background_type = config.get("background_type", "color")
+        if self.background_type == "image" and config.get("background_image", "") != "":
+            self.bg_image = self.pygame.image.load(config.get("background_image", ""))
+            self.bg_image = self.pygame.transform.scale(self.bg_image, (
+                PROP_SCREEN_WIDTH * PROP_WINDOW_SCALE,
+                PROP_SCREEN_HEIGHT * PROP_WINDOW_SCALE
+            ))
+
+
+        # Get the font path
+        if config.get("font_name", None) is not None:
+            self.font_path = self.pygame.font.match_font(config['font_name'])
+        else:
+            self.font_path = self.pygame.font.get_default_font()
+
+        # Load the font
+        self.font = self.pygame.font.Font(self.font_path, config.get("font_size", 15 * PROP_WINDOW_SCALE))
+        self.font.bold = config.get("font_bold", False)
+        self.font.italic = config.get("font_italic", False)
+
+        # Set the colours
+        self.color = config.get("font_color", "white")
+
+    def draw(self, screen):
+        if self.draw_background:
+            if self.background_type == "color":
+                screen.fill(self.background_color)
+            elif self.background_type == "image":
+                screen.blit(self.bg_image, (0, 0))
+
+        text_surface = self.font.render(f"IP Address:", True, self.color)
+        text_surface_2 = self.font.render(get_ip(), True, self.color)
+        text_rect = text_surface.get_rect(center=get_screen_centre())
+        y = get_screen_centre()[1] + self.font.size("IP Address:")[1] + 10
+        text_rect_2 = text_surface_2.get_rect(center=(get_screen_centre()[0], y))
+        screen.blit(text_surface, text_rect)
+        screen.blit(text_surface_2, text_rect_2)
+