From 75d480eda1bb74e49de10daca45b003b908532e0 Mon Sep 17 00:00:00 2001
From: David Trattnig <david@subsquare.at>
Date: Fri, 24 Nov 2023 13:57:53 +0100
Subject: [PATCH] chore: merge main

---
 .dockerignore                           |   2 +-
 .gitignore                              |   6 +-
 .gitlab-ci.yml                          |   2 +-
 .pre-commit-config.yaml                 |   6 +-
 Dockerfile                              |   2 +-
 Makefile                                |   8 +-
 README.md                               |  10 +-
 config/sample.engine.docker.ini         |  82 ----------
 config/sample.engine.docker.yaml        | 126 ++++++++++++++
 config/sample.engine.ini                |  82 ----------
 config/sample.engine.yaml               | 127 +++++++++++++++
 docs/developer-guide.md                 |   6 +-
 poetry.lock                             |  17 +-
 pyproject.toml                          |   1 +
 scripts/initialize-systemd.sh           |   2 +-
 src/aura_engine/app.py                  |   4 +-
 src/aura_engine/base/config.py          | 208 ++++++++++++++----------
 src/aura_engine/base/logger.py          |   4 +-
 src/aura_engine/core/channels.py        |   4 +-
 src/aura_engine/core/client.py          |   8 +-
 src/aura_engine/core/mixer.py           |   6 +-
 src/aura_engine/engine.py               |  17 +-
 src/aura_engine/events.py               |   2 +-
 src/aura_engine/plugins/clock.py        |  10 +-
 src/aura_engine/plugins/monitor.py      |  41 +++--
 src/aura_engine/scheduling/api.py       |   8 +-
 src/aura_engine/scheduling/scheduler.py |   4 +-
 src/aura_engine/scheduling/utils.py     |   2 +-
 tests/test_config.py                    |  25 +--
 tests/test_logger.py                    |   2 +-
 tests/test_simple_api.py                |   2 +-
 tests/test_simple_api_cached.py         |   4 +-
 32 files changed, 485 insertions(+), 345 deletions(-)
 delete mode 100644 config/sample.engine.docker.ini
 create mode 100644 config/sample.engine.docker.yaml
 delete mode 100644 config/sample.engine.ini
 create mode 100644 config/sample.engine.yaml

diff --git a/.dockerignore b/.dockerignore
index 51bd219f..a3faf998 100644
--- a/.dockerignore
+++ b/.dockerignore
@@ -77,7 +77,7 @@ python
 .noseids
 
 # Configurations
-config/engine.docker.ini
+config/engine.docker.yaml
 env.list
 
 # Socket
diff --git a/.gitignore b/.gitignore
index 2bb85a60..6f08aff2 100644
--- a/.gitignore
+++ b/.gitignore
@@ -13,10 +13,10 @@ docker.env
 /.cache
 /.build
 /.bash_history
-/config/engine.ini
+/config/engine.yaml
 /config/systemd/dev/
 /audio
-/config/docker.engine.ini
-/config/engine.docker.ini
+/config/docker.engine.yaml
+/config/engine.docker.yaml
 .coverage
 coverage.xml
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 23331578..4c9a67ee 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -23,7 +23,7 @@ run_test_cases:
   stage: test
   before_script:
     - *install_requirements
-    - cp config/sample.engine.ini config/engine.ini
+    - cp config/sample.engine.yaml config/engine.yaml
   script:
     - make coverage
   coverage: '/(?i)total.*? (100(?:\.0+)?\%|[1-9]?\d(?:\.\d+)?\%)$/'
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index f3ac07b2..36d930d3 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -1,6 +1,6 @@
 repos:
   - repo: https://github.com/psf/black
-    rev: 23.1.0
+    rev: 23.7.0
     hooks:
     - id: black
   - repo: https://github.com/PyCQA/isort
@@ -8,12 +8,12 @@ repos:
     hooks:
       - id: isort
   - repo: https://github.com/codespell-project/codespell
-    rev: "v2.2.2"
+    rev: "v2.2.5"
     hooks:
       - id: codespell
         args: [""]
   - repo: https://github.com/PyCQA/flake8
-    rev: 6.0.0
+    rev: 6.1.0
     hooks:
       - id: flake8
         args: ["--config=.flake8"]
diff --git a/Dockerfile b/Dockerfile
index 79d396c8..4aea77b5 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -32,7 +32,7 @@ WORKDIR /srv
 
 # Init Application
 COPY ./src/aura_engine /srv/src/aura_engine
-COPY config/sample.engine.docker.ini /srv/config/engine.ini
+COPY config/sample.engine.docker.yaml /srv/config/engine.yaml
 RUN poetry install --no-interaction --no-ansi
 
 # Update Permissions
diff --git a/Makefile b/Makefile
index cdc610c8..95eaa86b 100644
--- a/Makefile
+++ b/Makefile
@@ -22,7 +22,7 @@ help::
 TIMEZONE := "Europe/Vienna"
 
 AURA_ENGINE_CORE_SOCKET := "aura_engine_socket"
-AURA_ENGINE_CONFIG := ${CURDIR}/config/engine.docker.ini
+AURA_ENGINE_CONFIG := ${CURDIR}/config/engine.docker.yaml
 AURA_AUDIO_STORE_SOURCE := ${CURDIR}/../engine-core/audio/source
 AURA_AUDIO_STORE_PLAYLIST := ${CURDIR}/../engine-core/audio/playlist
 AURA_LOGS := ${CURDIR}/logs
@@ -35,7 +35,7 @@ DOCKER_RUN = @docker run \
 		--mount type=tmpfs,destination=/tmp \
 		--env-file docker.env \
 		-v aura_engine_socket:"/srv/socket" \
-		-v "$(AURA_ENGINE_CONFIG)":"/etc/aura/engine.ini":ro \
+		-v "$(AURA_ENGINE_CONFIG)":"/etc/aura/engine.yaml":ro \
 		-v "$(AURA_AUDIO_STORE_SOURCE)":"/var/audio/source":ro \
 		-v "$(AURA_AUDIO_STORE_PLAYLIST)":"/var/audio/playlist":ro \
 		-v "$(AURA_LOGS)":"/srv/logs" \
@@ -47,14 +47,14 @@ DOCKER_RUN = @docker run \
 
 init.app:: pyproject.toml
 	poetry install
-	cp -n config/sample.engine.ini config/engine.ini
+	cp -n config/sample.engine.yaml config/engine.yaml
 	mkdir -p .cache
 
 init.dev:: pyproject.toml
 	poetry install --with dev
 	poetry run pre-commit autoupdate
 	poetry run pre-commit install
-	cp -n config/sample.engine.ini config/engine.ini
+	cp -n config/sample.engine.yaml config/engine.yaml
 	mkdir -p .cache
 
 api::
diff --git a/README.md b/README.md
index 4f85caf5..cb426757 100644
--- a/README.md
+++ b/README.md
@@ -90,7 +90,7 @@ Install dependencies and prepare config file:
 make init.app
 ```
 
-This also creates a default configuration file at `config/engine.ini`.
+This also creates a default configuration file at `config/engine.yaml`.
 
 For development install with:
 
@@ -98,15 +98,15 @@ For development install with:
 make init.dev
 ```
 
-Note, if some configuration exists under `/etc/aura/engine.ini` the configuration by default is drawn from there. This overrides any configuration located in the local configuration file.
+Note, if some configuration exists under `/etc/aura/engine.yaml` the configuration by default is drawn from there. This overrides any configuration located in the local configuration file.
 
 ## Configuration
 
-Edit the configuration file `config/engine.ini`. Verify or change at least these config options:
+Edit the configuration file `config/engine.yaml`. Verify or change at least these config options:
 
