diff --git a/.envrc b/.envrc
new file mode 100644
index 0000000000000000000000000000000000000000..fa370586604656b1b4b386d4af41bf5f1067412e
--- /dev/null
+++ b/.envrc
@@ -0,0 +1,5 @@
+# shellcheck shell=bash
+
+strict_env
+
+PATH_add scripts
diff --git a/.gitignore b/.gitignore
index 8a515e36db912a9aa732e087adce88365a4cf433..03910c478c78239a6a357f2af0567dc56349f73f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,8 +4,5 @@
 # KiCad
 /schematic/*-backups/
 
-# Dependencies
-/src/lib/
-
 # User-specific configuration
-/src/config.json
+/src/settings.toml
diff --git a/.vscode/settings.json b/.vscode/settings.json
index 6491b5829468afcb5e129b44acfb746ee2375273..000811b4d0f9f2ff7dbaa56f142c5f4686c1de05 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -1,9 +1,8 @@
 {
   "python.analysis.diagnosticSeverityOverrides": {
-    "reportMissingModuleSource": "none"
+    "reportMissingImports": "none",
+    "reportMissingModuleSource": "none",
+    "reportShadowedImports": "none"
   },
-  "python.formatting.provider": "black",
-  "python.analysis.extraPaths": [
-    "src/lib"
-  ]
+  "python.formatting.provider": "black"
 }
diff --git a/LICENSE b/LICENSE
index c18da5b3d9505d241b1dd6152b23a0a92307f58c..6b987ead96f93bee793c2e54a28f66aaac61ffc5 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,6 +1,6 @@
 MIT License
 
-Copyright (c) 2022 James Ide
+Copyright (c) 2023 James Ide
 
 Permission is hereby granted, free of charge, to any person obtaining a copy
 of this software and associated documentation files (the "Software"), to deal
diff --git a/README.md b/README.md
index c08e7e6a2d5cb1cc2acc5f814f18adb818a5de1a..252467d568f47d8bbfcb002e26f5531d110ef3a2 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@
 # Pico W Garage Door Sensor
 
-Get notified when your garage door's been left open. This sensor uses the Raspberry Pi Pico W and MicroPython.
+Get notified when your garage door's been left open. This sensor uses the Raspberry Pi Pico W and CircuitPython and requires Home Assistant with MQTT set up.
 
 ## Hardware
 
@@ -28,7 +28,18 @@ You could add a small capacitor to debounce the reed switch in hardware but this
 |           | Stranded copper wire, 22-24 AWG (long enough to reach your garage door from the Pico and back) |                                                                                                                                                                                               |
 ## Software
 
-Install MicroPython on your Pico W using [a .uf2 file](https://micropython.org/download/rp2-pico-w/) from the MicroPython website.
+Install CircuitPython 8 on your Pico W using [a .uf2 file](https://circuitpython.org/board/raspberry_pi_pico_w/) from the CircuitPython website.
 
+Create settings.toml under src or directly in your CIRCUITPY drive and define the following environment variables:
+```toml
+CIRCUITPY_WIFI_SSID = "your Wi-Fi access point's name"
+CIRCUITPY_WIFI_PASSWORD = "your Wi-Fi password"
 
+MQTT_HOSTNAME = "your MQTT broker's hostname"
+MQTT_USERNAME = "the username to use with your MQTT broker"
+MQTT_PASSWORD = "the password to use with your MQTT broker"
 
+WIFI_HOSTNAME = "garagesensor"
+```
+
+There are convenient scripts under the `scripts` directory for deploying software to your Pico W and connecting to its REPL. Only macOS is supported.
diff --git a/deploy.sh b/deploy.sh
deleted file mode 100755
index 873fc3a5a3c0b6609b5c35b1f61e7107509b725b..0000000000000000000000000000000000000000
--- a/deploy.sh
+++ /dev/null
@@ -1,14 +0,0 @@
-#!/usr/bin/env bash
-
-set -euo pipefail
-
-pico_name="${PICO_BOARD:-pyboard}"
-script_directory="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
-
-if ! command -v rshell &> /dev/null; then
-  echo 'rshell is not installed on this computer. Install it by following the instructions in the rshell repository at: https://github.com/dhylands/rshell'
-  exit 1
-fi
-
-rshell rsync --mirror "$script_directory/src/" "/$pico_name"
-rshell repl '~ import machine~ machine.soft_reset() ~'
diff --git a/install.sh b/install.sh
deleted file mode 100755
index 81ff47450670713744bc2c9f4235c79eec978f1d..0000000000000000000000000000000000000000
--- a/install.sh
+++ /dev/null
@@ -1,21 +0,0 @@
-#!/usr/bin/env bash
-
-set -euo pipefail
-
-script_directory="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
-lib_directory="$script_directory/src/lib"
-
-mkdir -p "$lib_directory"
-
-# microdot 1.2.0
-echo "Downloading microdot..."
-curl --silent --output-dir "$lib_directory" \
-  --remote-name https://raw.githubusercontent.com/miguelgrinberg/microdot/v1.2.0/src/microdot.py \
-  --remote-name https://raw.githubusercontent.com/miguelgrinberg/microdot/v1.2.0/src/microdot_asyncio.py #\
-  # --remote-name https://raw.githubusercontent.com/miguelgrinberg/microdot/v1.2.0/src/microdot_asyncio_websocket.py \
-  # --remote-name https://raw.githubusercontent.com/miguelgrinberg/microdot/v1.2.0/src/microdot_websocket.py
-
-# mqtt_as 0.7.0
-echo "Downloading mqtt_as..."
-curl --silent --output-dir "$lib_directory" \
-  --remote-name https://raw.githubusercontent.com/peterhinch/micropython-mqtt/000b67778bf0a9308ac99c7ce2b54c8a1c4ce23c/mqtt_as/mqtt_as.py
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 0000000000000000000000000000000000000000..3b1c8ac02782d1d3d938d7e6245906cc4dbb0bb9
--- /dev/null
+++ b/requirements.txt
@@ -0,0 +1,4 @@
+adafruit_debouncer==2.0.5
+adafruit_ticks==1.0.9
+adafruit_logging==5.2.1
+adafruit_minimqtt==7.2.2
diff --git a/scripts/deploy b/scripts/deploy
new file mode 100755
index 0000000000000000000000000000000000000000..da2b9570fe2468dcc3d62842de009eedc77492bf
--- /dev/null
+++ b/scripts/deploy
@@ -0,0 +1,51 @@
+#!/usr/bin/env bash
+
+set -euo pipefail
+
+script_directory="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+project_directory="$script_directory/.."
+
+install_dependencies=false
+while getopts ":d" opt; do
+  case $opt in
+    d)
+      install_dependencies=true
+      ;;
+    *)
+      ;;
+  esac
+done
+
+if ! command -v rsync &> /dev/null; then
+  echo 'rsync is not installed on this computer. rsync is used to copy the application code to your device. Install it using the package manager you use with your OS.'
+  exit 1
+fi
+
+if ! command -v circup &> /dev/null; then
+  echo 'circup is not installed on this computer. circup is used to install CircuitPython dependencies on your device. Install it by following the instructions in the rshell repository at: https://github.com/adafruit/circup'
+  exit 1
+fi
+
+echo 'Detecting your CircuitPython device...'
+device_path=$(python3 -c 'import circup; print(circup.find_device())')
+if [ "$device_path" == 'None' ]; then
+  echo 'circup could not detect a connected CircuitPython device. Make sure your device is flashed with CircuitPython and connected to your computer with a USB data cable.'
+  exit 1
+fi
+echo "Found device at $device_path"
+
+if [ $install_dependencies = true ] || \
+   [ ! -d "$device_path/lib" ] || \
+   [ -z "$(ls -A "$device_path/lib")" ]; then
+  echo 'Installing dependencies...'
+  circup install --requirement "$project_directory/requirements.txt"
+  echo 'Finished installing dependencies'
+fi
+
+echo 'Copying application code...'
+rsync --verbose --recursive --delete --checksum \
+  --include='/._*' --exclude "/.*" \
+  --exclude "/boot_out.txt" --exclude "/lib/" \
+  --exclude "/log.txt" \
+  "$project_directory/src/" "$device_path"
+echo 'Finished copying application code'
diff --git a/scripts/repl b/scripts/repl
new file mode 100755
index 0000000000000000000000000000000000000000..54a68faea363825fa49e50783ee46f7c0cbb1c10
--- /dev/null
+++ b/scripts/repl
@@ -0,0 +1,8 @@
+#!/usr/bin/env bash
+
+set -euo pipefail
+
+device=$(ls -1 -t /dev/tty.usbmodem*)
+
+# Reattach to an existing session if one exists; otherwise create a new one
+screen -D -R -S circuitpython "$device" 115200
diff --git a/src/boot.py b/src/boot.py
new file mode 100644
index 0000000000000000000000000000000000000000..ca15b5a206919979329a4004872f48cf08c3e3de
--- /dev/null
+++ b/src/boot.py
@@ -0,0 +1,8 @@
+import os
+
+import storage
+
+if os.getenv("LOG_FILE"):
+  # Allow CircuitPython to write to its flash storage
+  # https://learn.adafruit.com/circuitpython-essentials/circuitpython-storage
+  storage.remount("/", readonly=False, disable_concurrent_write_protection=True)
diff --git a/src/code.py b/src/code.py
new file mode 100644
index 0000000000000000000000000000000000000000..7eeb815720ac730129effed3f0c32d447ca87017
--- /dev/null
+++ b/src/code.py
@@ -0,0 +1,90 @@
+import binascii
+import os
+import time
+import traceback
+
+import board
+import digitalio
+import microcontroller
+import socketpool
+import supervisor
+import wifi
+
+import adafruit_debouncer
+import adafruit_logging
+import adafruit_minimqtt.adafruit_minimqtt as adafruit_minimqtt
+
+from logging import create_logger
+from mqtt import publish_homeassistant_discovery_message, publish_sensor_state_message
+
+
+def main(logger: adafruit_logging.Logger) -> None:
+    led = digitalio.DigitalInOut(board.LED)
+    led.switch_to_output()
+
+    switch_io = digitalio.DigitalInOut(board.GP22)
+    # GPIO 22 has a pull-up resistor so we don't pull it up in code
+    switch_io.switch_to_input()
+    switch = adafruit_debouncer.Debouncer(switch_io)
+
+    # Connect to the Wi-Fi network
+    logger.info("Connecting to the local Wi-Fi network...")
+    wifi.radio.hostname = os.getenv("WIFI_HOSTNAME")
+    wifi.radio.connect(
+        os.getenv("CIRCUITPY_WIFI_SSID"), os.getenv("CIRCUITPY_WIFI_PASSWORD")
+    )
+    logger.info(
+        "Connected to the local network: IP address = %s, router = %s, DNS server %s",
+        wifi.radio.ipv4_address,
+        wifi.radio.ipv4_gateway,
+        wifi.radio.ipv4_dns,
+    )
+    pool = socketpool.SocketPool(wifi.radio)
+
+    # Connect to the MQTT broker
+    logger.info("Connecting to the MQTT broker...")
+    mqtt = adafruit_minimqtt.MQTT(
+        broker=os.getenv("MQTT_HOSTNAME"),
+        username=os.getenv("MQTT_USERNAME"),
+        password=os.getenv("MQTT_PASSWORD"),
+        is_ssl=False,
+        socket_pool=pool,
+    )
+    mqtt.logger = logger
+    mqtt.connect()
+    logger.info("Connected to the MQTT broker")
+
+    mqtt_device_id = binascii.hexlify(microcontroller.cpu.uid).decode("utf-8")
+    mqtt_state_topic = f"door/{mqtt_device_id}/state"
+    publish_homeassistant_discovery_message(mqtt, mqtt_device_id, mqtt_state_topic)
+
+    # The switch is open when the door is closed
+    led.value = not switch.value
+    publish_sensor_state_message(mqtt, mqtt_state_topic, not switch.value)
+    logger.info("Advertised Home Assistant discovery message and current door state")
+
+    logger.info("Monitoring door sensor...")
+    while True:
+        switch.update()
+        mqtt.loop()
+
+        if switch.rose or switch.fell:
+            is_door_open = not switch.value
+            logger.info(
+                "Detected the door open" if is_door_open else "Detected the door closed"
+            )
+            led.value = is_door_open
+            publish_sensor_state_message(mqtt, mqtt_state_topic, is_door_open)
+
+
+logger = create_logger()
+
+try:
+    main(logger)
+except Exception as exception:
+    logger.critical("%s", "".join(traceback.format_exception(exception, limit=8)))
+    time.sleep(10)
+    if supervisor.runtime.usb_connected:
+        supervisor.reload()
+    else:
+        microcontroller.reset()
diff --git a/src/debounce.py b/src/debounce.py
deleted file mode 100644
index f505b0ca50914f16f533c7e788b17f1675c59b3b..0000000000000000000000000000000000000000
--- a/src/debounce.py
+++ /dev/null
@@ -1,72 +0,0 @@
-from machine import Pin, Timer
-from micropython import const, schedule
-from uasyncio import ThreadSafeFlag
-
-
-class PinDebouncer:
-    DEFAULT_DEBOUNCE_PERIOD_MS: int = const(10)
-
-    def __init__(
-        self,
-        initial_value: int,
-        debounce_period_ms: int = DEFAULT_DEBOUNCE_PERIOD_MS,
-        callback=None,
-    ) -> None:
-        self.value = initial_value
-        self._value_change_flag = ThreadSafeFlag()
-        self._value_change_callback = callback
-        self._debounce_timer = Timer()
-        self._debounce_period_ms = debounce_period_ms
-
-        # The pin's value at the time of the last edge trigger, only to be accessed during an IRQ
-        self._irq_pin_value = initial_value
-
-        # Eagerly bind instance methods so memory is not allocated during an IRQ
-        self._debounce_timer_isr = self._handle_debounce_timer_irq
-        self._set_value = self.set_value
-
-    def handle_edge_trigger_irq(self, pin: Pin) -> None:
-        """
-        Handles a change in the switch pin's state, triggered by either a rising or falling edge.
-        This function runs inside an IRQ. The switch is debounced using a timer mainly to filter
-        noise from the reed switch.
-        """
-        self._irq_pin_value = pin.value()
-
-        # If there is a pending timer due to an earlier edge trigger, setting the timer here cancels
-        # the previous timer and schedules a new one
-        self._debounce_timer.init(
-            mode=Timer.ONE_SHOT,
-            period=self._debounce_period_ms,
-            callback=self._debounce_timer_isr,
-        )
-
-    def _handle_debounce_timer_irq(self, timer: Timer) -> None:
-        """
-        Handles the debounce timer when enough time has elapsed since the last edge trigger IRQ.
-        This function itself runs inside the timer's IRQ.
-        """
-        schedule(self._set_value, self._irq_pin_value)
-
-    def set_value(self, value: int) -> None:
-        """
-        Sets the switch state to the given pin's current value. This function runs outside of an
-        IRQ.
-        """
-        if value == self.value:
-            return
-
-        self.value = value
-        self._value_change_flag.set()
-
-        if self._value_change_callback is not None:
-            self._value_change_callback(value)
-
-    async def wait_for_toggle(self) -> int:
-        """
-        A coroutine that waits for the pin to change value and returns the new, debounced value of
-        the pin. Only one task at a time may await this coroutine.
-        """
-        self._value_change_flag.clear()
-        await self._value_change_flag.wait()
-        return self.value
diff --git a/src/html/hello.html b/src/html/hello.html
deleted file mode 100644
index ba8ecd4264e2ae78fa948b8a4ce1578d3e4748cc..0000000000000000000000000000000000000000
--- a/src/html/hello.html
+++ /dev/null
@@ -1,9 +0,0 @@
-<!DOCTYPE html>
-<html>
-  <head>
-    <title>Pico W Garage Door Sensor</title>
-  </head>
-  <body>
-    <p>Hello world</p>
-  </body>
-</html>
diff --git a/src/logging.py b/src/logging.py
new file mode 100644
index 0000000000000000000000000000000000000000..3a9c241f3b185328c64db54df356d8dc2673034d
--- /dev/null
+++ b/src/logging.py
@@ -0,0 +1,56 @@
+import errno
+import os
+
+import storage
+import supervisor
+
+import adafruit_logging
+
+
+_LOG_SIZE_THRESHOLD_BYTES = 100 * 1024**2
+
+
+def create_logger() -> adafruit_logging.Logger:
+    """Create the root logger and configure it to print all info messages and higher to stderr.
+    Error messages and higher are written to a log file when the file system is writable.
+    """
+    logger = adafruit_logging.getLogger()
+    logger.setLevel(adafruit_logging.INFO)
+
+    if supervisor.runtime.usb_connected:
+        logger.addHandler(adafruit_logging.StreamHandler())
+    else:
+        # The logging package prints messages with its default stream handler when the message is
+        # too low in severity for any of the registered handlers. This null handler accepts messages
+        # of all severity levels and suppresses the logger from writing high numbers of debug
+        # messages through the default stream handler to stderr.
+        logger.addHandler(adafruit_logging.NullHandler())
+
+    log_filename = os.getenv("LOG_FILE")
+    if log_filename and not storage.getmount("/").readonly:
+        handler = _file_handler(logger, log_filename)
+        handler.setLevel(adafruit_logging.ERROR)
+        logger.addHandler(handler)
+
+    return logger
+
+
+def _file_handler(
+    logger: adafruit_logging.Logger, log_filename: str
+) -> adafruit_logging.FileHandler:
+    # Prevent the log file from filling the flash memory
+    log_file_mode = "a"
+    try:
+        size = os.stat(log_filename)[6]  # st_size is at index 6
+        if size >= _LOG_SIZE_THRESHOLD_BYTES:
+            log_file_mode = "w"
+            logger.info("Log file is %d bytes, creating a new log file", size)
+        else:
+            logger.info("Log file is %d bytes, appending to existing log file", size)
+    except OSError as error:
+        if error.errno != errno.ENOENT:
+            raise error
+        log_file_mode = "w"
+        logger.info("Log file does not exist, creating a new log file")
+
+    return adafruit_logging.FileHandler(log_filename, mode=log_file_mode)
diff --git a/src/main.py b/src/main.py
deleted file mode 100644
index 9ce23700f3486b3f4c56e208b9582b3ab9f541e3..0000000000000000000000000000000000000000
--- a/src/main.py
+++ /dev/null
@@ -1,85 +0,0 @@
-import gc
-import json
-from machine import reset, unique_id
-from micropython import alloc_emergency_exception_buf, const
-import rp2
-from sys import print_exception
-import uasyncio as asyncio
-
-from mqtt_as import MQTTClient
-
-from mqtt import (
-    mqtt_client_config,
-    mqtt_json_message,
-    mqtt_string_message,
-    publish_homeassistant_discovery_message,
-)
-from sensor import enable_sensor
-
-DOOR_SWITCH_GPIO: int = const(22)
-
-
-async def main() -> None:
-    gc.collect()
-
-    # Allocate a small amount of memory to capture stack traces within interrupt handlers
-    alloc_emergency_exception_buf(100)
-
-    # Read in the global configuration from the filesystem
-    config = load_config()
-
-    # Use the Wi-Fi channels of the configured country or default to a worldwide subset
-    wifi_config = config.get("wifi", {})
-    country = wifi_config.get("country")
-    if country is not None:
-        rp2.country(country)
-
-    # The MQTT client also handles connecting to the configured Wi-Fi network
-    mqtt = MQTTClient(mqtt_client_config(config))
-    try:
-        await mqtt.connect()
-    except OSError as e:
-        print("Failed to connect to the MQTT broker")
-        print_exception(e)
-        reset()
-
-    # Announce this device to Home Assistant
-    # https://www.home-assistant.io/integrations/mqtt/#mqtt-discovery
-    mqtt_device_id = unique_id().hex()
-    mqtt_state_topic = f"door/{mqtt_device_id}/state"
-    await publish_homeassistant_discovery_message(
-        mqtt, mqtt_device_id, mqtt_state_topic
-    )
-
-    # Start listening to the garage door sensor
-    loop = asyncio.get_event_loop()
-
-    def publish_sensor_value(value: int) -> None:
-        print(
-            f"The door is open (sensor value = {value})"
-            if value is 0
-            else f"The door is closed (sensor value = {value})"
-        )
-        loop.run_until_complete(
-            mqtt.publish(
-                mqtt_state_topic,
-                mqtt_string_message("ON" if value is 0 else "OFF"),
-                retain=True,
-                qos=1,
-            )
-        )
-
-    sensor_pin = enable_sensor(
-        config.get("sensor_pin", DOOR_SWITCH_GPIO), publish_sensor_value
-    )
-    publish_sensor_value(sensor_pin.value())
-
-    gc.collect()
-
-
-def load_config() -> dict:
-    with open("config.json") as config_file:
-        return json.load(config_file)
-
-
-asyncio.run(main())
diff --git a/src/mqtt.py b/src/mqtt.py
index df831d205d806229241b09f100fadf6f690ada4e..b2ab815ec027e7a712d3261ddf168b8987b433de 100644
--- a/src/mqtt.py
+++ b/src/mqtt.py
@@ -1,27 +1,11 @@
 import json
 
-from mqtt_as import MQTTClient, config as mqtt_default_client_config
-
-
-def mqtt_client_config(config: dict) -> dict:
-    mqtt_config = config.get("mqtt", {})
-    wifi_config = config.get("wifi", {})
-    return dict(
-        mqtt_default_client_config,
-        ssid=wifi_config.get("ssid"),
-        wifi_pw=wifi_config.get("password"),
-        server=mqtt_config.get("hostname"),
-        user=mqtt_config.get("username"),
-        password=mqtt_config.get("password"),
-    )
-
-
-async def publish_homeassistant_discovery_message(
-    mqtt: MQTTClient, device_id: str, state_topic: str
+def publish_homeassistant_discovery_message(
+    mqtt, device_id: str, state_topic: str
 ) -> None:
-    await mqtt.publish(
+    mqtt.publish(
         f"homeassistant/binary_sensor/{device_id}/config",
-        mqtt_json_message(
+        json.dumps(
             {
                 "unique_id": device_id,
                 "name": "Garage Door",
@@ -41,9 +25,10 @@ async def publish_homeassistant_discovery_message(
     )
 
 
-def mqtt_string_message(string: str) -> bytes:
-    return bytes(string, "utf-8")
-
-
-def mqtt_json_message(object: dict) -> bytes:
-    return bytes(json.dumps(object), "utf-8")
+def publish_sensor_state_message(mqtt, state_topic: str, sensor_state: bool) -> None:
+    mqtt.publish(
+        state_topic,
+        "ON" if sensor_state else "OFF",
+        retain=True,
+        qos=1,
+    )
diff --git a/src/sensor.py b/src/sensor.py
deleted file mode 100644
index 249f829bf8dc67fc05a162a06c7203ff367c40eb..0000000000000000000000000000000000000000
--- a/src/sensor.py
+++ /dev/null
@@ -1,38 +0,0 @@
-from machine import Pin
-from micropython import const
-
-from debounce import PinDebouncer
-
-
-"""
-Any sub-second debounce period feels responsive enough for detecting a garage door so we choose a
-reliable debounce period even though the datasheet says the operate and release times are 3.0 ms
-"""
-DEBOUNCE_PERIOD_MS: int = const(30)
-
-
-def enable_sensor(sensor_pin_id: int, callback=None) -> Pin:
-    # The GPIO pin must be pulled up externally
-    switch = Pin(sensor_pin_id, Pin.IN, pull=None)
-    _toggle_led(switch.value())
-
-    def handle_switch_value_change(value: int) -> None:
-        _toggle_led(value)
-        if callback:
-            callback(value)
-
-    switch_debouncer = PinDebouncer(
-        switch.value(),
-        debounce_period_ms=DEBOUNCE_PERIOD_MS,
-        callback=handle_switch_value_change,
-    )
-    switch.irq(
-        switch_debouncer.handle_edge_trigger_irq,
-        trigger=Pin.IRQ_FALLING | Pin.IRQ_RISING,
-    )
-
-    return switch
-
-
-def _toggle_led(value: int) -> None:
-    Pin("LED", Pin.OUT, value=value)
diff --git a/src/web.py b/src/web.py
deleted file mode 100644
index 78499af666136bc87c3a59c38e90e398df206091..0000000000000000000000000000000000000000
--- a/src/web.py
+++ /dev/null
@@ -1,21 +0,0 @@
-from time import localtime
-
-from microdot import send_file
-from microdot_asyncio import Microdot
-
-
-def web_server() -> Microdot:
-    app = Microdot()
-
-    @app.route("/")
-    async def index(request):
-        now = localtime()
-        return f"Hello World! It is now {now[3]}:{now[4]}:{now[5]}"
-
-    @app.route("/<path:path>")
-    async def html(request, path):
-        if ".." in path:
-            return "Not found", 404
-        return send_file("html/" + path)
-
-    return app