v0.8.30-beta - depends_on in sjablonen, en vijftien domotica-apps
All checks were successful
Deploy server-up (dev) / deploy (push) Successful in 13m41s

- Nieuwe metadata-sleutel depends_on: een sjabloon kan zeggen welke andere app
  het nodig heeft en waarom. Het installatiescherm toont dat voor het invullen,
  met een knop naar dat sjabloon. Advies, geen blokkade -- de broker of Home
  Assistant kan net zo goed op een andere machine staan
- De reden is verplicht; een waarschuwing zonder uitleg klik je weg. De korte
  vorm ["mosquitto"] werkt ook, voor sjablonen uit externe repo's
- Toegepast op acht sjablonen waar het gemis onzichtbaar was: zigbee2mqtt,
  alloy, alertmanager, node-exporter, cadvisor, collabora, double-take, soularr.
  Niet op element, open-webui, exportarr, unpackerr en onlyoffice: die werken
  met meerdere alternatieven, en depends_on kent geen "een van deze"
- Vijftien domotica-apps, van 12 naar 27 in die categorie: Piper, Whisper en
  openWakeWord (spraakbediening van Home Assistant, tot nu toe volledig
  afwezig), Matter Server, Matter Hub, AppDaemon, go2rtc, Double Take,
  Domoticz, openHAB, DSMR-reader (P1-poort slimme meter), rtl_433 (433 MHz
  sensoren naar MQTT), Traccar, Snapcast en EMQX
- Catalogus van 241 naar 256 apps; alle 285 images gecontroleerd

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q9eqpADJSRs49SoGGr4NAy
This commit is contained in:
Ramon 2026-08-01 04:58:50 +02:00
parent 42bd87047a
commit b54e7003e8
49 changed files with 1798 additions and 6 deletions

View file

@ -1,3 +1,42 @@
# v0.8.30-beta — Sjablonen kunnen zeggen wat ze nodig hebben, en vijftien domotica-apps
**Een app kon niet zeggen dat hij een andere nodig had.** Het `connect`-veld
wijst naar een andere app en vult een adres in, maar zegt niets over noodzaak.
Daardoor kon je Zigbee2MQTT installeren zonder MQTT-broker, Alertmanager zonder
Prometheus en Grafana Alloy zonder Loki. Alle drie starten ze, doen niets, en de
reden staat nergens.
Sjablonen kunnen dat nu wél zeggen, met `depends_on` in de metadata. Het
installatiescherm toont vóór het invullen wat er ontbreekt, met de reden erbij en
een knop die je meteen naar dat sjabloon brengt. **Advies, geen blokkade** — je
kunt de broker of Home Assistant net zo goed op een andere machine hebben staan.
De reden is verplicht: een waarschuwing zonder uitleg klik je weg.
Meteen toegepast op acht sjablonen waar het gemis tot nu toe onzichtbaar was:
Zigbee2MQTT, Alloy, Alertmanager, node-exporter, cAdvisor, Collabora, Double Take
en Soularr. Bewust níét op Element, open-webui, Exportarr, Unpackerr en ONLYOFFICE:
die werken met meerdere alternatieven, en `depends_on` kent geen "een van deze".
**Vijftien domotica-apps.** Van 12 naar 27 in die categorie — vooral het spul dat
*om* Home Assistant heen hoort:
- **Stemassistent**, tot nu toe volledig afwezig: Piper (tekst naar spraak),
Whisper (spraak naar tekst) en openWakeWord (het activeerwoord). Samen maken ze
de spraakbediening van HA compleet zonder dat er iets naar een clouddienst gaat.
- **Matter Server** en **Matter Hub**, plus **AppDaemon** voor automatiseringen in
Python
- **go2rtc** (camerastreams) en **Double Take** (gezichtsherkenning op Frigate)
- **Domoticz** en **openHAB** als alternatief voor Home Assistant zelf
- Voor de Nederlandse opstelling: **DSMR-reader** leest de P1-poort van je slimme
meter uit, **rtl_433** vangt de 433 MHz-sensoren van weerstations en regenmeters
op en zet ze op MQTT
- **Traccar** (GPS-tracking), **Snapcast** (audio door het hele huis) en **EMQX**
(MQTT-broker met webinterface)
Drie daarvan hebben een fysiek apparaat nodig — een P1-kabel, een SDR-stick, een
Zigbee-dongle. Die krijgen een veld met uitleg over hoe je het juiste pad vindt,
in plaats van een gok die stilzwijgend faalt.
# v0.8.20-beta — Prometheus startte niet, en 23 apps erbij
**Prometheus stond kapot in de catalogus.** Het sjabloon koppelde een lege map op

View file

@ -16,7 +16,7 @@ IP-adres in je netwerk geven.
| | |
|---|---|
| **App-catalogus** | 241 kant-en-klare apps, ingedeeld in categorieën met kleur. Zoeken, filteren en installeren met een formulier in plaats van YAML. |
| **App-catalogus** | 256 kant-en-klare apps, ingedeeld in categorieën met kleur. Zoeken, filteren en installeren met een formulier in plaats van YAML. |
| **Stacks beheren** | Starten, stoppen, bijwerken en herstarten — per stack of per losse container, met logs en live CPU-/geheugengebruik. |
| **Apps koppelen** | Stacks komen op een gedeeld netwerk, zodat ze elkaar op naam bereiken. Een veld dat naar een andere app verwijst toont een keuzelijst met wat je al draait. |
| **Eigen IP per stack** | Met macvlan of ipvlan krijgt een app een eigen adres in je LAN in plaats van poorten op de host. |

View file

@ -1 +1 @@
0.8.20-beta
0.8.30-beta

View file