-```ini
+```yaml
 # The secret which is used to authenticate against Tank
-api_tank_secret="aura-engine-secret"
+api_tank_secret: aura-engine-secret
 ```
 
 ## Running Engine
diff --git a/config/sample.engine.docker.ini b/config/sample.engine.docker.ini
deleted file mode 100644
index 221624aa..00000000
--- a/config/sample.engine.docker.ini
+++ /dev/null
@@ -1,82 +0,0 @@
-##############################################
-#            Engine Configuration            #
-##############################################
-
-[general]
-# Path to the engine-core socket directory relative to the engine project root
-socket_dir="/srv/socket"
-# Directory to store temporary data
-cache_dir="/tmp"
-# Directory where the log file resides
-log_dir="logs"
-# Possible values: debug, info, warning, error, critical
-log_level="${AURA_ENGINE_LOG_LEVEL}"
-# Details for the Station Fallback
-fallback_show_name="${AURA_ENGINE_FALLBACK_SHOW_NAME}"
-fallback_show_id="${AURA_ENGINE_FALLBACK_SHOW_ID}"
-
-[monitoring]
-# Seconds how often the vitality of Engine Core should be checked (default=1)
-heartbeat_frequency="${AURA_ENGINE_HEARTBEAT_FREQUENCY}"
-# Host where heartbeat is sent to (disabled if empty string)
-heartbeat_server="${AURA_ENGINE_HEARTBEAT_SERVER}"
-# Some UDP port
-heartbeat_port="${AURA_ENGINE_HEARTBEAT_SERVER_PORT}"
-
-[api]
-## STEERING ##
-# The URL to get the health status
-api_steering_status="${AURA_STEERING_BASE_URL}api/v1/"
-# The URL to get the Calendar via Steering
-api_steering_calendar="${AURA_STEERING_BASE_URL}api/v1/playout"
-
-## TANK ##
-# The session name which is used to authenticate against Tank
-api_tank_session="${AURA_TANK_ENGINE_USER}"
-# The secret which is used to authenticate against Tank
-api_tank_secret="${AURA_TANK_ENGINE_PASSWORD}"
-# The URL to get the health status
-api_tank_status="${AURA_TANK_BASE_URL}healthz"
-# The URL to get playlist details via Tank
-api_tank_playlist="${AURA_TANK_BASE_URL}api/v1/playlists/${ID}"
-
-## ENGINE-API ##
-# Engine ID (1 or 2)
-api_engine_number=1
-# Engine API availability check
-api_engine_status="${AURA_ENGINE_API_BASE_URL}api/v1/ui/"
-# Engine API endpoint to store playlogs
-api_engine_store_playlog="${AURA_ENGINE_API_BASE_URL}api/v1/playlog"
-# Engine API endpoint to store clock information
-api_engine_store_clock="${AURA_ENGINE_API_BASE_URL}api/v1/clock"
-# Engine API endpoint to store health information
-api_engine_store_health="${AURA_ENGINE_API_BASE_URL}api/v1/source/health/${ENGINE_NUMBER}"
-
-[scheduler]
-# Base path as seen by "engine-core", not accessed by "engine"; this is required to construct the absolute audio file path (check "Audio Store" in the docs)
-# Either provide an absolute base path or a relative one starting in the `engine` directory. In case of `engine-core` running in docker use `/var/audio/source`
-audio_source_folder="/var/audio/source"
-audio_source_extension=".flac"
-# Folder holding M3U Playlists to be scheduled in form of Engine Playlists (similar as audio source folder above)
-audio_playlist_folder="/var/audio/playlist"
-# Offset in seconds how long it takes for Liquidsoap to actually execute a scheduler command; Crucial to keep things in sync
-engine_latency_offset="${AURA_ENGINE_LATENCY_OFFSET}"
-# How often should the calendar be fetched in seconds. This determines the time of the last changes applied, before a specific show is aired
-fetching_frequency=30
-# The scheduling window defines when the items of each timeslot are queued for play-out. The windows start at (timeslot.start - window_start) seconds
-# and ends at (timeslot.end - window.end) seconds. Its also worth noting, that timeslots can only be deleted before the start of the window.
-scheduling_window_start=60
-scheduling_window_end=60
-# How many seconds before the actual schedule time the item should be pre-loaded. Note to provide enough timeout for
-# contents which take longer to load (big files, bad connectivity to streams etc.). If the planned start time is in
-# the past the offset is ignored and the item is played as soon as possible
-preload_offset=15
-# Sometimes it might take longer to get a stream connected. Here you can define a viable length.
-# But note, that this may affect the preloading time (see `preload_offset`), hence affecting the
-# overall playout, its delays and possible fallbacks
-input_stream_retry_delay=1
-input_stream_max_retries=10
-input_stream_buffer=3.0
-# Fade duration when selecting another mixer input (seconds)
-fade_in_time="${AURA_ENGINE_FADE_IN_TIME}"
-fade_out_time="${AURA_ENGINE_FADE_OUT_TIME}"
diff --git a/config/sample.engine.docker.yaml b/config/sample.engine.docker.yaml
new file mode 100644
index 00000000..1ab33b0d
--- /dev/null
+++ b/config/sample.engine.docker.yaml
@@ -0,0 +1,126 @@
+##############################################
+#            Engine Configuration            #
+##############################################
+
+general:
+  # Path to the engine-core socket directory relative to the engine project root
+  socket_dir: /srv/socket
+  # Directory to store temporary data
+  cache_dir: /tmp
+
+  # Details for the Station Fallback
+  fallback_show_name: ${AURA_ENGINE_FALLBACK_SHOW_NAME}
+  fallback_show_id: ${AURA_ENGINE_FALLBACK_SHOW_ID}
+
+log:
+  # Directory where the log file resides
+  directory: logs
+  # Possible values: debug, info, warning, error, critical
+  level: ${AURA_ENGINE_LOG_LEVEL}
+
+monitoring:
+  mail:
+    # Mail server credentials for sending email notifications (Admin and Programme Coordination)
+    host: mail.your-radio.org
+    port: 587
+    user: aura@subsquare.at
+    pwd: ---SECRET--PASSWORD---
+
+    coordinator:
+      # Set to "true" if you want to notify programme-coordinators about about fallback situations, otherwise "false"
+      enabled: false
+      # If you want to address multiple programme-coordinators separate their emails by space
+      mail: programme-coordinator@your-radio.org
+
+    admin:
+      # Set to "true" if you want to notify admins about incidents, otherwise "false"
+      enabled: false
+      # If you want to address multiple administrators separate their emails by space
+      mail: david@subsquare.at
+
+    # The FROM email address used when sending
+    from: monitoring@aura.engine
+    # A subject prefix allows applying filter rules in your mail client
+    subject_prefix: "[AURA Engine]" # default: [AURA Engine]
+
+  heartbeat:
+    # Seconds how often the vitality of Engine Core should be checked (default: 1)
+    frequency: ${AURA_ENGINE_HEARTBEAT_FREQUENCY}
+    # Host where heartbeat is sent to (disabled if empty string)
+    host: ${AURA_ENGINE_HEARTBEAT_SERVER}
+    # Some UDP port
+    port: ${AURA_ENGINE_HEARTBEAT_SERVER_PORT}
+
+api:
+  ## STEERING ##
+  steering:
+    # The URL to get the health status
+    status: ${AURA_STEERING_BASE_URL}api/v1/
+    # The URL to get the Calendar via Steering
+    calendar: ${AURA_STEERING_BASE_URL}api/v1/playout
+
+  ## TANK ##
+  tank:
+    # The session name which is used to authenticate against Tank
+    session: ${AURA_TANK_ENGINE_USER}
+    # The secret which is used to authenticate against Tank
+    secret: ${AURA_TANK_ENGINE_PASSWORD}
+    # The URL to get the health status
+    status: ${AURA_TANK_BASE_URL}healthz
+    # The URL to get playlist details via Tank
+    playlist: ${AURA_TANK_BASE_URL}api/v1/playlists/${ID}
+
+  ## ENGINE-API ##
+  engine:
+    # Engine ID (1 or 2)
+    number: 1
+    # Engine API availability check
+    status: ${AURA_ENGINE_API_BASE_URL}api/v1/ui/
+    # Engine API endpoint to store playlogs
+    store_playlog: ${AURA_ENGINE_API_BASE_URL}api/v1/playlog
+    # Engine API endpoint to store clock information
+    store_clock: ${AURA_ENGINE_API_BASE_URL}api/v1/clock
+    # Engine API endpoint to store health information
+    store_health: ${AURA_ENGINE_API_BASE_URL}api/v1/source/health/${ENGINE_NUMBER}
+scheduler:
+  # Database settings: Use 'postgresql', 'sqlite' or 'mysql'. In case of SQLite the "db_name" is the name of the file.
+  db:
+    type: postgresql
+    name: ${AURA_ENGINE_DB_NAME}
+    user: ${AURA_ENGINE_DB_USER}
+    pwd:  ${AURA_ENGINE_DB_PASS}
+    host: ${AURA_ENGINE_DB_HOST}
+    charset: utf8
+    
+  # Base path as seen by "engine-core", not accessed by "engine"; this is required to construct the absolute audio file path (check "Audio Store" in the docs)
+  # Either provide an absolute base path or a relative one starting in the `engine` directory. In case of `engine-core` running in docker use `/var/audio/source`
+  audio:
+    source_folder: /var/audio/source
+    source_extension: .flac
+    # Folder holding M3U Playlists to be scheduled in form of Engine Playlists (similar as audio source folder above)
+    playlist_folder: /var/audio/playlist
+    # Offset in seconds how long it takes for Liquidsoap to actually execute a scheduler command; Crucial to keep things in sync
+    engine_latency_offset: ${AURA_ENGINE_LATENCY_OFFSET}
+
+  # How often should the calendar be fetched in seconds. This determines the time of the last changes applied, before a specific show is aired
+  fetching_frequency: 30
+  # The scheduling window defines when the entries of each timeslot are queued for play-out. The windows start at (timeslot.start - window_start) seconds
+  # and ends at (timeslot.end - window.end) seconds. Its also worth noting, that timeslots can only be deleted before the start of the window.
+  scheduling_window_start: 60
+  scheduling_window_end: 60
+  # How many seconds before the actual schedule time the entry should be pre-loaded. Note to provide enough timeout for
+  # contents which take longer to load (big files, bad connectivity to streams etc.). If the planned start time is in
+  # the past the offset is ignored and the entry is played as soon as possible
+  preload_offset: 15
+
+  # Sometimes it might take longer to get a stream connected. Here you can define a viable length.
+  # But note, that this may affect the preloading time (see `preload_offset`), hence affecting the
+  # overall playout, its delays and possible fallbacks
+  input_stream:
+    retry_delay: 1
+    max_retries: 10
+    buffer: 3.0
+
+  # Fade duration when selecting another mixer input (seconds)
+  fade_in_time: ${AURA_ENGINE_FADE_IN_TIME}
+  fade_out_time: ${AURA_ENGINE_FADE_OUT_TIME}
diff --git a/config/sample.engine.ini b/config/sample.engine.ini
deleted file mode 100644
index acf1cdcc..00000000
--- a/config/sample.engine.ini
+++ /dev/null
@@ -1,82 +0,0 @@
-##############################################
-#            Engine Configuration            #
-##############################################
-
-[general]
-# Path to the engine-core socket directory relative to the engine project root
-socket_dir="../engine-core/socket"
-# Directory to store temporary data
-cache_dir="./.cache"
-# Directory where the log file resides
-log_dir="logs"
-# Possible values: debug, info, warning, error, critical
-log_level="info"
-# Details for the Station Fallback
-fallback_show_name="Random Music"
-fallback_show_id="-1"
-
-[monitoring]
-# Seconds how often the vitality of Engine Core should be checked (default=1)
-heartbeat_frequency=1
-# Host where heartbeat is sent to (disabled if empty string)
-heartbeat_server=""
-# Some UDP port
-heartbeat_port=43334
-
-[api]
-## STEERING ##
-# The URL to get the health status
-api_steering_status="http://localhost:8000/api/v1/"
-# The URL to get the Calendar via Steering
-api_steering_calendar="http://localhost:8000/api/v1/playout"
-
-## TANK ##
-# The session name which is used to authenticate against Tank
-api_tank_session="engine"
-# The secret which is used to authenticate against Tank
-api_tank_secret="rather-secret"
-# The URL to get the health status
-api_tank_status="http://localhost:8040/healthz"
-# The URL to get playlist details via Tank
-api_tank_playlist="http://localhost:8040/api/v1/playlists/${ID}"
-
-## ENGINE-API ##
-# Engine ID (1 or 2)
-api_engine_number=1
-# Engine API availability check
-api_engine_status="http://localhost:8008/api/v1/ui/"
-# Engine API endpoint to store playlogs
-api_engine_store_playlog="http://localhost:8008/api/v1/playlog"
-# Engine API endpoint to store clock information
-api_engine_store_clock="http://localhost:8008/api/v1/clock"
-# Engine API endpoint to store health information
-api_engine_store_health="http://localhost:8008/api/v1/source/health/${ENGINE_NUMBER}"
-
-[scheduler]
-# Base path as seen by "engine-core", not accessed by "engine"; this is required to construct the absolute audio file path (check "Audio Store" in the docs)
-# Either provide an absolute base path or a relative one starting in the `engine` directory. In case of `engine-core` running in docker use `/var/audio/source`
-audio_source_folder="../engine-core/audio/source"
-audio_source_extension=".flac"
-# Folder holding M3U Playlists to be scheduled in form of Engine Playlists (similar as audio source folder above)
-audio_playlist_folder="../engine-core/audio/playlist"
-# Offset in seconds how long it takes for Liquidsoap to actually execute a scheduler command; Crucial to keep things in sync
-engine_latency_offset=0.5
-# How often should the calendar be fetched in seconds. This determines the time of the last changes applied, before a specific show is aired
-fetching_frequency=30
-# The scheduling window defines when the items of each timeslot are queued for play-out. The windows start at (timeslot.start - window_start) seconds
-# and ends at (timeslot.end - window.end) seconds. Its also worth noting, that timeslots can only be deleted before the start of the window.
-scheduling_window_start=60
-scheduling_window_end=60
-# How many seconds before the actual schedule time the item should be pre-loaded. Note to provide enough timeout for
-# contents which take longer to load (big files, bad connectivity to streams etc.). If the planned start time is in
-# the past the offset is ignored and the item is played as soon as possible
-preload_offset=15
-# Sometimes it might take longer to get a stream connected. Here you can define a viable length.
-# But note, that this may affect the preloading time (see `preload_offset`), hence affecting the
-# overall playout, its delays and possible fallbacks
-input_stream_retry_delay=1
-input_stream_max_retries=10
-input_stream_buffer=3.0
-# Fade duration when selecting another mixer input (seconds)
-fade_in_time="1.5"
-fade_out_time="1.5"
diff --git a/config/sample.engine.yaml b/config/sample.engine.yaml
new file mode 100644
index 00000000..3a645752
--- /dev/null
+++ b/config/sample.engine.yaml
@@ -0,0 +1,127 @@
+##############################################
+#            Engine Configuration            #
+##############################################
+
+general:
+  # Path to the engine-core socket directory relative to the engine project root
+  socket_dir: ../engine-core/socket
+  # Directory to store temporary data
+  cache_dir: ./.cache
+
+  # Details for the Station Fallback
+  fallback_show_name: Random Music
+  fallback_show_id: -1
+
+log:
+  # Directory where the log file resides
+  directory: logs
+  # Possible values: debug, info, warning, error, critical
+  level: info
+
+monitoring:
+  mail:
+    # Mail server credentials for sending email notifications (Admin and Programme Coordination)
+    host: mail.your-radio.org
+    port: 587
+    user: aura@subsquare.at
+    pwd: ---SECRET--PASSWORD---
+
+    coordinator:
+      # Set to "true" if you want to notify programme-coordinators about about fallback situations, otherwise "false"
+      enabled: false
+      # If you want to address multiple programme-coordinators separate their emails by space
+      mail: programme-coordinator@your-radio.org
+
+    admin:
+      # Set to "true" if you want to notify admins about incidents, otherwise "false"
+      enabled: false
+      # If you want to address multiple administrators separate their emails by space
+      mail: david@subsquare.at
+
+    # The FROM email address used when sending
+    from: monitoring@aura.engine
+    # A subject prefix allows applying filter rules in your mail client
+    subject_prefix: "[AURA Engine]" # default: [AURA Engine]
+
+  heartbeat:
+    # Seconds how often the vitality of Engine Core should be checked (default: 1)
+    frequency: 1
+    # Host where heartbeat is sent to (disabled if empty string)
+    host: ""
+    # Some UDP port
+    port: 43334
+
+api:
+  ## STEERING ##
+  steering:
+    # The URL to get the health status
+    status: http://localhost:8000/api/v1/
+    # The URL to get the Calendar via Steering
+    calendar: http://localhost:8000/api/v1/playout
+
+  ## TANK ##
+  tank:
+    # The session name which is used to authenticate against Tank
+    session: engine
+    # The secret which is used to authenticate against Tank
+    secret: rather-secret
+    # The URL to get the health status
+    status: http://localhost:8040/healthz
+    # The URL to get playlist details via Tank
+    playlist: http://localhost:8040/api/v1/playlists/${ID}
+
+  ## ENGINE-API ##
+  engine:
+    # Engine ID (1 or 2)
+    number: 1
+    # Engine API availability check
+    status: http://localhost:8008/api/v1/ui/
+    # Engine API endpoint to store playlogs
+    store_playlog: http://localhost:8008/api/v1/playlog
+    # Engine API endpoint to store clock information
+    store_clock: http://localhost:8008/api/v1/clock
+    # Engine API endpoint to store health information
+    store_health: http://localhost:8008/api/v1/source/health/${ENGINE_NUMBER}
+
+scheduler:
+  # Database settings: Use 'postgresql', 'sqlite' or 'mysql'. In case of SQLite the "db_name" is the name of the file.
+  db:
+    type: postgresql
+    name: aura_engine
+    user: aura_engine
+    pwd: "1234"
+    host: localhost
+    charset: utf8
+    
+  # Base path as seen by "engine-core", not accessed by "engine"; this is required to construct the absolute audio file path (check "Audio Store" in the docs)
+  # Either provide an absolute base path or a relative one starting in the `engine` directory. In case of `engine-core` running in docker use `/var/audio/source`
+  audio:
+    source_folder: ../engine-core/audio/source
+    source_extension: .flac
+    # Folder holding M3U Playlists to be scheduled in form of Engine Playlists (similar as audio source folder above)
+    playlist_folder: ../engine-core/audio/playlist
+    # Offset in seconds how long it takes for Liquidsoap to actually execute a scheduler command; Crucial to keep things in sync
+    engine_latency_offset: 0.5
+
+  # How often should the calendar be fetched in seconds. This determines the time of the last changes applied, before a specific show is aired
+  fetching_frequency: 30
+  # The scheduling window defines when the entries of each timeslot are queued for play-out. The windows start at (timeslot.start - window_start) seconds
+  # and ends at (timeslot.end - window.end) seconds. Its also worth noting, that timeslots can only be deleted before the start of the window.
+  scheduling_window_start: 60
+  scheduling_window_end: 60
+  # How many seconds before the actual schedule time the entry should be pre-loaded. Note to provide enough timeout for
+  # contents which take longer to load (big files, bad connectivity to streams etc.). If the planned start time is in
+  # the past the offset is ignored and the entry is played as soon as possible
+  preload_offset: 15
+
+  # Sometimes it might take longer to get a stream connected. Here you can define a viable length.
+  # But note, that this may affect the preloading time (see `preload_offset`), hence affecting the
+  # overall playout, its delays and possible fallbacks
+  input_stream:
+    retry_delay: 1
+    max_retries: 10
+    buffer: 3.0
+
+  # Fade duration when selecting another mixer input (seconds)
+  fade_in_time: 1.5
+  fade_out_time: 1.5
diff --git a/docs/developer-guide.md b/docs/developer-guide.md
index 2ee255ff..dd6d64e7 100644
--- a/docs/developer-guide.md
+++ b/docs/developer-guide.md
@@ -118,12 +118,12 @@ point in time and the involved phase before:
 
   The start and the end of the window is defined by the start of the timeslot minus
   a configured amount of seconds (see `scheduling_window_start` and `scheduling_window_end`
-  in `engine.ini`). The actual start of the window is calculated by (timeslot start - window start)
+  in `engine.yaml`). The actual start of the window is calculated by (timeslot start - window start)
   and the end by (timeslot end - window end)
 
   During the scheduling window, the external API Endpoints are pulled continuously, to
   check for updated timeslots and related playlists. Also, any changes to playlists and
-  its items are respected within that window (see `fetching_frequency` in `engine.ini`).
+  its items are respected within that window (see `fetching_frequency` in `engine.yaml`).
 
   > Important: It is vital that the the scheduling window is wider than the fetching frequency.
   > Otherwise one fetch might never hit a scheduling window, hence not being able to schedule stuff.
@@ -136,7 +136,7 @@ point in time and the involved phase before:
 - **Queuing and Pre-Loading**: Before any playlist items of the timeslot can be turned into
   sound, they need to be queued and pre-loaded. Ideally the pre-loading happens somewhat before
   the scheduled play-out time to avoid any delays in timing. Set the maximum time reserved for
-  pre-loading in your configuration (compare `preload_offset`in `engine.ini`).
+  pre-loading in your configuration (compare `preload_offset`in `engine.yaml`).
 
   If there is not enough time to reserve the given amount of time for preloading (i.e. some playlist item
   should have started in the past already) the offset is ignored and the item is played as soon as possible.
diff --git a/poetry.lock b/poetry.lock
index 58885a86..003f5115 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -1,4 +1,4 @@
-# This file is automatically @generated by Poetry 1.4.0 and should not be changed by hand.
+# This file is automatically @generated by Poetry 1.4.2 and should not be changed by hand.
 
 [[package]]
 name = "anyio"
@@ -293,6 +293,21 @@ files = [
     {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
 ]
 
+[[package]]
+name = "confuse"
+version = "2.0.1"
+description = "Painless YAML configuration."
+category = "main"
+optional = false
+python-versions = ">=3.6"
+files = [
+    {file = "confuse-2.0.1-py3-none-any.whl", hash = "sha256:9b9e5bbc70e2cb9b318bcab14d917ec88e21bf1b724365e3815eb16e37aabd2a"},
+    {file = "confuse-2.0.1.tar.gz", hash = "sha256:7379a2ad49aaa862b79600cc070260c1b7974d349f4fa5e01f9afa6c4dd0611f"},
+]
+
+[package.dependencies]
+pyyaml = "*"
+
 [[package]]
 name = "coverage"
 version = "7.3.2"
diff --git a/pyproject.toml b/pyproject.toml
index 71eaeaf4..75d63311 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -41,6 +41,7 @@ http-parser = "^0.9.0"
 openapi-python-client = "^0.14.0"
 flake8-bandit = "^4.1.1"
 tomli = "^2.0.1"
+confuse = "^2.0.1"
 jsonpickle = "^3.0.2"
 
 [tool.poetry.group.dev.dependencies]
diff --git a/scripts/initialize-systemd.sh b/scripts/initialize-systemd.sh
index 7fce56f1..3a41a810 100755
--- a/scripts/initialize-systemd.sh
+++ b/scripts/initialize-systemd.sh
@@ -6,7 +6,7 @@
 # You'll need sudo/root privileges.
 #
 
-echo "Set Ownership of '/opt/aura/engine', '/var/log/aura/' and '/etc/aura/engine.ini' to Engine User"
+echo "Set Ownership of '/opt/aura/engine', '/var/log/aura/' and '/etc/aura/engine.yaml' to Engine User"
 chown -R engineuser:engineuser /opt/aura
 chown -R engineuser:engineuser /etc/aura
 chown -R engineuser:engineuser /var/log/aura
diff --git a/src/aura_engine/app.py b/src/aura_engine/app.py
index d8aa11a2..c3d4671f 100755
--- a/src/aura_engine/app.py
+++ b/src/aura_engine/app.py
@@ -31,7 +31,7 @@ from aura_engine.base.config import AuraConfig
 from aura_engine.base.logger import AuraLogger
 from aura_engine.engine import Engine
 
-config = AuraConfig()
+config = AuraConfig.instance
 
 
 class EngineRunner:
@@ -47,7 +47,7 @@ class EngineRunner:
         """
         Constructor.
         """
-        self.config = config
+        self.config = config.config
         AuraLogger(self.config)
         self.logger = logging.getLogger("engine")
         self.engine = Engine()
diff --git a/src/aura_engine/base/config.py b/src/aura_engine/base/config.py
index d358e429..385a7b18 100644
--- a/src/aura_engine/base/config.py
+++ b/src/aura_engine/base/config.py
@@ -24,124 +24,160 @@ Dealing with configuration data.
 import logging
 import os
 import os.path
+import re
 import sys
-from configparser import ConfigParser
 from pathlib import Path
 
+import confuse
+import yaml
+
+template = {
+    "general": {
+        "socket_dir": str,
+        "cache_dir": str,
+        "fallback_show_name": str,
+        "fallback_show_id": int,
+    },
+    "log": {
+        "directory": str,
+        "level": confuse.OneOf(["debug", "info", "warning", "error", "critical"]),
+    },
+    "monitoring": {
+        "mail": {
+            "host": str,
+            "port": int,
+            "user": str,
+            "pwd": str,
+            "from": str,
+            "subject_prefix": str,
+            "coordinator": {"enabled": bool, "mail": str},
+            "admin": {"enabled": bool, "mail": str},
+        },
+        "heartbeat": {"host": str, "port": int, "frequency": int},
+    },
+    "api": {
+        "steering": {"status": str, "calendar": str},
+        "tank": {"session": str, "secret": str, "status": str, "playlist": str},
+        "engine": {
+            "number": int,
+            "status": str,
+            "store_playlog": str,
+            "store_clock": str,
+            "store_health": str,
+        },
+    },
+    "scheduler": {
+        "db": {
+            "type": confuse.OneOf(["postgresql", "mysql", "sqlite"]),
+            "name": str,
+            "user": str,
+            "pwd": str,
+            "host": str,
+            "charset": str,
+        },
+        "audio": {
+            "source_folder": str,
+            "source_extension": str,
+            "playlist_folder": str,
+            "engine_latency_offset": float,
+        },
+        "fetching_frequency": int,
+        "scheduling_window_start": int,
+        "scheduling_window_end": int,
+        "preload_offset": int,
+        "input_stream": {"retry_delay": int, "max_retries": int, "buffer": float},
+        "fade_in_time": float,
+        "fade_out_time": float,
+    },
+}
+
 
 class AuraConfig:
     """
-    Holds the Engine Configuration as in the file `engine.ini`.
+    Creates config by reading yaml file according to template above.
     """
 
-    instance = None
-    ini_path = ""
+    _instance = None
+    yaml_path = ""
+    confuse_config = None
+    config = None  # points to a validated config (hopefully later)
     logger = None
 
-    def __init__(self, ini_path="/etc/aura/engine.ini"):
+    @classmethod
+    @property
+    def instance(cls):
+        """Create and return singleton instance."""
+        if cls._instance is None:
+            cls._instance = AuraConfig()
+        return cls._instance
+
+    def __init__(self, yaml_path="/etc/aura/engine.yaml"):
         """
-        Initialize the configuration, defaults to `/etc/aura/engine.ini`.
+        Initialize the configuration, defaults to `/etc/aura/engine.yaml`.
 
-        If this file doesn't exist it uses `./config/engine.ini` from
+        If this file doesn't exist it uses `./config/engine.yaml` from
         the project directory.
 
         Args:
-            ini_path(String): The path to the configuration file `engine.ini`
+            yaml_path(String): The path to the configuration file `engine.yaml`
 
         """
         self.logger = logging.getLogger("engine")
-        config_file = Path(ini_path)
+        config_file = Path(yaml_path)
         project_root = Path(__file__).parent.parent.parent.parent.absolute()
 
         if not config_file.is_file():
-            ini_path = f"{project_root}/config/engine.ini"
-
-        self.ini_path = ini_path
+            yaml_path = f"{project_root}/config/engine.yaml"
+
+        self.yaml_path = yaml_path
+        print(f"Using configuration at: {yaml_path}")
+
+        envar_matcher = re.compile(r"\$\{([^}^{]+)\}")
+
+        def envar_constructor(loader, node):
+            value = os.path.expandvars(node.value)
+            # workaround not to parse numerics as strings
+            try:
+                value = int(value)
+            except ValueError:
+                pass
+            try:
+                value = float(value)
+            except ValueError:
+                pass
+            return value
+
+        envar_loader = yaml.SafeLoader
+        envar_loader.add_implicit_resolver("!envar", envar_matcher, None)
+        envar_loader.add_constructor("!envar", envar_constructor)
+
+        self.confuse_config = confuse.Configuration("engine", loader=envar_loader)
+        self.confuse_config.set_file(yaml_path)
         self.load_config()
-        AuraConfig.instance = self
 
-        # Defaults
-        self.set("install_dir", os.path.realpath(project_root))
-        self.set("config_dir", os.path.dirname(ini_path))
-        print(f"Using configuration at: {ini_path}")
+        # custom overrides and defaults
+        self.confuse_config["install_dir"].set(os.path.realpath(project_root))
+        self.confuse_config["config_dir"].set(os.path.dirname(yaml_path))
+
+        AuraConfig.instance = self
 
     def init_version(self, version: dict):
         """
         Read and set the component version from VERSION file in project root.
         """
-        self.set("version_control", version.get("control"))
-        self.set("version_core", version.get("core"))
-        self.set("version_liquidsoap", version.get("liquidsoap"))
-
-    @staticmethod
-    def config():
-        """
-        Retrieve the global instances of the configuration.
-        """
-        return AuraConfig.instance
-
-    def set(self, key, value):
-        """
-        Set specific config property.
-
-        Args:
-            key (String): key
-            default (*): value
-
-        """
-        try:
-            self.__dict__[key] = int(value)
-        except ValueError:
-            self.__dict__[key] = str(value)
-
-    def get(self, key, default=None):
-        """
-        Get for some specific config property.
-
-        Args:
-            key (String): key
-            default (*): value
-
-        """
-        if key not in self.__dict__:
-            if default:
-                self.set(key, default)
-            else:
-                self.logger.warning(
-                    "Key " + key + " not found in configfile " + self.ini_path + "!"
-                )
-                return None
-
-        value = self.__dict__[key]
-        if value and isinstance(value, str):
-            value = os.path.expandvars(value)
-        return value
+        self.confuse_config["version_control"].set(version.get("control"))
+        self.confuse_config["version_core"].set(version.get("core"))
+        self.confuse_config["version_liquidsoap"].set(version.get("liquidsoap"))
 
     def load_config(self):
         """
         Set config defaults and load settings from file.
         """
-        if not os.path.isfile(self.ini_path):
-            self.logger.critical(self.ini_path + " not found  :(")
+        if not os.path.isfile(self.yaml_path):
+            self.logger.critical(self.yaml_path + " not found  :(")
             sys.exit(1)
 
-        # Read the file
-        f = open(self.ini_path, "r")
-        ini_str = f.read()
-        f.close()
-
-        # Parse the values
-        config_parser = ConfigParser()
-        try:
-            config_parser.read_string(ini_str)
-        except Exception as e:
-            self.logger.critical("Cannot read " + self.ini_path + "! Reason: " + str(e))
-            sys.exit(0)
-
-        for section in config_parser.sections():
-            for key, value in config_parser.items(section):
-                v = config_parser.get(section, key).replace('"', "").strip()
-                self.set(key, v)
+        self.config = self.confuse_config.get(template)
 
     def to_abs_path(self, path):
         """
@@ -152,16 +188,16 @@ class AuraConfig:
         if path.startswith("/"):
             return path
         else:
-            return self.get("install_dir") + "/" + path
+            return self.confuse_config["install_dir"].get() + "/" + path
 
     def abs_audio_store_path(self):
         """
         Return the absolute path to the audio store, based on the `audio_source_folder` setting.
         """
-        return self.to_abs_path(self.get("audio_source_folder"))
+        return self.to_abs_path(self.config.scheduler.audio.source_folder)
 
     def abs_playlist_path(self):
         """
         Return the absolute path to the playlist folder.
         """
-        return self.to_abs_path(self.get("audio_playlist_folder"))
+        return self.to_abs_path(self.config.scheduler.audio.playlist_folder)
diff --git a/src/aura_engine/base/logger.py b/src/aura_engine/base/logger.py
index a86ebc7d..43054725 100644
--- a/src/aura_engine/base/logger.py
+++ b/src/aura_engine/base/logger.py
@@ -53,7 +53,7 @@ class AuraLogger:
         """
         Retrieve the configured log level (default=INFO).
         """
-        lvl = self.config.get("log_level")
+        lvl = self.config.log.level
         mapping = {
             "debug": logging.DEBUG,
             "info": logging.INFO,
@@ -83,7 +83,7 @@ class AuraLogger:
 
         if not self.logger.hasHandlers():
             # create file handler for logger
-            file_handler = logging.FileHandler(self.config.get("log_dir") + "/" + name + ".log")
+            file_handler = logging.FileHandler(self.config.log.directory + "/" + name + ".log")
             file_handler.setLevel(lvl)
 
             # create stream handler for logger
diff --git a/src/aura_engine/core/channels.py b/src/aura_engine/core/channels.py
index 810101b4..41e3258a 100644
--- a/src/aura_engine/core/channels.py
+++ b/src/aura_engine/core/channels.py
@@ -148,7 +148,7 @@ class GenericChannel:
             mixer (Mixer): The mixer instance
 
         """
-        self.config = AuraConfig.config()
+        self.config = AuraConfig.instance.config
         self.logger = logging.getLogger("engine")
         self.mixer = mixer
         self.name = channel_name
@@ -576,7 +576,7 @@ class ChannelFactory:
             mixer (Mixer): The mixer instance
 
         """
-        self.config = AuraConfig()
+        self.config = AuraConfig.instance
         self.logger = logging.getLogger("engine")
         self.mixer = mixer
 
diff --git a/src/aura_engine/core/client.py b/src/aura_engine/core/client.py
index d68d5b01..96704b8a 100644
--- a/src/aura_engine/core/client.py
+++ b/src/aura_engine/core/client.py
@@ -50,7 +50,7 @@ class CoreClient:
         Initialize the client.
         """
         self.logger = logging.getLogger("engine")
-        self.config = AuraConfig.config()
+        self.config = AuraConfig.instance.config
         self.event_dispatcher = event_dispatcher
         self.conn = CoreConnection()
 
@@ -151,7 +151,7 @@ class CoreClient:
         @private
 
         """
-        if self.config.get("log_level") == "debug":
+        if self.config.log.level == "debug":
             cmds = CoreClient.skip_log_commands
             base_cmd = command.split(" ")[0]
             if not base_cmd.startswith(cmds):
@@ -234,8 +234,8 @@ class CoreConnection:
         Initialize the connection.
         """
         self.logger = logging.getLogger("engine")
-        config = AuraConfig.config()
-        socket_path = config.get("socket_dir") + "/engine.sock"
+        config = AuraConfig.instance
+        socket_path = config.config.general.socket_dir + "/engine.sock"
         self.socket_path = config.to_abs_path(socket_path)
         self.logger.debug(f"Using socket at '{self.socket_path}'")
 
diff --git a/src/aura_engine/core/mixer.py b/src/aura_engine/core/mixer.py
index 83022296..ca933fce 100644
--- a/src/aura_engine/core/mixer.py
+++ b/src/aura_engine/core/mixer.py
@@ -63,7 +63,7 @@ class Mixer:
             client (PlayoutClient): The client for controlling playout
 
         """
-        self.config = AuraConfig.config()
+        self.config = AuraConfig.instance.config
         self.logger = logging.getLogger("engine")
         self.mixer_id = mixer_id
         self.client = client
@@ -234,7 +234,7 @@ class Mixer:
                 self.logger.info(msg)
                 return
 
-            fade_in_time = float(self.config.get("fade_in_time"))
+            fade_in_time = self.config.scheduler.fade_in_time
 
             if fade_in_time > 0:
                 self.fade_in_active = True
@@ -274,7 +274,7 @@ class Mixer:
                 self.logger.info(msg)
                 return
 
-            fade_out_time = float(self.config.get("fade_out_time"))
+            fade_out_time = self.config.scheduler.fade_out_time
 
             if fade_out_time > 0:
                 step = abs(fade_out_time) / current_volume
diff --git a/src/aura_engine/engine.py b/src/aura_engine/engine.py
index 08a7cd32..93d032f8 100644
--- a/src/aura_engine/engine.py
+++ b/src/aura_engine/engine.py
@@ -71,8 +71,8 @@ class Engine:
             raise Exception("Engine is already running!")
         Engine.instance = self
         self.logger = logging.getLogger("engine")
-        self.config = AuraConfig.config()
-        Engine.engine_time_offset = float(self.config.get("engine_latency_offset"))
+        self.config = AuraConfig.instance.config
+        Engine.engine_time_offset = self.config.scheduler.audio.engine_latency_offset
 
     def start(self):
         """
@@ -143,7 +143,7 @@ class Engine:
 
         def dispatch_fallback_event():
             timeslot = self.scheduler.timetable.get_current_timeslot()
-            fallback_show_name = self.config.get("fallback_show_name")
+            fallback_show_name = self.config.general.fallback_show_name
             self.event_dispatcher.on_fallback_active(timeslot, fallback_show_name)
 
         # Initialize state
@@ -173,8 +173,8 @@ class Engine:
         Update the config of playout with the current values.
         """
         playout_config = {
-            "fallback_show_id": int(self.config.get("fallback_show_id")),
-            "fallback_show_name": self.config.get("fallback_show_name"),
+            "fallback_show_id": self.config.general.fallback_show_id,
+            "fallback_show_name": self.config.general.fallback_show_name,
         }
         json_config = json.dumps(playout_config, ensure_ascii=False)
         response = self.playout.set_config(json_config)
@@ -197,9 +197,8 @@ class Engine:
         ctrl_version = toml_file["tool"]["poetry"]["version"]
         versions = self.playout.get_version()
         versions = DotDict(json.loads(versions))
-        self.config.set("version_control", ctrl_version)
-        self.config.set("version_core", versions.core)
-        self.config.set("version_liquidsoap", versions.liquidsoap)
+        versions.control = ctrl_version
+        AuraConfig.instance.init_version(versions)
 
     @staticmethod
     def engine_time():
@@ -275,7 +274,7 @@ class Player:
             event_dispatcher (EventDispather): Dispatcher for issuing events
 
         """
-        self.config = AuraConfig.config()
+        self.config = AuraConfig.instance.config
         self.logger = logging.getLogger("engine")
         self.event_dispatcher = event_dispatcher
         self.resource_map = ResourceMapping()
diff --git a/src/aura_engine/events.py b/src/aura_engine/events.py
index 1355affe..172dce0d 100644
--- a/src/aura_engine/events.py
+++ b/src/aura_engine/events.py
@@ -88,7 +88,7 @@ class EngineEventDispatcher:
         """
         self.subscriber_registry = dict()
         self.logger = logging.getLogger("engine")
-        self.config = AuraConfig.config()
+        self.config = AuraConfig.instance.config
         self.engine = engine
 
     #
diff --git a/src/aura_engine/plugins/clock.py b/src/aura_engine/plugins/clock.py
index 7bc60af6..ea6c6dd1 100644
--- a/src/aura_engine/plugins/clock.py
+++ b/src/aura_engine/plugins/clock.py
@@ -53,7 +53,7 @@ class ClockInfoHandler:
         Initialize.
         """
         self.logger = logging.getLogger("engine")
-        self.config = AuraConfig.config()
+        self.config = AuraConfig.instance.config
         self.api = SimpleRestApi()
         self.engine = engine
 
@@ -77,8 +77,8 @@ class ClockInfoHandler:
             return
 
         self.logger.info(f"Fallback '{fallback_name}' activated, clock update required")
-        fallback_show_id = self.config.get("fallback_show_id")
-        fallback_show_name = self.config.get("fallback_show_name")
+        fallback_show_id = self.config.general.fallback_show_id
+        fallback_show_name = self.config.general.fallback_show_name
 
         # Interpolate timeslot-less slot
         # TODO start time to be calculated based on previous timeslot (future station logic)
@@ -139,12 +139,12 @@ class ClockInfoHandler:
             built_upcoming.append(self.build_timeslot(upcoming_timeslot, None))
 
         data = {
-            "engineSource": self.config.get("api_engine_number"),
+            "engineSource": self.config.api.engine.number,
             "currentTimeslot": self.build_timeslot(active_timeslot, active_playlist),
             "upcomingTimeslots": built_upcoming,
             "plannedPlaylist": self.build_playlist(active_playlist),
         }
-        url = self.config.get("api_engine_store_clock")
+        url = self.config.api.engine.store_clock
         self.logger.info(f"PUT clock info to '{url}': \n{data}")
         self.api.put(url, data=data)
 
diff --git a/src/aura_engine/plugins/monitor.py b/src/aura_engine/plugins/monitor.py
index 160d2a02..f3ebd04c 100644
--- a/src/aura_engine/plugins/monitor.py
+++ b/src/aura_engine/plugins/monitor.py
@@ -90,7 +90,7 @@ class AuraMonitor:
         """
         self.api = SimpleRestApi()
         self.logger = logging.getLogger("engine")
-        self.config = AuraConfig.config()
+        self.config = AuraConfig.instance.config
         self.engine = engine
         self.status = dict()
         self.status["engine"] = dict()
@@ -103,9 +103,9 @@ class AuraMonitor:
 
         # Heartbeat settings
         self.heartbeat_running = False
-        self.heartbeat_server = self.config.get("heartbeat_server")
-        self.heartbeat_port = self.config.get("heartbeat_port")
-        self.heartbeat_frequency = self.config.get("heartbeat_frequency")
+        self.heartbeat_server = self.config.monitoring.heartbeat.host
+        self.heartbeat_port = self.config.monitoring.heartbeat.port
+        self.heartbeat_frequency = self.config.monitoring.heartbeat.frequency
         self.heartbeat_socket = socket(AF_INET, SOCK_DGRAM)
 
         self.engine_id = self.get_engine_id()
@@ -201,7 +201,7 @@ class AuraMonitor:
         body["details"] = json.dumps(data, default=str)
         json_data = json.dumps(body, default=str)
         timeout = 5
-        url = self.config.get("api_engine_store_health")
+        url = self.config.api.engine.store_health
         url = url.replace("${ENGINE_NUMBER}", str(self.config.get("api_engine_number")))
         headers = {"content-type": "application/json"}
         response = requests.Response()
@@ -234,28 +234,25 @@ class AuraMonitor:
         Request the current status of all components.
         """
         self.engine.init_version()
-        ctrl_version = self.config.get("version_control")
-        core_version = self.config.get("version_core")
-        liq_version = self.config.get("version_liquidsoap")
-
+        ctrl_version = AuraConfig.instance.confuse_config["version_control"].get()
+        core_version = AuraConfig.instance.confuse_config["version_core"].get()
+        liq_version = AuraConfig.instance.confuse_config["version_liquidsoap"].get()
         self.status["engine"]["version"] = ctrl_version
         self.status["lqs"]["version"] = {"core": core_version, "liquidsoap": liq_version}
         self.status["lqs"]["outputs"] = self.engine.player.mixer.get_outputs()
         self.status["lqs"]["mixer"] = self.engine.player.mixer.get_inputs()
-        self.status["api"]["steering"]["url"] = self.config.get("api_steering_status")
+        self.status["api"]["steering"]["url"] = self.config.api.steering.status
         self.status["api"]["steering"]["available"] = self.validate_url_connection(
-            self.config.get("api_steering_status")
+            self.config.api.steering.status
         )
-        self.status["api"]["tank"]["url"] = self.config.get("api_tank_status")
+        self.status["api"]["tank"]["url"] = self.config.api.tank.status
         self.status["api"]["tank"]["available"] = self.validate_url_connection(
-            self.config.get("api_tank_status")
-        )
-        self.status["api"]["tank"]["status"] = self.get_url_response(
-            self.config.get("api_tank_status")
+            self.config.api.tank.status
         )
-        self.status["api"]["engine"]["url"] = self.config.get("api_engine_status")
+        self.status["api"]["tank"]["status"] = self.get_url_response(self.config.api.tank.status)
+        self.status["api"]["engine"]["url"] = self.config.api.engine.status
         self.status["api"]["engine"]["available"] = self.validate_url_connection(
-            self.config.get("api_engine_status")
+            self.config.api.engine.status
         )
 
         self.update_vitality_status()
@@ -266,7 +263,9 @@ class AuraMonitor:
         """
         self.status["lqs"]["status"] = self.engine.update_playout_state()
         self.status["lqs"]["available"] = self.status["lqs"]["status"] is not None
-        self.status["audio_source"] = self.validate_directory(self.config.abs_audio_store_path())
+        self.status["audio_source"] = self.validate_directory(
+            AuraConfig.instance.abs_audio_store_path()
+        )
 
         # After first update start the Heartbeat Monitor
         if not self.heartbeat_running:
@@ -282,7 +281,7 @@ class AuraMonitor:
         """
         if self.has_valid_status(True):
             # Always check status, but only send heartbeat if wanted so
-            if self.config.get("heartbeat_server") != "":
+            if self.config.monitoring.heartbeat.host != "":
                 self.heartbeat_socket.sendto(
                     str.encode("OK"), (self.heartbeat_server, self.heartbeat_port)
                 )
@@ -310,7 +309,7 @@ class AuraMonitor:
                     {"engine_id": self.engine_id, "status": status}
                 )
 
-        heartbeat_frq = self.config.get("heartbeat_frequency", 1)
+        heartbeat_frq = self.config.monitoring.heartbeat.frequency  # default: 1
         if int(heartbeat_frq or 0) < 1:
             heartbeat_frq = 1
         threading.Timer(heartbeat_frq, self.heartbeat).start()
diff --git a/src/aura_engine/scheduling/api.py b/src/aura_engine/scheduling/api.py
index d03ad07f..a1c12035 100644
--- a/src/aura_engine/scheduling/api.py
+++ b/src/aura_engine/scheduling/api.py
@@ -79,12 +79,12 @@ class ApiFetcher(threading.Thread):
         """
         Initialize the API Fetcher.
         """
-        self.config = AuraConfig.config()
+        self.config = AuraConfig.instance.config
         self.logger = logging.getLogger("engine")
-        cache_location = self.config.get("cache_dir")
+        cache_location = self.config.general.cache_dir
         self.api = SimpleCachedRestApi(SimpleRestApi(), cache_location)
-        self.url_api_timeslots = self.config.get("api_steering_calendar")
-        self.url_api_playlist = self.config.get("api_tank_playlist")
+        self.url_api_timeslots = self.config.api.steering.calendar
+        self.url_api_playlist = self.config.api.tank.playlist
         self.queue = queue.Queue()
         self.stop_event = threading.Event()
 
diff --git a/src/aura_engine/scheduling/scheduler.py b/src/aura_engine/scheduling/scheduler.py
index aed67a5c..2e4e9336 100644
--- a/src/aura_engine/scheduling/scheduler.py
+++ b/src/aura_engine/scheduling/scheduler.py
@@ -278,8 +278,8 @@ class AuraScheduler(threading.Thread):
         """
         while not self.exit_event.is_set():
             try:
-                self.config.load_config()
-                seconds_to_wait = int(self.config.get("fetching_frequency"))
+                AuraConfig.instance.load_config()
+                seconds_to_wait = self.config.scheduler.fetching_frequency
                 msg = f"== start fetching new timeslots (every {seconds_to_wait} seconds) =="
                 self.logger.info(SU.cyan(msg))
 
diff --git a/src/aura_engine/scheduling/utils.py b/src/aura_engine/scheduling/utils.py
index a35f54fa..d96b7a0a 100644
--- a/src/aura_engine/scheduling/utils.py
+++ b/src/aura_engine/scheduling/utils.py
@@ -45,7 +45,7 @@ class M3UPlaylistProcessor:
         """
         Initialize.
         """
-        self.config = AuraConfig.config()
+        self.config = AuraConfig.instance
         self.logger = logging.getLogger("engine")
         self.playlist_folder = self.config.abs_playlist_path()
 
diff --git a/tests/test_config.py b/tests/test_config.py
index 11051e7a..f5fc8651 100644
--- a/tests/test_config.py
+++ b/tests/test_config.py
@@ -39,23 +39,24 @@ class TestConfig(unittest.TestCase):
         print(self._testMethodName)
 
         # Check if config is available
-        self.assertIsNotNone(self.config.ini_path)
+        self.assertIsNotNone(self.config.yaml_path)
 
         # Check if "install_dir" is a valid directory (is evaluated at runtime)
-        self.assertTrue(os.path.isdir(self.config.get("install_dir")))
+        self.assertTrue(os.path.isdir(self.config.confuse_config["install_dir"].get()))
+
+        # Reference to confuse config
+        cfg = self.config.config
 
         # Check API Urls
-        self.assertTrue(validators.url(self.config.get("api_steering_status")))
-        self.assertTrue(validators.url(self.config.get("api_steering_calendar")))
-        self.assertTrue(validators.url(self.config.get("api_tank_status")))
-        tank_playlist_url = self.config.get("api_tank_playlist").replace("${ID}", "1")
+        self.assertTrue(validators.url(cfg.api.steering.status))
+        self.assertTrue(validators.url(cfg.api.steering.calendar))
+        self.assertTrue(validators.url(cfg.api.tank.status))
+        tank_playlist_url = cfg.api.tank.playlist.replace("${ID}", "1")
         self.assertTrue(validators.url(tank_playlist_url))
-        self.assertTrue(validators.url(self.config.get("api_engine_status")))
-        self.assertTrue(validators.url(self.config.get("api_engine_store_playlog")))
-        self.assertTrue(validators.url(self.config.get("api_engine_store_clock")))
-        engine_health_url = self.config.get("api_engine_store_health").replace(
-            "${ENGINE_NUMBER}", "1"
-        )
+        self.assertTrue(validators.url(cfg.api.engine.status))
+        self.assertTrue(validators.url(cfg.api.engine.store_playlog))
+        self.assertTrue(validators.url(cfg.api.engine.store_clock))
+        engine_health_url = cfg.api.engine.store_health.replace("${ENGINE_NUMBER}", "1")
         self.assertTrue(validators.url(engine_health_url))
 
 
diff --git a/tests/test_logger.py b/tests/test_logger.py
index ac532bd8..4344b55c 100644
--- a/tests/test_logger.py
+++ b/tests/test_logger.py
@@ -31,7 +31,7 @@ class TestLogger(unittest.TestCase):
     aura_logger = None
 
     def setUp(self):
-        self.config = AuraConfig()
+        self.config = AuraConfig().config
         self.aura_logger = AuraLogger(self.config)
 
     def test_logger(self):
diff --git a/tests/test_simple_api.py b/tests/test_simple_api.py
index 5f445030..7097198f 100644
--- a/tests/test_simple_api.py
+++ b/tests/test_simple_api.py
@@ -101,7 +101,7 @@ class TestApi(unittest.TestCase):
         print(self._testMethodName)
 
         # Check if config is available
-        self.assertIsNotNone(self.config.ini_path)
+        self.assertIsNotNone(self.config.yaml_path)
 
     def test_clean_dict(self):
         print(self._testMethodName)
diff --git a/tests/test_simple_api_cached.py b/tests/test_simple_api_cached.py
index e1168b55..6c96c5c8 100644
--- a/tests/test_simple_api_cached.py
+++ b/tests/test_simple_api_cached.py
@@ -63,7 +63,7 @@ class TestCachedApi(unittest.TestCase):
 
     def setUp(self):
         self.config = AuraConfig()
-        cache_location = self.config.get("cache_dir")
+        cache_location = self.config.config.general.cache_dir
         self.api = SimpleCachedRestApi(SimpleRestApi(), cache_location)
 
     #
@@ -74,7 +74,7 @@ class TestCachedApi(unittest.TestCase):
         print(self._testMethodName)
 
         # Check if config is available
-        self.assertIsNotNone(self.config.ini_path)
+        self.assertIsNotNone(self.config.yaml_path)
 
     def test_build_filename(self):
         url = "https://dashboard.aura.radio/steering/api/v1/playout"
-- 
GitLab