@ -18,6 +18,12 @@
"categories": [
"monitoring",
"communicatie"
],
"depends_on": [
{
"app": "prometheus",
"reason": "Alertmanager verwerkt de alarmen die Prometheus opwerpt; los draaiend krijgt hij er nooit een."
}
]
},
"variables": [

View file

@ -17,6 +17,12 @@
},
"categories": [
"monitoring"
],
"depends_on": [
{
"app": "loki",
"reason": "Alloy verzamelt logs en stuurt ze door. Zonder Loki om ze te bewaren gaan ze meteen weer verloren."
}
]
},
"variables": [

View file

@ -0,0 +1,14 @@
services:
<< service_name >>:
image: acockburn/appdaemon:latest
container_name: << service_name >>
environment:
- TZ=<< timezone >>
- HA_URL=<< ha_url >>
- TOKEN=<< ha_token >>
- DASH_URL=<< dash_url >>
volumes:
- << appdata_dir >>/<< service_name >>:/conf
ports:
- "<< port >>:5050"
restart: unless-stopped

View file

@ -0,0 +1,106 @@
{
"kind": "compose",
"metadata": {
"name": "AppDaemon",
"description": "Automatiseringen voor Home Assistant in Python in plaats van YAML. Voor logica die met de ingebouwde automatiseringen onhandig wordt.",
"tags": [
"domotica",
"automatisering",
"python"
],
"icon": {
"provider": "selfhst",
"id": "appdaemon"
},
"version": {
"name": "latest"
},
"categories": [
"domotica",
"ontwikkeling"
],
"depends_on": [
{
"app": "home-assistant",
"reason": "Dit is een dienst die Home Assistant aanroept; los draaiend doet hij niets."
}
]
},
"variables": [
{
"title": "Algemeen",
"items": [
{
"name": "service_name",
"type": "str",
"title": "Servicenaam",
"default": "appdaemon",
"required": true
},
{
"name": "port",
"type": "int",
"title": "Webinterface",
"default": 5050,
"required": true
},
{
"name": "appdata_dir",
"type": "str",
"title": "Appdata-map",
"default": "/opt/serverup/appdata",
"required": true,
"description": "Basismap voor de gegevens van deze app"
},
{
"name": "timezone",
"type": "str",
"title": "Tijdzone",
"default": "Europe/Amsterdam",
"required": true
}
]
},
{
"title": "Instellingen",
"items": [
{
"name": "ha_url",
"type": "str",
"title": "Adres van Home Assistant",
"default": "http://homeassistant:8123",
"required": true,
"connect": {
"app": "home-assistant",
"scheme": "http",
"port": 8123
},
"help": "Waar AppDaemon Home Assistant bereikt."
},
{
"name": "dash_url",
"type": "str",
"title": "Eigen adres",
"default": "http://localhost:5050",
"required": true,
"description": "Waar het dashboard van AppDaemon zelf op staat"
}
]
},
{
"title": "Toegang",
"items": [
{
"name": "ha_token",
"type": "str",
"title": "Toegangstoken van Home Assistant",
"default": "",
"required": true,
"secret": true,
"eigen_invoer": true,
"description": "Maak er een aan onder je profiel in Home Assistant"
}
]
}
]
}

View file

@ -19,6 +19,12 @@
"categories": [
"monitoring",
"beheer"
],
"depends_on": [
{
"app": "prometheus",
"reason": "Levert meetwaarden per container waar Prometheus ze komt ophalen."
}
]
},
"variables": [

View file

@ -18,6 +18,12 @@
"categories": [
"documenten",
"productiviteit"
],
"depends_on": [
{
"app": "nextcloud-aio",
"reason": "Collabora is de bewerker; de bestanden en de gebruikers komen uit Nextcloud. Los draaiend heb je niets om te openen."
}
]
},
"variables": [

View file

@ -0,0 +1,14 @@
services:
<< service_name >>:
image: domoticz/domoticz:stable
container_name: << service_name >>
environment:
- TZ=<< timezone >>
- WWW_PORT=8080
- SSL_PORT=6144
volumes:
- << appdata_dir >>/<< service_name >>:/opt/domoticz/userdata
ports:
- "<< port >>:8080"
- "<< port_ssl >>:6144"
restart: unless-stopped

View file

@ -0,0 +1,65 @@
{
"kind": "compose",
"metadata": {
"name": "Domoticz",
"description": "Lichtgewicht domoticasysteem dat op bescheiden hardware draait, met veel ondersteuning voor oudere protocollen. Let op: Heb je een USB-stick voor Z-Wave of RFXCOM, geef die dan door onder `devices` in het compose-bestand.",
"tags": [
"domotica",
"automatisering"
],
"icon": {
"provider": "selfhst",
"id": "domoticz"
},
"version": {
"name": "latest"
},
"categories": [
"domotica"
]
},
"variables": [
{
"title": "Algemeen",
"items": [
{
"name": "service_name",
"type": "str",
"title": "Servicenaam",
"default": "domoticz",
"required": true
},
{
"name": "port",
"type": "int",
"title": "Webinterface",
"default": 8219,
"required": true,
"description": "Standaard 8080; hier verplaatst omdat die poort al bezet is"
},
{
"name": "port_ssl",
"type": "int",
"title": "Beveiligde poort",
"default": 6144,
"required": true
},
{
"name": "appdata_dir",
"type": "str",
"title": "Appdata-map",
"default": "/opt/serverup/appdata",
"required": true,
"description": "Basismap voor de gegevens van deze app"
},
{
"name": "timezone",
"type": "str",
"title": "Tijdzone",
"default": "Europe/Amsterdam",
"required": true
}
]
}
]
}

View file

@ -0,0 +1,11 @@
services:
<< service_name >>:
image: jakowenko/double-take:latest
container_name: << service_name >>
environment:
- TZ=<< timezone >>
volumes:
- << appdata_dir >>/<< service_name >>:/.storage
ports:
- "<< port >>:3000"
restart: unless-stopped

View file

@ -0,0 +1,66 @@
{
"kind": "compose",
"metadata": {
"name": "Double Take",
"description": "Herkent gezichten in de beelden die Frigate doorgeeft, en meldt aan Home Assistant wie er voor de deur staat.",
"tags": [
"camera",
"gezichtsherkenning",
"domotica"
],
"icon": {
"provider": "selfhst",
"id": "double-take"
},
"version": {
"name": "latest"
},
"categories": [
"domotica",
"beveiliging"
],
"depends_on": [
{
"app": "frigate",
"reason": "Double Take verwerkt de beelden die Frigate aanlevert; zonder Frigate krijgt het niets binnen."
}
]
},
"variables": [
{
"title": "Algemeen",
"items": [
{
"name": "service_name",
"type": "str",
"title": "Servicenaam",
"default": "doubletake",
"required": true
},
{
"name": "port",
"type": "int",
"title": "Webinterface",
"default": 8224,
"required": true,
"description": "Standaard 3000; hier verplaatst omdat die poort al bezet is"
},
{
"name": "appdata_dir",
"type": "str",
"title": "Appdata-map",
"default": "/opt/serverup/appdata",
"required": true,
"description": "Basismap voor de gegevens van deze app"
},
{
"name": "timezone",
"type": "str",
"title": "Tijdzone",
"default": "Europe/Amsterdam",
"required": true
}
]
}
]
}

View file

@ -0,0 +1,34 @@
services:
<< service_name >>:
image: xirixiz/dsmr-reader-docker:latest
container_name: << service_name >>
environment:
- DJANGO_TIME_ZONE=<< timezone >>
- DJANGO_SECRET_KEY=<< secret_key >>
- DJANGO_DATABASE_HOST=<< service_name >>-db
- DJANGO_DATABASE_NAME=dsmrreader
- DJANGO_DATABASE_USER=dsmrreader
- DJANGO_DATABASE_PASSWORD=<< db_password >>
- DSMR_USER=<< admin_user >>
- DSMR_PASSWORD=<< admin_password >>
- DSMRREADER_REMOTE_DATALOGGER_INPUT_METHOD=serial
- DSMRREADER_REMOTE_DATALOGGER_SERIAL_PORT=/dev/ttyUSB0
volumes:
- << appdata_dir >>/<< service_name >>:/app/backups
ports:
- "<< port >>:80"
depends_on:
- << service_name >>-db
devices:
- << p1_apparaat >>:/dev/ttyUSB0
restart: unless-stopped
<< service_name >>-db:
image: postgres:16-alpine
container_name: << service_name >>-db
environment:
- POSTGRES_DB=dsmrreader
- POSTGRES_USER=dsmrreader
- POSTGRES_PASSWORD=<< db_password >>
volumes:
- << appdata_dir >>/<< service_name >>-db:/var/lib/postgresql/data
restart: unless-stopped

View file

@ -0,0 +1,114 @@
{
"kind": "compose",
"metadata": {
"name": "DSMR-reader",
"description": "Leest de P1-poort van je slimme meter uit en houdt verbruik, teruglevering en gasstanden bij, met grafieken per dag en per jaar.",
"tags": [
"energie",
"slimme-meter",
"p1"
],
"icon": {
"provider": "selfhst",
"id": "dsmr-reader"
},
"version": {
"name": "latest"
},
"categories": [
"domotica",
"gezin"
]
},
"variables": [
{
"title": "Algemeen",
"items": [
{
"name": "service_name",
"type": "str",
"title": "Servicenaam",
"default": "dsmrreader",
"required": true
},
{
"name": "port",
"type": "int",
"title": "Webinterface",
"default": 8221,
"required": true,
"description": "Standaard 80; hier verplaatst omdat die poort al bezet is"
},
{
"name": "appdata_dir",
"type": "str",
"title": "Appdata-map",
"default": "/opt/serverup/appdata",
"required": true,
"description": "Basismap voor de gegevens van deze app"
},
{
"name": "timezone",
"type": "str",
"title": "Tijdzone",
"default": "Europe/Amsterdam",
"required": true
}
]
},
{
"title": "Instellingen",
"items": [
{
"name": "p1_apparaat",
"type": "str",
"title": "P1-kabel",
"default": "/dev/ttyUSB0",
"required": true,
"description": "Het apparaat van je P1-kabel op de host",
"help": "Zoek het met `ls -l /dev/serial/by-id/`. Gebruik bij voorkeur dat pad in plaats van /dev/ttyUSB0: dat nummer kan na een herstart bij een ander apparaat horen."
},
{
"name": "admin_user",
"type": "str",
"title": "Gebruikersnaam",
"default": "admin",
"required": true
}
]
},
{
"title": "Toegang",
"items": [
{
"name": "db_password",
"type": "str",
"title": "Databasewachtwoord",
"default": "",
"required": true,
"secret": true,
"generate": "auto",
"description": "Wordt automatisch ingevuld en in .env bewaard"
},
{
"name": "secret_key",
"type": "str",
"title": "Django-sleutel",
"default": "",
"required": true,
"secret": true,
"generate": "auto"
},
{
"name": "admin_password",
"type": "str",
"title": "Wachtwoord",
"default": "",
"required": true,
"secret": true,
"eigen_invoer": true
}
]
}
]
}

View file

@ -0,0 +1,14 @@
services:
<< service_name >>:
image: emqx/emqx:latest
container_name: << service_name >>
environment:
- TZ=<< timezone >>
volumes:
- << appdata_dir >>/<< service_name >>:/opt/emqx/data
- << appdata_dir >>/<< service_name >>-log:/opt/emqx/log
ports:
- "<< port_mqtt >>:1883"
- "<< port >>:18083"
- "<< port_ws >>:8083"
restart: unless-stopped

76
apps/emqx/template.json Normal file
View file

@ -0,0 +1,76 @@
{
"kind": "compose",
"metadata": {
"name": "EMQX",
"description": "MQTT-broker met een webinterface, regels en clustering. Zwaarder dan Mosquitto, maar je ziet wat er langskomt. Let op: Het standaardaccount is admin met wachtwoord public — wijzig dat meteen na de eerste keer inloggen.",
"tags": [
"mqtt",
"broker",
"domotica"
],
"icon": {
"provider": "selfhst",
"id": "emqx"
},
"version": {
"name": "latest"
},
"categories": [
"domotica",
"netwerk"
]
},
"variables": [
{
"title": "Algemeen",
"items": [
{
"name": "service_name",
"type": "str",
"title": "Servicenaam",
"default": "emqx",
"required": true
},
{
"name": "port_mqtt",
"type": "int",
"title": "MQTT-poort",
"default": 1884,
"required": true,
"description": "Standaard 1883; hier verplaatst omdat Mosquitto die al gebruikt"
},
{
"name": "port",
"type": "int",
"title": "Webinterface",
"default": 8223,
"required": true,
"description": "Standaard 18083; hier verplaatst omdat die poort al bezet is"
},
{
"name": "port_ws",
"type": "int",
"title": "Websocket-poort",
"default": 8227,
"required": true,
"description": "Standaard 8083; hier verplaatst omdat die poort al bezet is"
},
{
"name": "appdata_dir",
"type": "str",
"title": "Appdata-map",
"default": "/opt/serverup/appdata",
"required": true,
"description": "Basismap voor de gegevens van deze app"
},
{
"name": "timezone",
"type": "str",
"title": "Tijdzone",
"default": "Europe/Amsterdam",
"required": true
}
]
}
]
}

View file

@ -0,0 +1,14 @@
services:
<< service_name >>:
image: alexxit/go2rtc:latest
container_name: << service_name >>
environment:
- TZ=<< timezone >>
volumes:
- << appdata_dir >>/<< service_name >>:/config
ports:
- "<< port >>:1984"
- "<< port_rtsp >>:8554"
- "<< port_webrtc >>:8555"
- "<< port_webrtc >>:8555/udp"
restart: unless-stopped

74
apps/go2rtc/template.json Normal file
View file

@ -0,0 +1,74 @@
{
"kind": "compose",
"metadata": {
"name": "go2rtc",
"description": "Zet camerastreams om naar een formaat dat je browser aankan, met bijna geen vertraging. Werkt los, en zit ook in Frigate ingebouwd. Let op: De camera's zet je in go2rtc.yaml in de configuratiemap.",
"tags": [
"camera",
"streaming",
"domotica"
],
"icon": {
"provider": "selfhst",
"id": "go2rtc"
},
"version": {
"name": "latest"
},
"categories": [
"domotica",
"media"
]
},
"variables": [
{
"title": "Algemeen",
"items": [
{
"name": "service_name",
"type": "str",
"title": "Servicenaam",
"default": "go2rtc",
"required": true
},
{
"name": "port",
"type": "int",
"title": "Webinterface",
"default": 1984,
"required": true
},
{
"name": "port_rtsp",
"type": "int",
"title": "RTSP-poort",
"default": 8228,
"required": true,
"description": "Standaard 8554; hier verplaatst omdat Frigate die al gebruikt"
},
{
"name": "port_webrtc",
"type": "int",
"title": "WebRTC-poort",
"default": 8555,
"required": true
},
{
"name": "appdata_dir",
"type": "str",
"title": "Appdata-map",
"default": "/opt/serverup/appdata",
"required": true,
"description": "Basismap voor de gegevens van deze app"
},
{
"name": "timezone",
"type": "str",
"title": "Tijdzone",
"default": "Europe/Amsterdam",
"required": true
}
]
}
]
}

View file

@ -0,0 +1,15 @@
services:
<< service_name >>:
image: ghcr.io/t0bst4r/home-assistant-matter-hub:latest
container_name: << service_name >>
environment:
- TZ=<< timezone >>
- HAMH_HOME_ASSISTANT_URL=<< ha_url >>
- HAMH_HOME_ASSISTANT_ACCESS_TOKEN=<< ha_token >>
- HAMH_LOG_LEVEL=info
volumes:
- << appdata_dir >>/<< service_name >>:/data
ports:
- "<< port >>:8482"
network_mode: host
restart: unless-stopped

View file

@ -0,0 +1,97 @@
{
"kind": "compose",
"metadata": {
"name": "Matter Hub",
"description": "Biedt je Home Assistant-apparaten aan als Matter-bridge, zodat Alexa, Google Home en Apple Home ze zien. Let op: Draait op het netwerk van de host, want Matter-apparaten vinden elkaar via uitzendingen op je LAN.",
"tags": [
"matter",
"domotica",
"bridge"
],
"icon": {
"provider": "selfhst",
"id": "matter"
},
"version": {
"name": "latest"
},
"categories": [
"domotica"
],
"depends_on": [
{
"app": "home-assistant",
"reason": "Dit is een dienst die Home Assistant aanroept; los draaiend doet hij niets."
}
]
},
"variables": [
{
"title": "Algemeen",
"items": [
{
"name": "service_name",
"type": "str",
"title": "Servicenaam",
"default": "matterhub",
"required": true
},
{
"name": "port",
"type": "int",
"title": "Webinterface",
"default": 8482,
"required": true
},
{
"name": "appdata_dir",
"type": "str",
"title": "Appdata-map",
"default": "/opt/serverup/appdata",
"required": true,
"description": "Basismap voor de gegevens van deze app"
},
{
"name": "timezone",
"type": "str",
"title": "Tijdzone",
"default": "Europe/Amsterdam",
"required": true
}
]
},
{
"title": "Instellingen",
"items": [
{
"name": "ha_url",
"type": "str",
"title": "Adres van Home Assistant",
"default": "http://homeassistant:8123",
"required": true,
"connect": {
"app": "home-assistant",
"scheme": "http",
"port": 8123
},
"help": "Het adres waarop de bridge Home Assistant bereikt."
}
]
},
{
"title": "Toegang",
"items": [
{
"name": "ha_token",
"type": "str",
"title": "Toegangstoken van Home Assistant",
"default": "",
"required": true,
"secret": true,
"eigen_invoer": true,
"description": "Maak er een aan onder je profiel, onderaan bij 'Langlopende toegangstokens'"
}
]
}
]
}

View file

@ -0,0 +1,12 @@
services:
<< service_name >>:
image: ghcr.io/home-assistant-libs/python-matter-server:stable
container_name: << service_name >>
environment:
- TZ=<< timezone >>
volumes:
- << appdata_dir >>/<< service_name >>:/data
ports:
- "<< port >>:5580"
network_mode: host
restart: unless-stopped

View file

@ -0,0 +1,65 @@
{
"kind": "compose",
"metadata": {
"name": "Matter Server",
"description": "Laat Home Assistant met Matter-apparaten praten. Draait als aparte dienst omdat het een eigen Thread- en Wi-Fi-stack meebrengt. Let op: Draait op het netwerk van de host: Matter en Thread werken met uitzendingen over IPv6, en die komen een Docker-bridge niet uit.",
"tags": [
"matter",
"thread",
"domotica"
],
"icon": {
"provider": "selfhst",
"id": "matter"
},
"version": {
"name": "latest"
},
"categories": [
"domotica"
],
"depends_on": [
{
"app": "home-assistant",
"reason": "Dit is een dienst die Home Assistant aanroept; los draaiend doet hij niets."
}
]
},
"variables": [
{
"title": "Algemeen",
"items": [
{
"name": "service_name",
"type": "str",
"title": "Servicenaam",
"default": "matterserver",
"required": true
},
{
"name": "port",
"type": "int",
"title": "Websocket-poort",
"default": 5580,
"required": true,
"description": "Hier maakt Home Assistant verbinding mee"
},
{
"name": "appdata_dir",
"type": "str",
"title": "Appdata-map",
"default": "/opt/serverup/appdata",
"required": true,
"description": "Basismap voor de gegevens van deze app"
},
{
"name": "timezone",
"type": "str",
"title": "Tijdzone",
"default": "Europe/Amsterdam",
"required": true
}
]
}
]
}

View file

@ -17,6 +17,12 @@
},
"categories": [
"monitoring"
],
"depends_on": [
{
"app": "prometheus",
"reason": "Levert meetwaarden waar Prometheus ze komt ophalen. Zonder Prometheus meet hij in het luchtledige."
}
]
},
"variables": [

View file

@ -0,0 +1,17 @@
services:
<< service_name >>:
image: openhab/openhab:latest
container_name: << service_name >>
environment:
- TZ=<< timezone >>
- OPENHAB_HTTP_PORT=8080
- OPENHAB_HTTPS_PORT=8443
- EXTRA_JAVA_OPTS=-Duser.timezone=<< timezone >>
volumes:
- << appdata_dir >>/<< service_name >>:/openhab/conf
- << appdata_dir >>/<< service_name >>-userdata:/openhab/userdata
- << appdata_dir >>/<< service_name >>-addons:/openhab/addons
ports:
- "<< port >>:8080"
- "<< port_https >>:8443"
restart: unless-stopped

View file

@ -0,0 +1,67 @@
{
"kind": "compose",
"metadata": {
"name": "openHAB",
"description": "Domoticasysteem met ruim vierhonderd koppelingen en een eigen regeltaal. Zwaarder dan Home Assistant, maar krachtiger in complexe opstellingen.",
"tags": [
"domotica",
"automatisering",
"java"
],
"icon": {
"provider": "selfhst",
"id": "openhab"
},
"version": {
"name": "latest"
},
"categories": [
"domotica"
]
},
"variables": [
{
"title": "Algemeen",
"items": [
{
"name": "service_name",
"type": "str",
"title": "Servicenaam",
"default": "openhab",
"required": true
},
{
"name": "port",
"type": "int",
"title": "Webinterface",
"default": 8220,
"required": true,
"description": "Standaard 8080; hier verplaatst omdat die poort al bezet is"
},
{
"name": "port_https",
"type": "int",
"title": "HTTPS-poort",
"default": 8229,
"required": true,
"description": "Standaard 8443; hier verplaatst omdat die poort al bezet is"
},
{
"name": "appdata_dir",
"type": "str",
"title": "Appdata-map",
"default": "/opt/serverup/appdata",
"required": true,
"description": "Basismap voor de gegevens van deze app"
},
{
"name": "timezone",
"type": "str",
"title": "Tijdzone",
"default": "Europe/Amsterdam",
"required": true
}
]
}
]
}

View file

@ -0,0 +1,12 @@
services:
<< service_name >>:
image: rhasspy/wyoming-openwakeword:latest
container_name: << service_name >>
environment:
- TZ=<< timezone >>
volumes:
- << appdata_dir >>/<< service_name >>:/data
command: --preload-model << activeerwoord >>
ports:
- "<< port >>:10400"
restart: unless-stopped

View file

@ -0,0 +1,87 @@
{
"kind": "compose",
"metadata": {
"name": "openWakeWord",
"description": "Luistert naar het activeerwoord waarmee je een gesprek met Home Assistant begint, zonder dat er iets meeluistert bij een ander.",
"tags": [
"spraak",
"domotica",
"wyoming"
],
"icon": {
"provider": "selfhst",
"id": "home-assistant"
},
"version": {
"name": "latest"
},
"categories": [
"domotica",
"ai"
],
"depends_on": [
{
"app": "home-assistant",
"reason": "Dit is een dienst die Home Assistant aanroept; los draaiend doet hij niets."
}
]
},
"variables": [
{
"title": "Algemeen",
"items": [
{
"name": "service_name",
"type": "str",
"title": "Servicenaam",
"default": "openwakeword",
"required": true
},
{
"name": "port",
"type": "int",
"title": "Wyoming-poort",
"default": 10400,
"required": true,
"description": "Hier maakt Home Assistant verbinding mee"
},
{
"name": "appdata_dir",
"type": "str",
"title": "Appdata-map",
"default": "/opt/serverup/appdata",
"required": true,
"description": "Basismap voor de gegevens van deze app"
},
{
"name": "timezone",
"type": "str",
"title": "Tijdzone",
"default": "Europe/Amsterdam",
"required": true
}
]
},
{
"title": "Instellingen",
"items": [
{
"name": "activeerwoord",
"type": "enum",
"title": "Activeerwoord",
"default": "ok_nabu",
"required": true,
"config": {
"options": [
"ok_nabu",
"hey_jarvis",
"alexa",
"hey_mycroft",
"hey_rhasspy"
]
}
}
]
}
]
}

View file

@ -0,0 +1,12 @@
services:
<< service_name >>:
image: rhasspy/wyoming-piper:latest
container_name: << service_name >>
environment:
- TZ=<< timezone >>
volumes:
- << appdata_dir >>/<< service_name >>:/data
command: --voice << stem >>
ports:
- "<< port >>:10200"
restart: unless-stopped

80
apps/piper/template.json Normal file
View file

@ -0,0 +1,80 @@
{
"kind": "compose",
"metadata": {
"name": "Piper",
"description": "Zet tekst om in spraak, op je eigen server. Home Assistant praat hiermee terug zonder dat er iets naar een clouddienst gaat.",
"tags": [
"spraak",
"domotica",
"wyoming"
],
"icon": {
"provider": "selfhst",
"id": "home-assistant"
},
"version": {
"name": "latest"
},
"categories": [
"domotica",
"ai"
],
"depends_on": [
{
"app": "home-assistant",
"reason": "Dit is een dienst die Home Assistant aanroept; los draaiend doet hij niets."
}
]
},
"variables": [
{
"title": "Algemeen",
"items": [
{
"name": "service_name",
"type": "str",
"title": "Servicenaam",
"default": "piper",
"required": true
},
{
"name": "port",
"type": "int",
"title": "Wyoming-poort",
"default": 10200,
"required": true,
"description": "Hier maakt Home Assistant verbinding mee"
},
{
"name": "appdata_dir",
"type": "str",
"title": "Appdata-map",
"default": "/opt/serverup/appdata",
"required": true,
"description": "Basismap voor de gegevens van deze app"
},
{
"name": "timezone",
"type": "str",
"title": "Tijdzone",
"default": "Europe/Amsterdam",
"required": true
}
]
},
{
"title": "Instellingen",
"items": [
{
"name": "stem",
"type": "str",
"title": "Stem",
"default": "nl_BE-nathalie-medium",
"required": true,
"description": "Bijvoorbeeld nl_BE-nathalie-medium of en_US-lessac-medium",
"help": "De stem wordt bij de eerste start gedownload. Nederlandse stemmen beginnen met nl_NL of nl_BE; het achtervoegsel (low, medium, high) bepaalt kwaliteit tegen snelheid."
}
]
}
]
}

View file

@ -0,0 +1,12 @@
services:
<< service_name >>:
image: hertzg/rtl_433:latest
container_name: << service_name >>
environment:
- TZ=<< timezone >>
volumes:
- << appdata_dir >>/<< service_name >>:/etc/rtl_433
command: -F "mqtt://<< mqtt_host >>,retain=1,devices=rtl_433[/model][/id]" -M time -M protocol
devices:
- << sdr_apparaat >>:/dev/bus/usb
restart: unless-stopped

85
apps/rtl433/template.json Normal file
View file

@ -0,0 +1,85 @@
{
"kind": "compose",
"metadata": {
"name": "rtl_433",
"description": "Vangt de signalen op van draadloze sensoren op 433 MHz — weerstations, regenmeters, deursensoren — en zet ze op MQTT.",
"tags": [
"sensoren",
"433mhz",
"mqtt",
"domotica"
],
"icon": {
"provider": "selfhst",
"id": "rtl433"
},
"version": {
"name": "latest"
},
"categories": [
"domotica"
],
"depends_on": [
{
"app": "mosquitto",
"reason": "rtl_433 publiceert alles wat het opvangt naar een MQTT-broker; zonder broker verdwijnen de metingen."
}
]
},
"variables": [
{
"title": "Algemeen",
"items": [
{
"name": "service_name",
"type": "str",
"title": "Servicenaam",
"default": "rtl433",
"required": true
},
{
"name": "appdata_dir",
"type": "str",
"title": "Appdata-map",
"default": "/opt/serverup/appdata",
"required": true,
"description": "Basismap voor de gegevens van deze app"
},
{
"name": "timezone",
"type": "str",
"title": "Tijdzone",
"default": "Europe/Amsterdam",
"required": true
}
]
},
{
"title": "Instellingen",
"items": [
{
"name": "mqtt_host",
"type": "str",
"title": "MQTT-broker",
"default": "mosquitto:1883",
"required": true,
"connect": {
"app": "mosquitto",
"scheme": "",
"port": 1883
},
"help": "Waar de opgevangen metingen naartoe gaan."
},
{
"name": "sdr_apparaat",
"type": "str",
"title": "SDR-stick",
"default": "/dev/bus/usb",
"required": true,
"description": "De USB-bus waar je RTL-SDR-ontvanger op zit",
"help": "De hele USB-bus doorgeven werkt, maar geeft de container toegang tot elk USB-apparaat. Wil je dat niet, zoek dan het pad van juist deze stick met `lsusb`."
}
]
}
]
}

View file

@ -0,0 +1,13 @@
services:
<< service_name >>:
image: saiyato/snapserver:latest
container_name: << service_name >>
environment:
- TZ=<< timezone >>
volumes:
- << appdata_dir >>/<< service_name >>:/etc/snapserver
ports:
- "<< port >>:1704"
- "<< port_control >>:1705"
- "<< port_web >>:1780"
restart: unless-stopped

View file

@ -0,0 +1,73 @@
{
"kind": "compose",
"metadata": {
"name": "Snapcast",
"description": "Speelt muziek gelijk af door het hele huis: elke speaker loopt op de milliseconde synchroon, ook op verschillende apparaten. Let op: De bronnen zet je in snapserver.conf in de configuratiemap. Op elke speaker draai je een snapclient.",
"tags": [
"muziek",
"multiroom",
"audio"
],
"icon": {
"provider": "selfhst",
"id": "snapcast"
},
"version": {
"name": "latest"
},
"categories": [
"domotica",
"media"
]
},
"variables": [
{
"title": "Algemeen",
"items": [
{
"name": "service_name",
"type": "str",
"title": "Servicenaam",
"default": "snapserver",
"required": true
},
{
"name": "port",
"type": "int",
"title": "Streampoort",
"default": 1704,
"required": true
},
{
"name": "port_control",
"type": "int",
"title": "Besturingspoort",
"default": 1705,
"required": true
},
{
"name": "port_web",
"type": "int",
"title": "Webinterface",
"default": 1780,
"required": true
},
{
"name": "appdata_dir",
"type": "str",
"title": "Appdata-map",
"default": "/opt/serverup/appdata",
"required": true,
"description": "Basismap voor de gegevens van deze app"
},
{
"name": "timezone",
"type": "str",
"title": "Tijdzone",
"default": "Europe/Amsterdam",
"required": true
}
]
}
]
}

View file

@ -17,6 +17,16 @@
"categories": [
"downloaden",
"media"
],
"depends_on": [
{
"app": "lidarr",
"reason": "Soularr pakt de gaten op die Lidarr aangeeft; zonder Lidarr weet het niet waarnaar het moet zoeken."
},
{
"app": "slskd",
"reason": "Het zoeken zelf gebeurt via Soulseek, waarvoor slskd de verbinding maakt."
}
]
},
"variables": [

View file

@ -0,0 +1,13 @@
services:
<< service_name >>:
image: traccar/traccar:latest
container_name: << service_name >>
environment:
- TZ=<< timezone >>
volumes:
- << appdata_dir >>/<< service_name >>:/opt/traccar/data
- << appdata_dir >>/<< service_name >>-logs:/opt/traccar/logs
ports:
- "<< port >>:8082"
- "<< port_osmand >>:5055"
restart: unless-stopped

View file

@ -0,0 +1,68 @@
{
"kind": "compose",
"metadata": {
"name": "Traccar",
"description": "Serverkant voor GPS-tracking: ontvangt posities van telefoons en trackers, met kaarten, routes en geofences.",
"tags": [
"gps",
"tracking",
"kaart"
],
"icon": {
"provider": "selfhst",
"id": "traccar"
},
"version": {
"name": "latest"
},
"categories": [
"domotica",
"gezin"
]
},
"variables": [
{
"title": "Algemeen",
"items": [
{
"name": "service_name",
"type": "str",
"title": "Servicenaam",
"default": "traccar",
"required": true
},
{
"name": "port",
"type": "int",
"title": "Webinterface",
"default": 8225,
"required": true,
"description": "Standaard 8082; hier verplaatst omdat die poort al bezet is"
},
{
"name": "port_osmand",
"type": "int",
"title": "OsmAnd-poort",
"default": 8226,
"required": true,
"description": "Waar telefoons hun positie naartoe sturen; standaard 5055, hier verplaatst omdat Jellyseerr die al gebruikt"
},
{
"name": "appdata_dir",
"type": "str",
"title": "Appdata-map",
"default": "/opt/serverup/appdata",
"required": true,
"description": "Basismap voor de gegevens van deze app"
},
{
"name": "timezone",
"type": "str",
"title": "Tijdzone",
"default": "Europe/Amsterdam",
"required": true
}
]
}
]
}

View file

@ -0,0 +1,12 @@
services:
<< service_name >>:
image: rhasspy/wyoming-whisper:latest
container_name: << service_name >>
environment:
- TZ=<< timezone >>
volumes:
- << appdata_dir >>/<< service_name >>:/data
command: --model << model >> --language << taal >>
ports:
- "<< port >>:10300"
restart: unless-stopped

View file

@ -0,0 +1,96 @@
{
"kind": "compose",
"metadata": {
"name": "Whisper",
"description": "Zet spraak om in tekst. De oorkant van de stembediening van Home Assistant, volledig lokaal.",
"tags": [
"spraak",
"domotica",
"wyoming",
"transcriptie"
],
"icon": {
"provider": "selfhst",
"id": "home-assistant"
},
"version": {
"name": "latest"
},
"categories": [
"domotica",
"ai"
],
"depends_on": [
{
"app": "home-assistant",
"reason": "Dit is een dienst die Home Assistant aanroept; los draaiend doet hij niets."
}
]
},
"variables": [
{
"title": "Algemeen",
"items": [
{
"name": "service_name",
"type": "str",
"title": "Servicenaam",
"default": "whisper",
"required": true
},
{
"name": "port",
"type": "int",
"title": "Wyoming-poort",
"default": 10300,
"required": true,
"description": "Hier maakt Home Assistant verbinding mee"
},
{
"name": "appdata_dir",
"type": "str",
"title": "Appdata-map",
"default": "/opt/serverup/appdata",
"required": true,
"description": "Basismap voor de gegevens van deze app"
},
{
"name": "timezone",
"type": "str",
"title": "Tijdzone",
"default": "Europe/Amsterdam",
"required": true
}
]
},
{
"title": "Instellingen",
"items": [
{
"name": "model",
"type": "enum",
"title": "Model",
"default": "base",
"required": true,
"config": {
"options": [
"tiny",
"base",
"small",
"medium",
"large-v3"
]
},
"description": "Groter is nauwkeuriger en trager; base is een goed begin"
},
{
"name": "taal",
"type": "str",
"title": "Taal",
"default": "nl",
"required": true
}
]
}
]
}

View file

@ -17,6 +17,12 @@
},
"categories": [
"domotica"
],
"depends_on": [
{
"app": "mosquitto",
"reason": "Zigbee2MQTT publiceert alles naar een MQTT-broker. Zonder broker start het wel, maar komt er niets aan bij Home Assistant."
}
]
},
"variables": [

View file

@ -373,6 +373,30 @@ containernamen zonder. Het wachtwoord is een veld met `"secret": true` en
belandt in plaats van in het compose-bestand. Vergeet `depends_on` niet, anders
start de app vóór zijn database en valt hij om op de eerste verbinding.
**Een app die een andere nodig heeft.** Een `connect`-veld wijst naar een andere
app en vult een adres in, maar zegt niet dát je die nodig hebt. Zigbee2MQTT
zonder MQTT-broker start prima en doet niets. Zeg het daarom expliciet, in
`metadata`:
```json
"depends_on": [
{"app": "mosquitto",
"reason": "Zigbee2MQTT publiceert alles naar een MQTT-broker. Zonder broker start het wel, maar komt er niets aan."}
]
```
Het installatiescherm toont vóór het invullen wat er nog ontbreekt, met een knop
die je meteen naar dat sjabloon brengt. **Het is advies, geen blokkade** — je kunt
de broker net zo goed op een andere machine hebben staan.
De reden is verplicht: een waarschuwing zonder uitleg is een waarschuwing die
mensen wegklikken. Gebruik het alleen waar het écht klopt. `depends_on` kent geen
"een van deze", dus bij een app die met Nextcloud óf Seafile werkt laat je het
weg; een halve waarheid is daar erger dan geen.
`["mosquitto"]` zonder reden mag ook — die korte vorm is er voor sjablonen uit
externe repo's, die niets van deze sleutel hoeven te weten.
**Sleutels in een bepaald formaat.** `"generate": "auto"` maakt url-veilige
base64 van 24 bytes — 32 tekens. Wil de app iets anders, zeg dat er dan bij:

View file

@ -1334,6 +1334,46 @@ def _gekozen_app_netwerken(body: dict, standaard: str) -> list[str]:
return [standaard] if verbinden else []
def _geinstalleerde_bronnen() -> dict[str, list[str]]:
"""Per app-sjabloon de stacks die eruit geïnstalleerd zijn.
`api_connectable` leidt hetzelfde af uit `.serverup.json`, maar draait daar
een `docker compose ps` per stack overheen om de containernamen op te halen.
Dat is te traag voor een controle die bij elk openen van de installatiemodal
langskomt; hier is alleen de bron nodig.
"""
uit: dict[str, list[str]] = {}
try:
lib = Path(cfg.load()["LIBRARY_DIR"]).resolve()
except Exception:
return uit
if not lib.is_dir():
return uit
for d in sorted(lib.iterdir()):
if not d.is_dir() or d.name.startswith("."):
continue
mf = d / ".serverup.json"
if not mf.exists():
continue
try:
bron = (json.loads(mf.read_text("utf-8")) or {}).get("source") or ""
except Exception:
continue
if bron:
uit.setdefault(bron, []).append(d.name)
return uit
def _afhankelijkheden_met_status(meta: dict) -> list[dict]:
"""De depends_on van een sjabloon, aangevuld met wat er al draait."""
bronnen = _geinstalleerde_bronnen()
uit = []
for dep in meta.get("depends_on") or []:
stacks = bronnen.get(dep["app"], [])
uit.append({**dep, "installed": bool(stacks), "stacks": stacks})
return uit
def _ip_mapping(net: dict, net_naam: str, gevraagd: dict,
eigen=()) -> tuple[dict, str, int]:
"""Controleer een mapping container → adres, en vul lege adressen aan.
@ -1479,6 +1519,7 @@ def api_store_preview():
metadata=meta,
fields=flds,
services=diensten,
depends_on=_afhankelijkheden_met_status(meta),
preview_files=preview_files,
# Backwards-compat voor de oude UI:
env_vars=[], images=[], ports=[],

View file

@ -71,9 +71,33 @@ def metadata(d: Path) -> dict:
"kind": t.get("kind", "compose"),
"format": "boilerplate",
"draft": bool(md.get("draft")),
"depends_on": _afhankelijkheden(md.get("depends_on")),
}
def _afhankelijkheden(waarde) -> list[dict]:
"""Welke andere apps heeft dit sjabloon nodig, en waarom?
Een `connect`-veld wijst naar een andere app en vult een adres in, maar zegt
niets over noodzaak. Zigbee2MQTT zonder MQTT-broker start prima en doet
niets en de reden staat nergens. Hiermee kan het sjabloon dat wél zeggen.
Zowel `["mosquitto"]` als `[{"app": "mosquitto", "reason": ""}]` mag; de
korte vorm is voor sjablonen uit externe repo's, die niets van deze sleutel
hoeven te weten.
"""
uit: list[dict] = []
for item in (waarde or []):
if isinstance(item, str):
item = {"app": item}
if not isinstance(item, dict):
continue
app = str(item.get("app") or "").strip()
if app:
uit.append({"app": app, "reason": str(item.get("reason") or "").strip()})
return uit
# ── Variable schema → Server Up form fields ──────────────────────────────────
def fields(d: Path) -> list[dict]:

View file

@ -1461,6 +1461,30 @@ tailwind.config = {
<template x-if="install.format === 'boilerplate' && !install.loading">
<div class="space-y-3">
<!-- Wat deze app nodig heeft en nog niet draait. Advies, geen
blokkade: je kunt de broker of Home Assistant net zo goed op een
andere machine hebben staan. -->
<div x-show="ontbrekendeAfhankelijkheden().length"
class="rounded-xl border border-wr/40 bg-wr/5 px-3.5 py-3 space-y-2">
<div class="flex items-center gap-2 text-xs font-bold text-wr">
<span class="mdi mdi-alert-outline"></span>
<span x-text="t('depends_on_missing')">Deze app heeft nog iets nodig</span>
</div>
<template x-for="d in ontbrekendeAfhankelijkheden()" :key="d.app">
<div class="flex items-start gap-2.5">
<span class="min-w-0 flex-1">
<span class="text-sm font-semibold text-tx dark:text-tx-dark" x-text="d.app"></span>
<span x-show="d.reason" class="block text-[11px] text-t2 dark:text-t2-dark leading-snug"
x-text="d.reason"></span>
</span>
<button type="button" class="btn-sm shrink-0" @click="installeerAfhankelijkheid(d)">
<span class="mdi mdi-download-outline"></span>
<span x-text="t('depends_on_install')">Eerst installeren</span>
</button>
</div>
</template>
</div>
<!-- Stapindicator; klikbaar om terug te springen -->
<div class="flex items-center gap-1 overflow-x-auto pb-1" x-show="!onePage">
<template x-for="(s, i) in wizardSteps()" :key="s.id">
@ -2815,6 +2839,20 @@ function app() {
this.install.connect_apps = d.connect_default !== false;
}
},
// Wat dit sjabloon nodig heeft en nog niet draait. De backend heeft de
// status er al bij gezet; hier blijft alleen het filteren over.
ontbrekendeAfhankelijkheden() {
return (this.install.depends_on || []).filter(d => !d.installed);
},
// Deze modal sluiten en die van de benodigde app openen. openInstall()
// vervangt install helemaal, dus er blijft niets van de vorige staan.
async installeerAfhankelijkheid(d) {
for (const [repoId, lijst] of Object.entries(this.storeStacks || {})) {
const st = (lijst || []).find(x => x.dir === d.app);
if (st) { await this.openInstall(st, repoId); return; }
}
this.toast(this.t('depends_on_not_in_store', {app: d.app}), 'er');
},
// Adressen van al geïnstalleerde apps die bij dit veld passen. Een veld met
// {"connect": {"app": "mosquitto", "scheme": "mqtt", "port": 1883}} levert
// mqtt://<containernaam>:1883 op voor elke geïnstalleerde Mosquitto.
@ -2954,7 +2992,7 @@ function app() {
envVars: [], images: [], envOverrides: {}, imageTags: {},
fields: [], values: {}, preview_files: {},
step: 0, tab: 'compose', editing: false, composeText: '', envText: '',
network: '', ip: '', ips: {}, services: []
network: '', ip: '', ips: {}, services: [], depends_on: []
};
this.modal = 'install';
this.fieldInfo = null;
@ -2990,6 +3028,7 @@ function app() {
}
this.install.values = init;
this.install.preview_files = d.preview_files || {};
this.install.depends_on = d.depends_on || [];
this.install.step = 0;
this.revealed = {};
this.ipCheck = {};

View file

@ -347,5 +347,8 @@
"ip_per_container": "Address per container",
"ip_per_container_hint": "Only give a container its own address if you want to reach it from outside. The rest keep their port on the host.",
"port_on_host": "port on the host",
"appdata_dir": "Appdata directory"
"appdata_dir": "Appdata directory",
"depends_on_missing": "This app still needs something",
"depends_on_install": "Install first",
"depends_on_not_in_store": "'{app}' is not in the store"
}

View file

@ -347,5 +347,8 @@
"ip_per_container": "Adres per container",
"ip_per_container_hint": "Geef alleen de containers een eigen adres die je van buitenaf wilt bereiken. De rest houdt zijn poort op de host.",
"port_on_host": "poort op de host",
"appdata_dir": "Appdata-map"
"appdata_dir": "Appdata-map",
"depends_on_missing": "Deze app heeft nog iets nodig",
"depends_on_install": "Eerst installeren",
"depends_on_not_in_store": "'{app}' staat niet in de store"
}

View file

@ -92,6 +92,23 @@ check('één groep: geen kop', c.stepGroups('basis')[0].showHeader === false);
c.install.fields.push(veld('c','basis',{group:'Tweede'}));
check('twee groepen: wel koppen', c.stepGroups('basis').every(g => g.showHeader));
// ── Afhankelijkheden ────────────────────────────────────────────────────────
// De melding hoort alleen te verschijnen voor wat er níét draait, en te
// verdwijnen zodra het er wel is.
c.install = {format:'boilerplate', loading:false, fields:[], values:{}, step:0,
depends_on: [
{app:'mosquitto', reason:'MQTT-broker', installed:false},
{app:'loki', reason:'logopslag', installed:true, stacks:['loki']},
]};
check('alleen het ontbrekende wordt gemeld', c.ontbrekendeAfhankelijkheden().length === 1);
check('en dat is mosquitto', c.ontbrekendeAfhankelijkheden()[0].app === 'mosquitto');
c.install.depends_on[0].installed = true;
check('niets meer te melden zodra alles draait',
c.ontbrekendeAfhankelijkheden().length === 0);
c.install.depends_on = [];
check('een app zonder afhankelijkheden meldt niets',
c.ontbrekendeAfhankelijkheden().length === 0);
// ── Sleutelformaten ─────────────────────────────────────────────────────────
// Een willekeurige string is niet genoeg: Laravel wil `base64:` ervoor, Homarr
// en LibreChat willen hexadecimaal van een vaste lengte. In het verkeerde

View file

@ -115,6 +115,80 @@ def test_koppelvelden_verwijzen_naar_bestaande_apps():
assert doel in apps, f"{d.name}/{f['name']} verwijst naar '{doel}'"
# ── Afhankelijkheden tussen apps ─────────────────────────────────────────────
# Een `connect`-veld wijst naar een andere app, maar zegt niet dát je die nodig
# hebt. Zigbee2MQTT zonder MQTT-broker start prima en doet niets; `depends_on`
# maakt dat zichtbaar vóór het installeren.
def _apps():
return sorted(d for d in (WORTEL / "apps").iterdir() if d.is_dir())
def _deps(d):
return bp.metadata(d).get("depends_on") or []
def test_afhankelijkheden_verwijzen_naar_bestaande_apps():
namen = {d.name for d in _apps()}
for d in _apps():
for dep in _deps(d):
assert dep["app"] in namen, f"{d.name} hangt af van '{dep['app']}'"
def test_elke_afhankelijkheid_heeft_een_reden():
"""Een waarschuwing zonder uitleg is een waarschuwing die je wegklikt."""
for d in _apps():
for dep in _deps(d):
assert dep["reason"], f"{d.name}{dep['app']}: geen reden opgegeven"
def test_geen_app_hangt_van_zichzelf_af():
for d in _apps():
assert d.name not in [x["app"] for x in _deps(d)], d.name
def test_geen_kringetjes():
"""A heeft B nodig en B heeft A nodig levert een melding op die je nooit
kunt wegwerken."""
graaf = {d.name: [x["app"] for x in _deps(d)] for d in _apps()}
bezocht: set[str] = set()
def loop(naam, pad):
if naam in pad:
raise AssertionError(f"kringetje: {''.join(pad + [naam])}")
if naam in bezocht:
return
bezocht.add(naam)
for volgende in graaf.get(naam, []):
loop(volgende, pad + [naam])
for naam in graaf:
loop(naam, [])
def test_korte_schrijfwijze_wordt_uitgeklapt(tmp_path):
"""Externe repo's hoeven niets van deze sleutel te weten; `["mosquitto"]`
moet net zo goed werken als de volledige vorm."""
import json as _json
d = tmp_path / "app"
(d / "files").mkdir(parents=True)
(d / "template.json").write_text(_json.dumps({
"kind": "compose",
"metadata": {"name": "x", "description": "y", "tags": ["z"],
"depends_on": ["mosquitto", {"app": "loki", "reason": "omdat"}]},
"variables": [],
}), encoding="utf-8")
(d / "files" / "compose.yaml").write_text("services: {}\n", encoding="utf-8")
assert bp.metadata(d)["depends_on"] == [
{"app": "mosquitto", "reason": ""},
{"app": "loki", "reason": "omdat"},
]
def test_zonder_depends_on_een_lege_lijst():
assert bp.metadata(WORTEL / "apps" / "vaultwarden")["depends_on"] == []
# ── Endpoint ─────────────────────────────────────────────────────────────────
def test_connectable_endpoint(client, env, monkeypatch):
@ -140,3 +214,37 @@ def test_connectable_endpoint(client, env, monkeypatch):
def test_connectable_zonder_stacks(client):
login(client)
assert client.get("/api/apps/connectable").get_json()["apps"] == []
def test_preview_meldt_wat_er_nog_ontbreekt(client, env, monkeypatch):
"""De installatiemodal moet vóór het installeren kunnen tonen wat er mist."""
import json as _json
from core import git
csrf = login(client)
bron = git.cache_dir("server-up") / "apps" / "z2m"
(bron / "files").mkdir(parents=True)
(bron / "template.json").write_text(_json.dumps({
"kind": "compose",
"metadata": {"name": "Zigbee2MQTT", "description": "test", "tags": ["mqtt"],
"depends_on": [{"app": "mosquitto", "reason": "broker nodig"},
{"app": "loki", "reason": "logs"}]},
"variables": [{"title": "Algemeen", "items": [
{"name": "service_name", "type": "str", "title": "Naam",
"default": "z2m", "required": True}]}],
}), encoding="utf-8")
(bron / "files" / "compose.yaml").write_text(
"services:\n << service_name >>:\n image: x\n", encoding="utf-8")
# Alleen Mosquitto draait al.
stack = env["lib"] / "mqtt"
stack.mkdir(parents=True)
(stack / "docker-compose.yml").write_text("services:\n a:\n image: x\n")
(stack / ".serverup.json").write_text(_json.dumps({"source": "mosquitto"}))
d = client.post("/api/store/preview", json={"stack": "z2m", "repo_id": "server-up"},
headers={"X-CSRF-Token": csrf}).get_json()
deps = {x["app"]: x for x in d["depends_on"]}
assert deps["mosquitto"]["installed"] is True
assert deps["mosquitto"]["stacks"] == ["mqtt"]
assert deps["loki"]["installed"] is False
assert deps["loki"]["reason"] == "logs"

View file

@ -307,7 +307,14 @@ SPEC = [
image="mrusse08/soularr:latest",
omschrijving="Laat Lidarr zoeken via Soulseek (slskd) voor albums die op de gewone indexers niet te vinden zijn.",
tags=["muziek", "downloaden"], cats=["downloaden", "media"],
poorten=[], data=True, config_pad="/data", sectie="Muziek"),
poorten=[], data=True, config_pad="/data", sectie="Muziek",
depends_on=[
{"app": "lidarr",
"reason": "Soularr pakt de gaten op die Lidarr aangeeft; zonder "
"Lidarr weet het niet waarnaar het moet zoeken."},
{"app": "slskd",
"reason": "Het zoeken zelf gebeurt via Soulseek, waarvoor slskd "
"de verbinding maakt."}]),
dict(dir="neutarr", naam="NeutArr", icon="huntarr",
image="iampuid0/neutarr:latest",
omschrijving="Zoekt gericht naar ontbrekende afleveringen en naar wat beter kan. Opvolger van Huntarr, dat in februari 2026 offline ging na een reeks lekken; dit is de fork van de laatste schone versie.",
@ -586,6 +593,9 @@ def schrijf_los(app):
"icon": {"provider": "selfhst", "id": app["icon"]},
"version": {"name": "latest"},
"categories": app["cats"],
# Wat deze app nodig heeft om iets te doen; de installatiemodal
# waarschuwt als het er nog niet is.
**({"depends_on": app["depends_on"]} if app.get("depends_on") else {}),
},
"variables": _velden(app),
}