Compare commits
34 Commits
agent/clau
...
a5acad4ca4
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a5acad4ca4 | ||
|
|
9506f1b0ad | ||
|
|
f7c00ca818 | ||
|
|
8443ffbef5 | ||
|
|
7d6caabe8c | ||
|
|
d7dfe1061b | ||
|
|
b517d0112e | ||
|
|
8097dac558 | ||
|
|
6ae37633d7 | ||
|
|
3eab28771b | ||
|
|
8fcfab1cb3 | ||
|
|
54e3e1ce7a | ||
|
|
9ab305855f | ||
|
|
585a723154 | ||
|
|
aeba8de6f4 | ||
|
|
dccdedf568 | ||
|
|
af5462eb0f | ||
|
|
b8a04ed953 | ||
|
|
3911ef424e | ||
|
|
da974f6a77 | ||
|
|
b7a4751520 | ||
|
|
bbf68e10ed | ||
|
|
9b20380c63 | ||
|
|
1ab1799276 | ||
|
|
49dd9f0e4e | ||
|
|
b453c60c33 | ||
|
|
aceeaf36b4 | ||
|
|
9faae5cc90 | ||
|
|
d3d3476986 | ||
|
|
c0aa5af0ce | ||
|
|
87c9b60be0 | ||
|
|
82ac7957c8 | ||
|
|
f0d20d77fa | ||
|
|
988cecb122 |
BIN
.github/stargazer_map.png
vendored
|
Before Width: | Height: | Size: 331 KiB After Width: | Height: | Size: 60 KiB |
BIN
.github/stats.png
vendored
|
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 1.8 KiB |
BIN
.github/stats_addons.png
vendored
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 4.1 KiB |
2
.github/workflows/daily_stale.yml
vendored
@@ -20,7 +20,7 @@ jobs:
|
||||
pull-requests: write
|
||||
|
||||
steps:
|
||||
- uses: actions/stale@v10
|
||||
- uses: actions/stale@v11
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
stale-issue-message: 'This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.'
|
||||
|
||||
2
.github/workflows/onpr_check-pr.yaml
vendored
@@ -91,7 +91,7 @@ jobs:
|
||||
uses: actions/checkout@v7.0.1
|
||||
|
||||
- name: 🔎 Run Home Assistant Add-on Lint
|
||||
uses: frenck/action-addon-linter@v2
|
||||
uses: frenck/action-addon-linter@f6bef06a4cee6c67924b0a70be643aacb8500a43
|
||||
with:
|
||||
path: "./${{ matrix.addon }}"
|
||||
|
||||
|
||||
2
.github/workflows/onpush_builder.yaml
vendored
@@ -114,7 +114,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v7.0.1
|
||||
- name: Run Home Assistant Add-on Lint
|
||||
uses: frenck/action-addon-linter@v2
|
||||
uses: frenck/action-addon-linter@f6bef06a4cee6c67924b0a70be643aacb8500a43
|
||||
with:
|
||||
path: "./${{ matrix.addon }}"
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ slug="${slug#*_}"
|
||||
if [ ! -f /config/configuration.yaml ] && [ ! -f /config/configuration.json ]; then
|
||||
# New config location
|
||||
CONFIGLOCATION="/config"
|
||||
CONFIGFILEBROWSER="/addon_configs/${HOSTNAME/-/_}/config.yaml"
|
||||
CONFIGFILEBROWSER="/app_configs/${HOSTNAME/-/_}/config.yaml"
|
||||
else
|
||||
# Legacy config location
|
||||
CONFIGLOCATION="/config/addons_config/${slug}"
|
||||
|
||||
@@ -174,7 +174,7 @@ slug="${slug#*_}"
|
||||
# Check type of config folder
|
||||
if [ ! -f /config/configuration.yaml ] && [ ! -f /config/configuration.json ]; then
|
||||
CONFIGLOCATION="/config"
|
||||
CONFIGFILEBROWSER="/addon_configs/${HOSTNAME/-/_}/$slug.sh"
|
||||
CONFIGFILEBROWSER="/app_configs/${HOSTNAME/-/_}/$slug.sh"
|
||||
else
|
||||
CONFIGLOCATION="/config/addons_autoscripts"
|
||||
CONFIGFILEBROWSER="/homeassistant/addons_autoscripts/$slug.sh"
|
||||
|
||||
@@ -70,7 +70,7 @@ version: "X.Y.Z" # upstream version (format varies; see Versioning sect
|
||||
ingress: true/false
|
||||
ingress_port: 8000
|
||||
map:
|
||||
- addon_config:rw # /addon_configs/<hostname>/
|
||||
- app_config:rw # /app_configs/<hostname>/
|
||||
- share:rw
|
||||
- media:rw
|
||||
- ssl
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
- Migrate legacy add-on configuration map names to current app configuration terminology.
|
||||
## 2026.08 (2026-08-01)
|
||||
|
||||
- Addon versions written in config.yaml now always comply with Home Assistant versioning: an upstream tag Home Assistant cannot order (`version-bf9e0b4f`, `ubuntu-2026-06-01`, ...) or would sort as older (`1.2.3-2`, `1.2.3+4`) no longer lands in config.yaml. The addon number is incremented instead, while the raw upstream tag stays in updater.json so the same release is never published twice
|
||||
@@ -31,7 +32,7 @@
|
||||
|
||||
## 3.19
|
||||
|
||||
- New HA config logic implemented. Files are now located in the addon config file, that can be accessed from the addon_configs folder from my filebrowser or cloudcommander addons. Migration of data, custom configs, and custom scripts should be automatic. Please be sure to update all your links however ! For more information, see here : https://developers.home-assistant.io/blog/2023/11/06/public-addon-config/
|
||||
- New HA config logic implemented. Files are now located in the addon config file, that can be accessed from the app_configs folder from my filebrowser or cloudcommander addons. Migration of data, custom configs, and custom scripts should be automatic. Please be sure to update all your links however ! For more information, see here : https://developers.home-assistant.io/blog/2023/11/06/public-addon-config/
|
||||
- Feat : if there is no releases in a github repo, check if there is a valid package and get the highest tag https://github.com/alexbelgium/hassio-addons/issues/1168
|
||||
- Feat : github_exclude applies to dockerhub
|
||||
|
||||
|
||||
@@ -115,7 +115,7 @@ verbose: "false"
|
||||
|
||||
### Custom Scripts and Environment Variables
|
||||
|
||||
This addon supports custom scripts and environment variables through the `addon_config` mapping:
|
||||
This addon supports custom scripts and environment variables through the `app_config` mapping:
|
||||
|
||||
- **Custom scripts**: See [Running Custom Scripts in Addons](https://github.com/alexbelgium/hassio-addons/wiki/Running-custom-scripts-in-Addons)
|
||||
- **Environment variables**: Use the add-on `env_vars` option and see [Add Environment Variables to your Addon](https://github.com/alexbelgium/hassio-addons/wiki/Add-Environment-variables-to-your-Addon) for details.
|
||||
|
||||
@@ -8,7 +8,7 @@ environment:
|
||||
image: ghcr.io/alexbelgium/addons_updater-{arch}
|
||||
init: false
|
||||
map:
|
||||
- addon_config:rw
|
||||
- app_config:rw
|
||||
name: Repository Updater
|
||||
options:
|
||||
date_iso8601: true
|
||||
|
||||
|
Before Width: | Height: | Size: 3.4 KiB After Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 1.2 KiB |
@@ -1,3 +1,4 @@
|
||||
- Migrate legacy add-on configuration map names to current app configuration terminology.
|
||||
|
||||
## 2.0.3 (2026-08-01)
|
||||
- Update to latest version from lklynet/aurral (changelog : https://github.com/lklynet/aurral/releases)
|
||||
|
||||
@@ -21,7 +21,7 @@ ports_description:
|
||||
webui: http://[HOST]:[PORT:3001]
|
||||
|
||||
map:
|
||||
- addon_config:rw
|
||||
- app_config:rw
|
||||
- share:rw
|
||||
- media:rw
|
||||
|
||||
|
||||
BIN
aurral/stats.png
|
Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 1.4 KiB |
@@ -1,3 +1,4 @@
|
||||
- Migrate legacy add-on configuration map names to current app configuration terminology.
|
||||
- The Home Assistant project has deprecated support for the armv7, armhf and i386 architectures. Support wil be fully dropped in the upcoming Home Assistant 2025.12 release
|
||||
|
||||
## 0.10.1-hafix4 (2025-11-18)
|
||||
@@ -21,7 +22,7 @@
|
||||
## 0.9.5 (2024-04-27)
|
||||
- Update to latest version from ckulka/baikal-docker (changelog : https://github.com/ckulka/baikal-docker/releases)
|
||||
## 0.9.4-3 (2024-04-26)
|
||||
- ⚠ MAJOR CHANGE : switch to the new config logic from homeassistant. Your configuration files will have migrated from /config/hassio_addons/baikal to a folder only accessible from my Filebrowser addon called /addon_configs/something-baikal. This avoids the addon to mess with your homeassistant configuration folder, and allows to backup the options. Migration of data, custom configs, and custom scripts should be automatic. Please be sure to update all your links however ! For more information, see here : https://developers.home-assistant.io/blog/2023/11/06/public-addon-config/
|
||||
- ⚠ MAJOR CHANGE : switch to the new config logic from homeassistant. Your configuration files will have migrated from /config/hassio_addons/baikal to a folder only accessible from my Filebrowser addon called /app_configs/something-baikal. This avoids the addon to mess with your homeassistant configuration folder, and allows to backup the options. Migration of data, custom configs, and custom scripts should be automatic. Please be sure to update all your links however ! For more information, see here : https://developers.home-assistant.io/blog/2023/11/06/public-addon-config/
|
||||
|
||||
## 0.9.4-2 (2024-01-14)
|
||||
|
||||
|
||||
@@ -48,7 +48,7 @@ Configurations can be done through the app webUI, except for the following optio
|
||||
|
||||
### Custom Scripts and Environment Variables
|
||||
|
||||
This addon supports custom scripts and environment variables through the `addon_config` mapping:
|
||||
This addon supports custom scripts and environment variables through the `app_config` mapping:
|
||||
|
||||
- **Custom scripts**: See [Running Custom Scripts in Addons](https://github.com/alexbelgium/hassio-addons/wiki/Running-custom-scripts-in-Addons)
|
||||
- **Environment variables**: See [Add Environment Variables to your Addon](https://github.com/alexbelgium/hassio-addons/wiki/Add-Environment-variables-to-your-Addon)
|
||||
|
||||
@@ -66,7 +66,7 @@ devices:
|
||||
- /dev/nvme2
|
||||
image: ghcr.io/alexbelgium/baikal-{arch}
|
||||
map:
|
||||
- addon_config:rw
|
||||
- app_config:rw
|
||||
- homeassistant_config:rw
|
||||
- share:rw
|
||||
- ssl:ro
|
||||
|
||||
BIN
baikal/stats.png
|
Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 1.6 KiB |
@@ -1,3 +1,4 @@
|
||||
- Migrate legacy add-on configuration map names to current app configuration terminology.
|
||||
## 2026.02.28 (28-02-2026)
|
||||
- Minor bugs fixed
|
||||
## 2026.02.25-2 (25-02-2026)
|
||||
|
||||
@@ -86,7 +86,7 @@ This addon supports mounting both local drives and remote SMB shares:
|
||||
|
||||
### Custom Scripts and Environment Variables
|
||||
|
||||
This addon supports custom scripts and environment variables through the `addon_config` mapping:
|
||||
This addon supports custom scripts and environment variables through the `app_config` mapping:
|
||||
|
||||
- **Custom scripts**: See [Running Custom Scripts in Addons](https://github.com/alexbelgium/hassio-addons/wiki/Running-custom-scripts-in-Addons)
|
||||
- **env_vars option**: Use the add-on `env_vars` option to pass extra environment variables (uppercase or lowercase names). See https://github.com/alexbelgium/hassio-addons/wiki/Add-Environment-variables-to-your-Addon-2 for details.
|
||||
|
||||
@@ -64,7 +64,7 @@ ingress: true
|
||||
ingress_stream: true
|
||||
init: false
|
||||
map:
|
||||
- addon_config:rw
|
||||
- app_config:rw
|
||||
- media:rw
|
||||
- share:rw
|
||||
- ssl
|
||||
|
||||
|
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 1.5 KiB |
@@ -1,3 +1,4 @@
|
||||
- Migrate legacy add-on configuration map names to current app configuration terminology.
|
||||
|
||||
## 1.6.0.2 (2026-07-27)
|
||||
|
||||
@@ -26,7 +27,7 @@
|
||||
- Update to latest version from linuxserver/docker-bazarr (changelog : https://github.com/linuxserver/docker-bazarr/releases)
|
||||
|
||||
## 1.5.4-1 (2026-01-08)
|
||||
- ⚠ MAJOR CHANGE : switch to the new config logic from homeassistant. Your configuration files will have migrated from /config/addons_config/bazarr to a folder only accessible from my Filebrowser addon called /addon_configs/xxx-bazarr. This avoids the addon to mess with your homeassistant configuration folder, and allows to backup the options. Migration of data should be automatic. Please be sure to update all your links however ! For more information, see here : https://developers.home-assistant.io/blog/2023/11/06/public-addon-config/
|
||||
- ⚠ MAJOR CHANGE : switch to the new config logic from homeassistant. Your configuration files will have migrated from /config/addons_config/bazarr to a folder only accessible from my Filebrowser addon called /app_configs/xxx-bazarr. This avoids the addon to mess with your homeassistant configuration folder, and allows to backup the options. Migration of data should be automatic. Please be sure to update all your links however ! For more information, see here : https://developers.home-assistant.io/blog/2023/11/06/public-addon-config/
|
||||
|
||||
## 1.5.4 (2026-01-08)
|
||||
- Update to latest version from linuxserver/docker-bazarr (changelog : https://github.com/linuxserver/docker-bazarr/releases)
|
||||
|
||||
@@ -76,7 +76,7 @@ ingress: true
|
||||
ingress_entry: bazarr
|
||||
init: false
|
||||
map:
|
||||
- addon_config:rw
|
||||
- app_config:rw
|
||||
- homeassistant_config:rw
|
||||
- share:rw
|
||||
- media:rw
|
||||
|
||||
@@ -23,7 +23,7 @@ fi
|
||||
slug=bazarr
|
||||
|
||||
if [ -d "/homeassistant/addons_config/$slug" ]; then
|
||||
echo "Migrating /homeassistant/addons_config/$slug to /addon_configs/xxx-$slug"
|
||||
echo "Migrating /homeassistant/addons_config/$slug to /app_configs/xxx-$slug"
|
||||
cp -rnf /homeassistant/addons_config/"$slug"/. /config/ || true
|
||||
mv /homeassistant/addons_config/"$slug" /homeassistant/addons_config/"$slug"_migrated
|
||||
fi
|
||||
|
||||
BIN
bazarr/stats.png
|
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 1.7 KiB |
@@ -1,3 +1,4 @@
|
||||
- Migrate legacy add-on configuration map names to current app configuration terminology.
|
||||
## 20260729.1 (2026-08-01)
|
||||
|
||||
- Version renamed from `source-20260729.1`, which Home Assistant could not order and therefore could not reliably offer as an update: every number of the previous version is kept, as a section of its own. The addon itself and the upstream version it tracks are unchanged
|
||||
|
||||
@@ -25,7 +25,7 @@ MQTT password : Ri5ahV1aipeiw0aelerooteixai5ohtoeNg6oo3mo0thi5te0phiezuge4Phoore
|
||||
MQTT broker : tcp://core-mosquitto:1883
|
||||
---
|
||||
|
||||
Edit this section of config.yaml found in addon_configs/db21ed7f_birdnet-go/:
|
||||
Edit this section of config.yaml found in app_configs/db21ed7f_birdnet-go/:
|
||||
mqtt:
|
||||
enabled: true # true to enable MQTT
|
||||
broker: tcp://core-mosquitto:1883 # MQTT (tcp://host:port)
|
||||
|
||||
@@ -82,7 +82,7 @@ This addon supports mounting both local drives and remote SMB shares:
|
||||
|
||||
### Custom Scripts and Environment Variables
|
||||
|
||||
This addon supports custom scripts and environment variables through the `addon_config` mapping:
|
||||
This addon supports custom scripts and environment variables through the `app_config` mapping:
|
||||
|
||||
- **Custom scripts**: See [Running Custom Scripts in Addons](https://github.com/alexbelgium/hassio-addons/wiki/Running-custom-scripts-in-Addons)
|
||||
- **env_vars option**: Use the add-on `env_vars` option to pass extra environment variables (uppercase or lowercase names). See https://github.com/alexbelgium/hassio-addons/wiki/Add-Environment-variables-to-your-Addon-2 for details.
|
||||
|
||||
@@ -80,7 +80,7 @@ ingress_entry: "ui/dashboard"
|
||||
ingress_stream: true
|
||||
init: false
|
||||
map:
|
||||
- addon_config:rw
|
||||
- app_config:rw
|
||||
- media:rw
|
||||
- share:rw
|
||||
name: Birdnet-go (customized and built from source)
|
||||
|
||||
|
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 1.3 KiB |
@@ -1,3 +1,4 @@
|
||||
- Migrate legacy add-on configuration map names to current app configuration terminology.
|
||||
|
||||
## 20260716 (2026-07-16)
|
||||
- Update to latest version from tphakala/birdnet-go (changelog : https://github.com/tphakala/birdnet-go/releases)
|
||||
|
||||
@@ -25,7 +25,7 @@ MQTT password : Ri5ahV1aipeiw0aelerooteixai5ohtoeNg6oo3mo0thi5te0phiezuge4Phoore
|
||||
MQTT broker : tcp://core-mosquitto:1883
|
||||
---
|
||||
|
||||
Edit this section of config.yaml found in addon_configs/db21ed7f_birdnet-go/:
|
||||
Edit this section of config.yaml found in app_configs/db21ed7f_birdnet-go/:
|
||||
mqtt:
|
||||
enabled: true # true to enable MQTT
|
||||
broker: tcp://core-mosquitto:1883 # MQTT (tcp://host:port)
|
||||
|
||||
@@ -79,7 +79,7 @@ This addon supports mounting both local drives and remote SMB shares:
|
||||
|
||||
### Custom Scripts and Environment Variables
|
||||
|
||||
This addon supports custom scripts and environment variables through the `addon_config` mapping:
|
||||
This addon supports custom scripts and environment variables through the `app_config` mapping:
|
||||
|
||||
- **Custom scripts**: See [Running Custom Scripts in Addons](https://github.com/alexbelgium/hassio-addons/wiki/Running-custom-scripts-in-Addons)
|
||||
- **env_vars option**: Use the add-on `env_vars` option to pass extra environment variables (uppercase or lowercase names). See https://github.com/alexbelgium/hassio-addons/wiki/Add-Environment-variables-to-your-Addon-2 for details.
|
||||
|
||||
@@ -81,7 +81,7 @@ ingress_entry: "ui/dashboard"
|
||||
ingress_stream: true
|
||||
init: false
|
||||
map:
|
||||
- addon_config:rw
|
||||
- app_config:rw
|
||||
- media:rw
|
||||
- share:rw
|
||||
name: Birdnet-go
|
||||
|
||||
|
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 1.5 KiB |
@@ -1,3 +1,4 @@
|
||||
- Migrate legacy add-on configuration map names to current app configuration terminology.
|
||||
## 2026.07.10-2 (10-07-2026)
|
||||
- Minor bugs fixed
|
||||
## 2026.07.10 (10-07-2026)
|
||||
|
||||
@@ -93,7 +93,7 @@ This addon supports mounting both local drives and remote SMB shares:
|
||||
|
||||
### Custom Scripts and Environment Variables
|
||||
|
||||
This addon supports custom scripts and environment variables through the `addon_config` mapping:
|
||||
This addon supports custom scripts and environment variables through the `app_config` mapping:
|
||||
|
||||
- **Custom scripts**: See [Running Custom Scripts in Addons](https://github.com/alexbelgium/hassio-addons/wiki/Running-custom-scripts-in-Addons)
|
||||
- **env_vars option**: Use the add-on `env_vars` option to pass extra environment variables (uppercase or lowercase names). See https://github.com/alexbelgium/hassio-addons/wiki/Add-Environment-variables-to-your-Addon-2 for details.
|
||||
|
||||
@@ -61,7 +61,7 @@ image: ghcr.io/alexbelgium/birdnet-pi-zach-{arch}
|
||||
ingress: true
|
||||
init: false
|
||||
map:
|
||||
- addon_config:rw
|
||||
- app_config:rw
|
||||
- media:rw
|
||||
- share:rw
|
||||
- ssl
|
||||
|
||||
|
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 1.3 KiB |
@@ -1,3 +1,8 @@
|
||||
- Migrate legacy add-on configuration map names to current app configuration terminology.
|
||||
## 2026.08.02 (02-08-2026)
|
||||
- Fix: ingress returned "502 Bad Gateway" because Caddy never listened on :8082. `91-nginx_ingress.sh` hooked the ingress site into `update_caddyfile.sh` with a sed anchored on `sudo caddy fmt --overwrite`, but 2026.07.10-1 strips `sudo` from every BirdNET-Pi script at build time, so the anchor stopped matching. `update_caddyfile.sh` then rewrote the Caddyfile from scratch just before Caddy started, dropping the ingress site
|
||||
- Fix: `caddy_ingress.sh` no longer appends a second `:8082` block when it runs twice (a duplicate site address makes Caddy refuse to start)
|
||||
- Fix: `02-caddy.sh` re-adds the ingress site if it is missing from the Caddyfile just before starting Caddy
|
||||
## 2026.07.22 (22-07-2026)
|
||||
- Fix: health-check the WebUI port (8081) instead of port 80, so the standalone Docker container no longer reports "unhealthy" when ssl=false
|
||||
- Fix: health-check now probes https when ssl is enabled, and no longer silently reports "healthy" regardless of the actual result (the previous check's `&>` redirection is a bash-ism that dash, the image's /bin/sh, parses as background + no-op, discarding curl's exit status)
|
||||
@@ -148,7 +153,7 @@
|
||||
- [ALL] Fix non-avx2 cpu support
|
||||
|
||||
## 2025.02.23 (2025-02-16)
|
||||
- WARNING 2025.02.14/16 was buggy. If you installed it you need to restore a backup or delete manually your /addon_configs/xxx-birdnet-pi/birdnet.conf file and recreate it
|
||||
- WARNING 2025.02.14/16 was buggy. If you installed it you need to restore a backup or delete manually your /app_configs/xxx-birdnet-pi/birdnet.conf file and recreate it
|
||||
- Allow usage as a standalone container (thanks @gotschi) https://github.com/mcguirepr89/BirdNET-Pi/issues/211#issuecomment-2650095952
|
||||
- Corrected a bug preventing to create db
|
||||
- Corrected a bug to ensure the the most up-to-date birdnet.conf on fresh start
|
||||
|
||||
@@ -93,7 +93,7 @@ This addon supports mounting both local drives and remote SMB shares:
|
||||
|
||||
### Custom Scripts and Environment Variables
|
||||
|
||||
This addon supports custom scripts and environment variables through the `addon_config` mapping:
|
||||
This addon supports custom scripts and environment variables through the `app_config` mapping:
|
||||
|
||||
- **Custom scripts**: See [Running Custom Scripts in Addons](https://github.com/alexbelgium/hassio-addons/wiki/Running-custom-scripts-in-Addons)
|
||||
- **env_vars option**: Use the add-on `env_vars` option to pass extra environment variables (uppercase or lowercase names). See https://github.com/alexbelgium/hassio-addons/wiki/Add-Environment-variables-to-your-Addon-2 for details.
|
||||
|
||||
@@ -61,7 +61,7 @@ image: ghcr.io/alexbelgium/birdnet-pi-{arch}
|
||||
ingress: true
|
||||
init: false
|
||||
map:
|
||||
- addon_config:rw
|
||||
- app_config:rw
|
||||
- media:rw
|
||||
- share:rw
|
||||
- ssl
|
||||
@@ -116,5 +116,5 @@ tmpfs: true
|
||||
udev: true
|
||||
url: https://github.com/alexbelgium/hassio-addons/tree/master/birdnet-pi
|
||||
usb: true
|
||||
version: 2026.07.22
|
||||
version: 2026.08.02
|
||||
video: true
|
||||
|
||||
@@ -25,5 +25,24 @@ export TZ="${TZ_VALUE:-Etc/UTC}"
|
||||
# Update caddyfile with password
|
||||
"$HOME"/BirdNET-Pi/scripts/update_caddyfile.sh &> /dev/null || true
|
||||
|
||||
# update_caddyfile.sh rewrites the Caddyfile from scratch. 91-nginx_ingress.sh
|
||||
# hooks the ingress site back into it, but if that hook ever fails to apply,
|
||||
# caddy would start without a :8082 listener and ingress would answer 502.
|
||||
# 91-nginx_ingress.sh writes /ingress_url when ingress is on and removes it when
|
||||
# it is off, so this is a no-op in standalone mode.
|
||||
# Require the Caddyfile to exist: if it is missing something went badly wrong
|
||||
# earlier, and caddy failing on a missing config is easier to diagnose than an
|
||||
# ingress-only Caddyfile conjured up here.
|
||||
if [[ -f /ingress_url ]] && [[ -f /etc/caddy/Caddyfile ]] \
|
||||
&& ! grep -qE '^[[:space:]]*:8082[[:space:]]*\{' /etc/caddy/Caddyfile 2> /dev/null; then
|
||||
echo "Ingress site missing from the Caddyfile, re-adding it"
|
||||
if ! /helpers/caddy_ingress.sh; then
|
||||
# Start caddy anyway: ingress stays broken, but direct access on 8081
|
||||
# keeps working. Exiting here would only make s6 restart this service in
|
||||
# a loop and take the WebUI down completely.
|
||||
echo "Failed to re-add the ingress site, the ingress panel will return 502" >&2
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "Starting service: caddy"
|
||||
exec /usr/bin/caddy run --config /etc/caddy/Caddyfile
|
||||
|
||||
@@ -26,6 +26,10 @@ ingress_entry=$(bashio::addon.ingress_entry)
|
||||
if [[ "$ingress_entry" != "/api"* ]]; then
|
||||
bashio::log.info "Ingress entry is not set, exiting configuration."
|
||||
sed -i "1a sleep infinity" /custom-services.d/02-nginx.sh
|
||||
# This script re-runs from /etc/scripts-init on an add-on restart, so drop
|
||||
# any marker left by an earlier run: it is what 02-caddy.sh reads to decide
|
||||
# whether the ingress site belongs in the Caddyfile.
|
||||
rm -f /ingress_url
|
||||
exit 0
|
||||
fi
|
||||
|
||||
@@ -68,9 +72,23 @@ sed -i "s|localhost|localhost:8082|g" "$HOME/BirdNET-Pi/scripts/utils/notificati
|
||||
|
||||
# Update the Caddyfile if update script exists
|
||||
caddy_update_script="$HOME/BirdNET-Pi/scripts/update_caddyfile.sh"
|
||||
if [ -f "$caddy_update_script" ]; then
|
||||
sed -i "/sudo caddy fmt --overwrite/i /helpers/caddy_ingress.sh" "$caddy_update_script"
|
||||
else
|
||||
if [ ! -f "$caddy_update_script" ]; then
|
||||
bashio::log.error "Caddy update script not found: $caddy_update_script"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# update_caddyfile.sh rewrites /etc/caddy/Caddyfile from scratch, which drops
|
||||
# the ingress site added just above. 02-caddy.sh runs it right before starting
|
||||
# caddy, so the hook below has to re-add the site from inside that script, just
|
||||
# before it formats and reloads the config.
|
||||
# The anchor must not require "sudo": the Dockerfile strips it from every
|
||||
# BirdNET-Pi script at build time, so the shipped line is "caddy fmt --overwrite".
|
||||
if ! grep -qF "/helpers/caddy_ingress.sh" "$caddy_update_script"; then
|
||||
sed -i -E "/^[[:space:]]*(sudo[[:space:]]+)?caddy[[:space:]]+fmt[[:space:]]+--overwrite/i /helpers/caddy_ingress.sh" "$caddy_update_script"
|
||||
fi
|
||||
|
||||
# sed is silent when the anchor is missing; make sure the hook is really there
|
||||
if ! grep -qF "/helpers/caddy_ingress.sh" "$caddy_update_script"; then
|
||||
bashio::log.warning "Could not anchor the ingress site in $caddy_update_script, appending it instead"
|
||||
printf '\n/helpers/caddy_ingress.sh\n' >> "$caddy_update_script"
|
||||
fi
|
||||
|
||||
@@ -6,6 +6,13 @@ set +u
|
||||
# shellcheck disable=SC1091
|
||||
source /etc/birdnet/birdnet.conf
|
||||
|
||||
# Nothing to do if the ingress site is already there. This script runs both from
|
||||
# cont-init and from update_caddyfile.sh, and a duplicate ":8082" site address
|
||||
# makes caddy refuse to start.
|
||||
if grep -qE '^[[:space:]]*:8082[[:space:]]*\{' /etc/caddy/Caddyfile 2> /dev/null; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Create ingress configuration for Caddyfile
|
||||
cat << EOF >> /etc/caddy/Caddyfile
|
||||
:8082 {
|
||||
|
||||
|
Before Width: | Height: | Size: 4.5 KiB After Width: | Height: | Size: 1.8 KiB |
@@ -1,3 +1,4 @@
|
||||
- Migrate legacy add-on configuration map names to current app configuration terminology.
|
||||
|
||||
## 0.8.5 (2026-08-01)
|
||||
- Update to latest version from Suncuss/BirdNET-PiPy (changelog : https://github.com/Suncuss/BirdNET-PiPy/releases)
|
||||
|
||||
@@ -47,7 +47,7 @@ This add-on supports mounting both local drives and remote SMB shares:
|
||||
|
||||
### Custom Scripts and Environment Variables
|
||||
|
||||
This add-on supports custom scripts and environment variables through the `addon_config` mapping:
|
||||
This add-on supports custom scripts and environment variables through the `app_config` mapping:
|
||||
|
||||
- **Custom scripts**: See [Running Custom Scripts in Addons](https://github.com/alexbelgium/hassio-addons/wiki/Running-custom-scripts-in-Addons)
|
||||
- **env_vars option**: Use the add-on `env_vars` option to pass extra environment variables (uppercase or lowercase names). See https://github.com/alexbelgium/hassio-addons/wiki/Add-Environment-variables-to-your-Addon-2 for details.
|
||||
|
||||
@@ -67,7 +67,7 @@ ingress: true
|
||||
ingress_stream: true
|
||||
init: false
|
||||
map:
|
||||
- addon_config:rw
|
||||
- app_config:rw
|
||||
- ssl:rw
|
||||
- share:rw
|
||||
name: BirdNET-PiPy
|
||||
|
||||
|
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 1.5 KiB |
@@ -1,3 +1,4 @@
|
||||
- Migrate legacy add-on configuration map names to current app configuration terminology.
|
||||
|
||||
## 1.93.129-ls119 (2026-08-01)
|
||||
- Update to latest version from linuxserver/docker-brave (changelog : https://github.com/linuxserver/docker-brave/releases)
|
||||
|
||||
@@ -26,7 +26,7 @@ image: ghcr.io/alexbelgium/brave-{arch}
|
||||
ingress: true
|
||||
init: false
|
||||
map:
|
||||
- addon_config:rw
|
||||
- app_config:rw
|
||||
- media:rw
|
||||
- share:rw
|
||||
- ssl
|
||||
|
||||
|
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 1.2 KiB |
@@ -1,3 +1,4 @@
|
||||
- Migrate legacy add-on configuration map names to current app configuration terminology.
|
||||
|
||||
## 2026.08.01.1 (2026-08-01)
|
||||
- Update to latest version from linuxserver/docker-chromium (changelog : https://github.com/linuxserver/docker-chromium/releases)
|
||||
|
||||
@@ -26,7 +26,7 @@ image: ghcr.io/alexbelgium/chromium-{arch}
|
||||
ingress: true
|
||||
init: false
|
||||
map:
|
||||
- addon_config:rw
|
||||
- app_config:rw
|
||||
- media:rw
|
||||
- share:rw
|
||||
- ssl
|
||||
|
||||
|
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 1.3 KiB |
@@ -1,3 +1,4 @@
|
||||
- Migrate legacy add-on configuration map names to current app configuration terminology.
|
||||
|
||||
## 2.55.2 (2026-08-01)
|
||||
- Update to latest version from browserless/chrome (changelog : https://github.com/browserless/chrome/releases)
|
||||
|
||||
@@ -50,7 +50,7 @@ TIMEOUT: 60000
|
||||
|
||||
### Custom Scripts and Environment Variables
|
||||
|
||||
This addon supports custom scripts and environment variables through the `addon_config` mapping:
|
||||
This addon supports custom scripts and environment variables through the `app_config` mapping:
|
||||
|
||||
- **Custom scripts**: See [Running Custom Scripts in Addons](https://github.com/alexbelgium/hassio-addons/wiki/Running-custom-scripts-in-Addons)
|
||||
- **env_vars option**: Use the add-on `env_vars` option to pass extra environment variables (uppercase or lowercase names). See https://github.com/alexbelgium/hassio-addons/wiki/Add-Environment-variables-to-your-Addon-2 for details.
|
||||
|
||||
@@ -69,7 +69,7 @@ environment:
|
||||
image: ghcr.io/alexbelgium/browserless_chrome-{arch}
|
||||
init: false
|
||||
map:
|
||||
- addon_config:rw
|
||||
- app_config:rw
|
||||
name: Browserless Chromium
|
||||
options:
|
||||
env_vars: []
|
||||
|
||||
|
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 1.5 KiB |
@@ -1,3 +1,4 @@
|
||||
- Migrate legacy add-on configuration map names to current app configuration terminology.
|
||||
|
||||
## 9.12.0 (2026-08-01)
|
||||
- Update to latest version from linuxserver/docker-calibre (changelog : https://github.com/linuxserver/docker-calibre/releases)
|
||||
@@ -217,7 +218,7 @@
|
||||
|
||||
- Update to latest version from linuxserver/docker-calibre
|
||||
- BREAKING CHANGE : please relink your library according to https://github.com/alexbelgium/hassio-addons/issues/1154#issuecomment-1879182729
|
||||
- ⚠ MAJOR CHANGE : switch to the new config logic from homeassistant. Your configuration files will have migrated from /config/hassio_addons/calibre to a folder only accessible from my Filebrowser addon called /addon_configs/something-calibre. This avoids the addon to mess with your homeassistant configuration folder, and allows to backup the options. Migration of data, custom configs, and custom scripts should be automatic. Please be sure to update all your links however ! For more information, see here : https://developers.home-assistant.io/blog/2023/11/06/public-addon-config/ https://github.com/alexbelgium/hassio-addons/issues/1154
|
||||
- ⚠ MAJOR CHANGE : switch to the new config logic from homeassistant. Your configuration files will have migrated from /config/hassio_addons/calibre to a folder only accessible from my Filebrowser addon called /app_configs/something-calibre. This avoids the addon to mess with your homeassistant configuration folder, and allows to backup the options. Migration of data, custom configs, and custom scripts should be automatic. Please be sure to update all your links however ! For more information, see here : https://developers.home-assistant.io/blog/2023/11/06/public-addon-config/ https://github.com/alexbelgium/hassio-addons/issues/1154
|
||||
|
||||
## 7.2.0 (2023-12-16)
|
||||
|
||||
|
||||
@@ -98,7 +98,7 @@ This addon supports mounting both local drives and remote SMB shares:
|
||||
|
||||
### Custom Scripts and Environment Variables
|
||||
|
||||
This addon supports custom scripts and environment variables through the `addon_config` mapping:
|
||||
This addon supports custom scripts and environment variables through the `app_config` mapping:
|
||||
|
||||
- **Custom scripts**: See [Running Custom Scripts in Addons](https://github.com/alexbelgium/hassio-addons/wiki/Running-custom-scripts-in-Addons)
|
||||
- **env_vars option**: Use the add-on `env_vars` option to pass extra environment variables (uppercase or lowercase names). See https://github.com/alexbelgium/hassio-addons/wiki/Add-Environment-variables-to-your-Addon-2 for details.
|
||||
|
||||
@@ -78,7 +78,7 @@ init: false
|
||||
map:
|
||||
- media:rw
|
||||
- share:rw
|
||||
- addon_config:rw
|
||||
- app_config:rw
|
||||
- homeassistant_config:rw
|
||||
- ssl
|
||||
name: Calibre
|
||||
|
||||
|
Before Width: | Height: | Size: 3.5 KiB After Width: | Height: | Size: 1.6 KiB |
@@ -1,3 +1,4 @@
|
||||
- Migrate legacy add-on configuration map names to current app configuration terminology.
|
||||
|
||||
## 0.6.26-2 (2026-04-06)
|
||||
- Fix: Install calibre (calibredb) at build time to fix 500 error when downloading books
|
||||
@@ -10,7 +11,7 @@
|
||||
- Update to latest version from linuxserver/docker-calibre-web (changelog : https://github.com/linuxserver/docker-calibre-web/releases)
|
||||
## 0.6.24-10 (2025-05-28)
|
||||
- Fix migration script
|
||||
- Allow access to Calibre folder through /addon_configs/xxx-Calibre
|
||||
- Allow access to Calibre folder through /app_configs/xxx-Calibre
|
||||
|
||||
## 0.6.24 (2024-11-23)
|
||||
- Update to latest version from linuxserver/docker-calibre-web (changelog : https://github.com/linuxserver/docker-calibre-web/releases)
|
||||
@@ -26,7 +27,7 @@
|
||||
- Minor bugs fixed
|
||||
## 0.6.21-5 (2024-01-12)
|
||||
|
||||
- ⚠ MAJOR CHANGE : switch to the new config logic from homeassistant. Your configuration files will have migrated from /config/hassio_addons/calibre-web to a folder only accessible from my Filebrowser addon called /addon_configs/something-calibre-web. This avoids the addon to mess with your homeassistant configuration folder, and allows to backup the options. Migration of data, custom configs, and custom scripts should be automatic. Please be sure to update all your links however ! For more information, see here : https://developers.home-assistant.io/blog/2023/11/06/public-addon-config/ https://github.com/alexbelgium/hassio-addons/issues/1177
|
||||
- ⚠ MAJOR CHANGE : switch to the new config logic from homeassistant. Your configuration files will have migrated from /config/hassio_addons/calibre-web to a folder only accessible from my Filebrowser addon called /app_configs/something-calibre-web. This avoids the addon to mess with your homeassistant configuration folder, and allows to backup the options. Migration of data, custom configs, and custom scripts should be automatic. Please be sure to update all your links however ! For more information, see here : https://developers.home-assistant.io/blog/2023/11/06/public-addon-config/ https://github.com/alexbelgium/hassio-addons/issues/1177
|
||||
|
||||
## 0.6.21-2 (2023-11-05)
|
||||
|
||||
|
||||
@@ -77,8 +77,8 @@ image: ghcr.io/alexbelgium/calibre_web-{arch}
|
||||
ingress: true
|
||||
init: false
|
||||
map:
|
||||
- addon_config:rw
|
||||
- all_addon_configs:rw
|
||||
- app_config:rw
|
||||
- all_app_configs:rw
|
||||
- homeassistant_config:rw
|
||||
- media:rw
|
||||
- share:rw
|
||||
|
||||
|
Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 1.4 KiB |
@@ -1,9 +1,40 @@
|
||||
## 1.38 (02-08-2026)
|
||||
|
||||
- Reduce Claude Desktop add-on RAM use in three places without removing Headroom, RTK, TokenSave, Cowork, Dispatch, or the streamed desktop. Headroom's heavy HTTP proxy is no longer started at container boot: a standard-library TCP gate stays on port 8787, starts the real proxy on the first request, and stops it after 15 minutes without traffic (`HEADROOM_IDLE_TIMEOUT_SECONDS` remains overridable through `env_vars`). This releases the proxy's Python, ONNX Runtime, tokenizer, and Kompress model allocations while the add-on is idle; a later request starts a clean backend transparently.
|
||||
- Prevent Headroom from loading a second Kompress model inside every Claude Desktop/Claude Code MCP process. The add-on now intercepts `headroom mcp serve` with a lightweight adapter that retains upstream MCP retrieval/statistics behavior but delegates `headroom_compress` to the shared proxy's loopback `/v1/compress` endpoint. The MCP process explicitly disables local Kompress and never imports `headroom.compress`, so only the proxy backend can own the ML runtime.
|
||||
- Remove the add-on-wide `tmpfs: true` mount and undo the shared Selkies script's `/tmp/cache` redirection for this add-on. Electron/Chromium, Mesa, and application caches now live under the selected persistent home (`$HOME/.cache`) as reclaimable filesystem cache instead of RAM-backed cgroup shmem. Runtime sockets and XDG runtime state remain under `/run`.
|
||||
- Migrate legacy add-on configuration map names to current app configuration terminology.
|
||||
## 2026.08.03 (03-08-2026)
|
||||
- Performance: Claude Desktop now uses the GPU instead of rendering on the CPU.
|
||||
Under Xvfb, Chromium probed GLX, found only Xvfb's software path, and fell back to
|
||||
`--use-gl=disabled` + `--disable-gpu-compositing`, making the renderer the add-on's largest
|
||||
CPU consumer. It is now launched with `--ozone-platform=x11 --use-gl=angle --use-angle=gl-egl`,
|
||||
but only when the new `claude-gpu-probe` confirms Claude Desktop's own bundled ANGLE can
|
||||
create a hardware GL context on this host. New `gpu_acceleration` option (`auto`/`on`/`off`,
|
||||
default `auto`); any probe failure keeps the previous software rendering unchanged.
|
||||
- Performance: new `max_resolution` option (default `1920x1080`) caps the virtual screen via the
|
||||
base image's `MAX_RES`. Xvfb previously ran at 15360x8640, so Xvfb and the Selkies capture
|
||||
loop tracked damage over a 133-megapixel area continuously, even with no browser connected.
|
||||
Selkies still resizes dynamically below the cap.
|
||||
- Performance: Claude Code now talks to the Home Assistant MCP server over its native HTTP
|
||||
transport instead of through the `mcp-proxy` stdio bridge, removing one Python process per
|
||||
Claude Code session (~45 MB of private resident memory each). Claude Desktop keeps the
|
||||
bridge, as the config schema for a remote entry there is not confirmed.
|
||||
- Performance: new `mcp_servers_desktop` / `mcp_servers_code` options select which MCP servers
|
||||
each client registers. Every stdio MCP server is a separate process per client, and Desktop
|
||||
starts another full set per Claude Code session it hosts. Defaults register all of them in
|
||||
both clients, i.e. the previous behaviour.
|
||||
- Fix: the Dockerfile's Intel graphics block was dead code. It was gated on `TARGETARCH`, which
|
||||
the repo's builder does not pass, so it never ran: the shipped amd64 image has no `vainfo` and
|
||||
no `intel-media-va-driver-non-free`. It now uses `BUILD_ARCH`, and its Vulkan ICD check no
|
||||
longer names `intel_icd.x86_64.json`, a file Debian does not ship.
|
||||
- Fix: stale Home Assistant MCP registrations (and the bearer token in them) are now removed
|
||||
from Claude Code's config when `enable_ha_mcp` is turned off. Ownership of an HTTP entry is
|
||||
recorded when the add-on writes it, so a manually configured `homeassistant` server is never
|
||||
claimed, overwritten, or deleted — not even when it sits on the default URL.
|
||||
- `--disable-dev-shm-usage` is now applied only when `/dev/shm` is actually small (under
|
||||
256 MB). It is a workaround for Docker's 64 MB default, but Home Assistant ignores the add-on's
|
||||
`shm_size` so the real size varies per install; the size is now read at startup, keeping the
|
||||
crash workaround where it is needed and dropping it where it only pushed Chromium's shared
|
||||
memory into ordinary files. If the size cannot be determined, the flag is kept.
|
||||
|
||||
## 2026.08.02 (02-08-2026)
|
||||
- Minor bugs fixed
|
||||
|
||||
## ubunturesolute-version-3a10bef7 (2026-08-01)
|
||||
- Update to latest version from linuxserver/docker-baseimage-selkies (changelog : https://github.com/linuxserver/docker-baseimage-selkies/releases)
|
||||
|
||||
@@ -146,7 +146,19 @@ RUN install -d -m 0755 /etc/apt/keyrings && \
|
||||
# /dev/dri nodes. Explicitly install the amd64 userspace stack needed for accelerated
|
||||
# OpenGL rendering, VA-API video encoding, and Vulkan, then fail the build if any driver
|
||||
# payload is missing. Keep aarch64 unchanged because these Intel packages are amd64-only.
|
||||
RUN if [[ "${TARGETARCH}" == "amd64" ]]; then \
|
||||
#
|
||||
# Gate on BUILD_ARCH, not TARGETARCH. TARGETARCH is a BuildKit-provided platform ARG; the
|
||||
# repo's builder (.github/workflows/onpush_builder.yaml) passes BUILD_ARCH explicitly and
|
||||
# that is the contract this repo can rely on. This block previously used TARGETARCH and
|
||||
# silently never ran: the shipped amd64 image has no `vainfo` and no
|
||||
# `intel-media-va-driver-non-free`, and its apt history contains no matching install — so
|
||||
# the "fail the build if a payload is missing" guarantee below had never once executed.
|
||||
#
|
||||
# The Vulkan ICD payload is checked via the driver directory rather than a single
|
||||
# distro-specific filename: the previous `test -f intel_icd.x86_64.json` named a file Debian
|
||||
# does not ship (it installs `intel_icd.json`), so restoring the guard without this change
|
||||
# would have turned dead code straight into a failing build.
|
||||
RUN if [[ "${BUILD_ARCH}" == "amd64" ]]; then \
|
||||
apt-get update && \
|
||||
apt-get install -y --no-install-recommends \
|
||||
intel-media-va-driver-non-free \
|
||||
@@ -155,7 +167,7 @@ RUN if [[ "${TARGETARCH}" == "amd64" ]]; then \
|
||||
vainfo && \
|
||||
test -f /usr/lib/x86_64-linux-gnu/dri/iHD_drv_video.so && \
|
||||
test -f /usr/lib/x86_64-linux-gnu/dri/iris_dri.so && \
|
||||
test -f /usr/share/vulkan/icd.d/intel_icd.x86_64.json && \
|
||||
ls /usr/share/vulkan/icd.d/intel_icd*.json > /dev/null && \
|
||||
command -v vainfo > /dev/null; \
|
||||
fi && \
|
||||
apt-get clean && \
|
||||
|
||||
@@ -97,6 +97,8 @@ Git synchronization hooks. A repository is indexed only when it is listed in
|
||||
| `KEYBOARD` | | Optional Selkies keyboard layout. |
|
||||
| `PASSWORD` | | Optional password for direct Selkies ports. |
|
||||
| `DRINODE` | | Optional GPU device override for Selkies. |
|
||||
| `gpu_acceleration` | `auto` | Whether Claude Desktop renders on the GPU. `auto` adds Chromium's ANGLE/EGL flags only when a probe confirms a hardware GL context is available, `on` forces them without probing, `off` keeps software rendering. Use `on` with care: it skips every safety check, so on a host that cannot actually drive those flags the desktop can come up black — set the option back to `auto` or `off` to recover. |
|
||||
| `max_resolution` | `1920x1080` | Caps the virtual screen. Selkies still resizes dynamically below this; raise it only if you drive the desktop from a larger display. |
|
||||
| `DNS_server` | `8.8.8.8` | DNS server used by the standard DNS module. |
|
||||
| `permission_mode` | `auto` | Claude Code permission policy: `strict`, `auto`, or `bypass`. |
|
||||
| `install_headroom` | `true` | Register Headroom MCP and run the supervised local proxy. |
|
||||
@@ -106,6 +108,8 @@ Git synchronization hooks. A repository is indexed only when it is listed in
|
||||
| `install_rtk` | `true` | Configure RTK's Claude Code `PreToolUse` Bash hook. |
|
||||
| `install_tokensave` | `true` | Install TokenSave's complete global Claude integration. |
|
||||
| `tokensave_project_paths` | `[]` | Explicit absolute Git repository paths to initialize or sync at startup. |
|
||||
| `mcp_servers_desktop` | all | Which managed MCP servers Claude Desktop registers (`headroom`, `tokensave`, `homeassistant`, `codex`). |
|
||||
| `mcp_servers_code` | all | Which managed MCP servers Claude Code registers. Each stdio server is a separate process per client, and Desktop starts another set per Claude Code session it hosts, so trimming this is the cheapest way to cut memory. |
|
||||
| `install_caveman` | `false` | Install the third-party Caveman Claude Code plugin at startup. |
|
||||
| `install_codex_cli` | `false` | Install the latest stable OpenAI Codex CLI at startup and register its native MCP server so Claude can delegate work to ChatGPT Codex. |
|
||||
| `codex_sandbox_mode` | `workspace-write` | Filesystem scope Codex runs with: `read-only`, `workspace-write`, or `danger-full-access`. |
|
||||
|
||||
@@ -61,7 +61,7 @@ magic link into the in-session Chromium (not a phone).
|
||||
- Add-on Configuration → `additional_apps: chromium`, restart (installed by
|
||||
`rootfs/etc/cont-init.d/80-configuration.sh`).
|
||||
- Add the two `xdg-settings`/`xdg-mime` commands to the custom script
|
||||
`/addon_configs/db21ed7f_claude-desktop/claude_desktop.sh` (the image ships no standalone
|
||||
`/app_configs/db21ed7f_claude-desktop/claude_desktop.sh` (the image ships no standalone
|
||||
terminal).
|
||||
|
||||
---
|
||||
|
||||
@@ -34,7 +34,7 @@ hassio_role: manager
|
||||
homeassistant_api: true
|
||||
auth_api: true
|
||||
map:
|
||||
- addon_config:rw
|
||||
- app_config:rw
|
||||
- share:rw
|
||||
- media:rw
|
||||
- ssl
|
||||
@@ -56,6 +56,7 @@ options:
|
||||
github_username: ""
|
||||
enable_tools_health_report: true
|
||||
expose_headroom_dashboard: false
|
||||
gpu_acceleration: auto
|
||||
headroom_auto_compress: true
|
||||
headroom_wrap_claude_code: true
|
||||
install_caveman: false
|
||||
@@ -65,6 +66,17 @@ options:
|
||||
install_headroom: true
|
||||
install_rtk: true
|
||||
install_tokensave: true
|
||||
max_resolution: 1920x1080
|
||||
mcp_servers_desktop:
|
||||
- headroom
|
||||
- tokensave
|
||||
- homeassistant
|
||||
- codex
|
||||
mcp_servers_code:
|
||||
- headroom
|
||||
- tokensave
|
||||
- homeassistant
|
||||
- codex
|
||||
permission_mode: auto
|
||||
tokensave_project_paths: []
|
||||
panel_admin: false
|
||||
@@ -106,6 +118,7 @@ schema:
|
||||
github_username: str?
|
||||
enable_tools_health_report: bool
|
||||
expose_headroom_dashboard: bool
|
||||
gpu_acceleration: list(auto|on|off)?
|
||||
headroom_auto_compress: bool?
|
||||
headroom_wrap_claude_code: bool
|
||||
install_caveman: bool
|
||||
@@ -115,11 +128,16 @@ schema:
|
||||
install_headroom: bool
|
||||
install_rtk: bool
|
||||
install_tokensave: bool
|
||||
max_resolution: str?
|
||||
mcp_servers_desktop:
|
||||
- list(headroom|tokensave|homeassistant|codex)
|
||||
mcp_servers_code:
|
||||
- list(headroom|tokensave|homeassistant|codex)
|
||||
permission_mode: list(strict|auto|bypass)
|
||||
tokensave_project_paths:
|
||||
- str
|
||||
slug: claude_desktop
|
||||
udev: true
|
||||
url: https://github.com/alexbelgium/hassio-addons
|
||||
version: "1.38"
|
||||
version: "2026.08.03"
|
||||
video: true
|
||||
|
||||
@@ -20,4 +20,67 @@
|
||||
# Headroom is intentionally not injected into the Desktop process: Claude Desktop
|
||||
# force-overrides ANTHROPIC_BASE_URL (headroom #869), so Desktop uses the registered Headroom
|
||||
# MCP tools instead.
|
||||
exec claude-desktop --no-sandbox --disable-dev-shm-usage --password-store=basic
|
||||
|
||||
# GPU acceleration.
|
||||
#
|
||||
# Under Xvfb, Chromium probes GLX, finds only Xvfb's indirect/software path, and falls back to
|
||||
# rendering everything on the CPU (`--use-gl=disabled` on the GPU process, and
|
||||
# `--disable-gpu-compositing` on the renderer). On a small Home Assistant host that is the
|
||||
# add-on's single largest CPU consumer.
|
||||
#
|
||||
# Routing Chromium through ANGLE's OpenGL backend over EGL uses the real render node instead.
|
||||
# The flags are only added when /usr/local/bin/claude-gpu-probe confirms that Claude Desktop's
|
||||
# own bundled ANGLE can create a hardware GL context here, because forcing them on a host with
|
||||
# no render node (or one where Mesa falls back to llvmpipe) trades a working software desktop
|
||||
# for a black window or a GPU-process crash loop. Any probe failure leaves the command line
|
||||
# untouched, i.e. exactly the pre-existing software-rendering behaviour.
|
||||
#
|
||||
# `--use-angle=gles-egl` is deliberately not used: Mesa rejects it with "Intel or NVIDIA
|
||||
# OpenGL ES drivers are not supported".
|
||||
#
|
||||
# /run/claude-desktop-gpu-mode is written each boot by /etc/cont-init.d/85-openbox_autostart.sh
|
||||
# from the `gpu_acceleration` add-on option (auto|on|off).
|
||||
GPU_FLAGS=""
|
||||
GPU_MODE="auto"
|
||||
if [ -r /run/claude-desktop-gpu-mode ]; then
|
||||
GPU_MODE="$(cat /run/claude-desktop-gpu-mode)"
|
||||
fi
|
||||
case "$GPU_MODE" in
|
||||
off)
|
||||
echo "claude-desktop: gpu_acceleration=off; using software rendering" >&2
|
||||
;;
|
||||
on)
|
||||
# Escape hatch for hosts where the probe is wrong in either direction.
|
||||
GPU_FLAGS="--ozone-platform=x11 --use-gl=angle --use-angle=gl-egl"
|
||||
echo "claude-desktop: gpu_acceleration=on; forcing ANGLE/EGL without probing" >&2
|
||||
;;
|
||||
*)
|
||||
# Bounded: this sits in the desktop's startup path, and a driver that wedges during
|
||||
# EGL init must not leave the user staring at an empty screen. A timeout is treated
|
||||
# exactly like a failed probe, i.e. software rendering.
|
||||
if timeout 15 claude-gpu-probe; then
|
||||
GPU_FLAGS="--ozone-platform=x11 --use-gl=angle --use-angle=gl-egl"
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
|
||||
# Shared memory.
|
||||
#
|
||||
# --disable-dev-shm-usage exists because Docker's default /dev/shm is 64 MB, which is not
|
||||
# enough for Chromium's renderers and produced a crash loop here (see the 1.3 changelog entry).
|
||||
# Home Assistant ignores the add-on's shm_size, so the size cannot be set from this repo and
|
||||
# genuinely varies between installs — it is 7.7 GB on some hosts and the 64 MB default on
|
||||
# others. Hardcoding either answer is wrong, so ask the kernel: keep the workaround when
|
||||
# /dev/shm is small, and drop it when there is plenty, where it would otherwise push Chromium's
|
||||
# shared memory into ordinary files for no benefit. If the size cannot be determined, keep the
|
||||
# flag — the crash it prevents is worse than the overhead it costs.
|
||||
SHM_FLAGS="--disable-dev-shm-usage"
|
||||
SHM_KB="$(df -k /dev/shm 2> /dev/null | awk 'NR==2 {print $2}')"
|
||||
if [ -n "$SHM_KB" ] && [ "$SHM_KB" -ge 262144 ]; then
|
||||
SHM_FLAGS=""
|
||||
fi
|
||||
|
||||
# GPU_FLAGS and SHM_FLAGS must stay unquoted so they expand to separate arguments, or to
|
||||
# nothing at all.
|
||||
# shellcheck disable=SC2086
|
||||
exec claude-desktop --no-sandbox --password-store=basic $SHM_FLAGS $GPU_FLAGS
|
||||
|
||||
66
claude_desktop/rootfs/etc/cont-init.d/22-display_tuning.sh
Executable file
@@ -0,0 +1,66 @@
|
||||
#!/usr/bin/with-contenv bashio
|
||||
# shellcheck shell=bash
|
||||
set -e
|
||||
|
||||
# Cap the virtual screen the desktop is drawn on.
|
||||
#
|
||||
# The Selkies base image starts Xvfb at DEFAULT_RES=15360x8640 so that a client on any monitor
|
||||
# can resize into it. Nothing here needs a 133-megapixel screen: it enlarges the area Xvfb and
|
||||
# the Selkies capture loop track for damage on every frame, which the add-on pays for
|
||||
# continuously — measurably so even with no browser connected at all.
|
||||
#
|
||||
# MAX_RES is the base image's own knob for this (svc-xorg prefers it over DEFAULT_RES) and it
|
||||
# only sets the *maximum*; Selkies still resizes dynamically underneath it, so a smaller cap
|
||||
# costs nothing until a client actually asks for something larger.
|
||||
#
|
||||
# This is a CPU and address-space saving, not a memory one: the framebuffer is a lazily
|
||||
# populated SysV shared segment, so the unused portion of the oversized screen was never
|
||||
# resident to begin with.
|
||||
#
|
||||
# Note SELKIES_MANUAL_WIDTH/HEIGHT is a different knob that *pins* the resolution and disables
|
||||
# dynamic resizing. It is deliberately not used here.
|
||||
MAX_RESOLUTION="$(bashio::config 'max_resolution' '1920x1080')"
|
||||
|
||||
if [ -z "$MAX_RESOLUTION" ]; then
|
||||
bashio::log.info "max_resolution is empty; leaving the base image's default virtual screen size"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Matched with bash's own =~ rather than grep: grep anchors per *line*, so a multi-line value
|
||||
# such as "1920x1080\n640x480" satisfies ^...$ on its first line and would be passed through to
|
||||
# Xvfb verbatim. Bash anchors the whole string, so an embedded newline is rejected.
|
||||
if [[ ! "$MAX_RESOLUTION" =~ ^[0-9]{1,5}x[0-9]{1,5}$ ]]; then
|
||||
bashio::log.warning "max_resolution '${MAX_RESOLUTION}' is not WIDTHxHEIGHT; leaving the base image default"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# A syntactically valid but nonsensical size (0x0, 99999x99999) would either stop Xvfb from
|
||||
# starting at all or ask it for a framebuffer larger than the default this option exists to
|
||||
# shrink. Bound it to something Xvfb and Selkies can actually serve; the upper bound is the
|
||||
# base image's own default, so this option can only ever reduce the screen.
|
||||
MAX_WIDTH="${MAX_RESOLUTION%%x*}"
|
||||
MAX_HEIGHT="${MAX_RESOLUTION##*x}"
|
||||
if [ "$MAX_WIDTH" -lt 640 ] || [ "$MAX_HEIGHT" -lt 480 ] ||
|
||||
[ "$MAX_WIDTH" -gt 15360 ] || [ "$MAX_HEIGHT" -gt 8640 ]; then
|
||||
bashio::log.warning "max_resolution '${MAX_RESOLUTION}' is outside the supported range (640x480 to 15360x8640); leaving the base image default"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# cont-init.d completes before any s6-rc service starts, so svc-xorg picks this up on the same
|
||||
# boot. Both paths are written because the base image's scripts read the legacy /var/run alias.
|
||||
written=0
|
||||
for envdir in /var/run/s6/container_environment /run/s6/container_environment; do
|
||||
if [ -d "$envdir" ]; then
|
||||
printf '%s' "$MAX_RESOLUTION" > "${envdir}/MAX_RES"
|
||||
written=1
|
||||
fi
|
||||
done
|
||||
|
||||
# Claiming success after writing nothing would send someone hunting for a cap that svc-xorg
|
||||
# never saw.
|
||||
if [ "$written" -eq 0 ]; then
|
||||
bashio::log.warning "No s6 environment directory found; leaving the base image's default virtual screen size"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
bashio::log.info "Virtual screen capped at ${MAX_RESOLUTION} (Selkies still resizes dynamically below this)"
|
||||
@@ -1,58 +0,0 @@
|
||||
#!/usr/bin/with-contenv bashio
|
||||
# shellcheck shell=bash
|
||||
set -e
|
||||
|
||||
# 20-folders.sh is shared with the Webtop add-ons and historically redirects
|
||||
# XDG_CACHE_HOME to /tmp/cache. This Claude-specific follow-up runs before the
|
||||
# graphical longruns and moves general application caches back under the
|
||||
# persistent home. Removing config.yaml's `tmpfs: true` then ensures Chromium,
|
||||
# Electron, Mesa and other cache pages are reclaimable filesystem cache instead
|
||||
# of permanently charged cgroup shmem.
|
||||
LOCATION="$(getent passwd abc 2> /dev/null | cut -d: -f6 || true)"
|
||||
if [ -z "$LOCATION" ] || [ "$LOCATION" = "/" ]; then
|
||||
bashio::log.warning "Unable to resolve abc home; leaving XDG cache configuration unchanged"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
CACHE_DIR="$LOCATION/.cache"
|
||||
if [ -L "$CACHE_DIR" ]; then
|
||||
rm -f "$CACHE_DIR"
|
||||
fi
|
||||
mkdir -p "$CACHE_DIR"
|
||||
chown "$(id -u abc):$(id -g abc)" "$CACHE_DIR"
|
||||
chmod 700 "$CACHE_DIR"
|
||||
|
||||
CACHE_DIR="$CACHE_DIR" LOCATION="$LOCATION" python3 - <<'PY'
|
||||
import os
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
cache = os.environ["CACHE_DIR"]
|
||||
quoted = cache.replace("\\", "\\\\").replace('"', '\\"')
|
||||
replacement = f'export XDG_CACHE_HOME="{quoted}"'
|
||||
|
||||
for path in Path("/etc/s6-overlay/s6-rc.d").glob("*/run"):
|
||||
try:
|
||||
text = path.read_text()
|
||||
except (OSError, UnicodeDecodeError):
|
||||
continue
|
||||
updated = re.sub(r"^export XDG_CACHE_HOME=.*$", replacement, text, flags=re.MULTILINE)
|
||||
if updated != text:
|
||||
path.write_text(updated)
|
||||
|
||||
bashrc = Path(os.environ["LOCATION"]) / ".bashrc"
|
||||
if bashrc.exists():
|
||||
text = bashrc.read_text()
|
||||
updated = re.sub(r"^export XDG_CACHE_HOME=.*$", replacement, text, flags=re.MULTILINE)
|
||||
if updated != text:
|
||||
bashrc.write_text(updated)
|
||||
PY
|
||||
|
||||
S6_ENVDIR="/run/s6/container_environment"
|
||||
mkdir -p "$S6_ENVDIR"
|
||||
printf '%s' "$CACHE_DIR" > "$S6_ENVDIR/XDG_CACHE_HOME"
|
||||
|
||||
# Safe here: no graphical longrun has started yet, and the former directory was
|
||||
# only a boot-created target for the now-removed persistent-home symlink.
|
||||
rm -rf /tmp/cache
|
||||
bashio::log.info "Application cache moved from RAM-backed /tmp to $CACHE_DIR"
|
||||
@@ -1,59 +0,0 @@
|
||||
#!/usr/bin/with-contenv bashio
|
||||
# shellcheck shell=bash
|
||||
set -e
|
||||
|
||||
# Keep every existing Headroom CLI command unchanged, but intercept the MCP
|
||||
# server entrypoint so it uses the add-on's single-runtime adapter. The real
|
||||
# binary is intentionally outside /usr/local/bin; writing the wrapper there
|
||||
# makes it the command 82-claude_tools.sh registers in Claude Desktop/Code.
|
||||
REAL_HEADROOM=""
|
||||
for candidate in /usr/bin/headroom /lsiopy/bin/headroom; do
|
||||
if [ -x "$candidate" ]; then
|
||||
REAL_HEADROOM="$candidate"
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
if [ -z "$REAL_HEADROOM" ]; then
|
||||
bashio::log.warning "Headroom executable was not found; MCP adapter wrapper was not installed"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# The adapter intentionally uses a small, stable subset of the upstream MCP
|
||||
# server. Since Headroom is installed unpinned at image build time, verify that
|
||||
# subset before replacing the command. A future incompatible Headroom release
|
||||
# therefore keeps its native MCP server instead of breaking Claude startup.
|
||||
if ! /lsiopy/bin/python3 - <<'PY'
|
||||
from headroom.ccr.mcp_server import HeadroomMCPServer
|
||||
|
||||
for name in ("run_stdio", "cleanup", "_compress_content"):
|
||||
if not callable(getattr(HeadroomMCPServer, name, None)):
|
||||
raise SystemExit(f"HeadroomMCPServer.{name} is unavailable")
|
||||
PY
|
||||
then
|
||||
bashio::log.warning "Installed Headroom is incompatible with the single-runtime MCP adapter; preserving the native MCP server"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
wrapper="$(mktemp /usr/local/bin/.headroom-wrapper.XXXXXX)"
|
||||
cleanup() {
|
||||
rm -f "$wrapper"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
cat > "$wrapper" <<EOF
|
||||
#!/bin/sh
|
||||
REAL_HEADROOM="$REAL_HEADROOM"
|
||||
if [ "\${1:-}" = "mcp" ] && [ "\${2:-}" = "serve" ]; then
|
||||
shift 2
|
||||
unset HF_HOME
|
||||
export HEADROOM_DISABLE_KOMPRESS=1
|
||||
exec /lsiopy/bin/python3 /usr/local/bin/headroom-mcp-proxy.py "\$@"
|
||||
fi
|
||||
exec "\$REAL_HEADROOM" "\$@"
|
||||
EOF
|
||||
chmod 0755 "$wrapper"
|
||||
mv -f "$wrapper" /usr/local/bin/headroom
|
||||
trap - EXIT
|
||||
|
||||
bashio::log.info "Headroom MCP compression is delegated to the shared lazy proxy runtime"
|
||||
@@ -308,10 +308,13 @@ if bashio::config.true 'install_codex_cli'; then
|
||||
fi
|
||||
|
||||
HA_MCP_ENABLED=false
|
||||
HA_MCP_URL=""
|
||||
HA_MCP_TOKEN=""
|
||||
# Read unconditionally, even when enable_ha_mcp is off. Home Assistant keeps an option's value
|
||||
# when its toggle is disabled, and the reconciliation below needs this URL to recognise the
|
||||
# HTTP entry it previously wrote so that it can be removed — together with the bearer token
|
||||
# inside it — rather than orphaned in ~/.claude.json.
|
||||
HA_MCP_URL="$(bashio::config 'ha_mcp_url' 'http://homeassistant:8123/api/mcp')"
|
||||
if bashio::config.true 'enable_ha_mcp'; then
|
||||
HA_MCP_URL="$(bashio::config 'ha_mcp_url' 'http://homeassistant:8123/api/mcp')"
|
||||
if bashio::config.has_value 'ha_mcp_token'; then
|
||||
HA_MCP_TOKEN="$(bashio::config 'ha_mcp_token')"
|
||||
fi
|
||||
@@ -325,12 +328,73 @@ if bashio::config.true 'enable_ha_mcp'; then
|
||||
fi
|
||||
fi
|
||||
|
||||
# Which of the managed MCP servers each client gets.
|
||||
#
|
||||
# Every stdio MCP server is a separate process *per client*, and Claude Desktop starts another
|
||||
# full set for each Claude Code session it hosts — so a server registered in both clients is
|
||||
# paid for several times over. Measured on a live add-on with three sets running, the private
|
||||
# (non-shared) resident cost was roughly 54 MB per extra `headroom mcp serve`, 45 MB per extra
|
||||
# `mcp-proxy`, and only ~11 MB and ~2 MB for `codex` and `tokensave`, which share most of their
|
||||
# pages. Registering a server only where it is actually used is therefore the cheapest lever
|
||||
# available; these two options expose that choice.
|
||||
#
|
||||
# Defaults keep every enabled server in both clients, i.e. the pre-existing behaviour.
|
||||
MCP_ALL_SERVERS="headroom tokensave homeassistant codex"
|
||||
|
||||
mcp_client_list() {
|
||||
local option="$1"
|
||||
local selected=() entry raw rc=0
|
||||
|
||||
# An option that is absent entirely — i.e. an existing install upgrading from a config that
|
||||
# predates these options — keeps the previous behaviour of registering every enabled server.
|
||||
# bashio distinguishes this from an explicitly empty list: an unset key yields the literal
|
||||
# "null", while `[]` yields an empty string. Those must not be conflated, because an empty
|
||||
# list is a legitimate way to say "no MCP servers in this client" and defaulting it back to
|
||||
# all four would silently ignore the user.
|
||||
if ! bashio::config.exists "$option"; then
|
||||
echo "$MCP_ALL_SERVERS"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Capture first: reading a bashio list straight into `while read` via process substitution
|
||||
# silently yields nothing under this script's errexit. The exit status is kept separately
|
||||
# so that a failed read is not mistaken for a deliberate empty selection.
|
||||
raw="$(bashio::config "$option" 2> /dev/null)" || rc=$?
|
||||
if [ "$rc" -ne 0 ]; then
|
||||
bashio::log.warning "Could not read '${option}'; registering every enabled MCP server for this client"
|
||||
echo "$MCP_ALL_SERVERS"
|
||||
return 0
|
||||
fi
|
||||
|
||||
while read -r entry; do
|
||||
[ -n "$entry" ] || continue
|
||||
# Reconciliation deletes any managed server not named here, so an unrecognised value
|
||||
# must never be treated as an authoritative selection.
|
||||
case " $MCP_ALL_SERVERS " in
|
||||
*" $entry "*) selected+=("$entry") ;;
|
||||
*)
|
||||
bashio::log.warning "Ignoring unknown MCP server '${entry}' in '${option}'; registering every enabled server for this client"
|
||||
echo "$MCP_ALL_SERVERS"
|
||||
return 0
|
||||
;;
|
||||
esac
|
||||
done <<< "$raw"
|
||||
|
||||
echo "${selected[@]:-}"
|
||||
}
|
||||
|
||||
MCP_SERVERS_DESKTOP="$(mcp_client_list 'mcp_servers_desktop')"
|
||||
MCP_SERVERS_CODE="$(mcp_client_list 'mcp_servers_code')"
|
||||
bashio::log.info "MCP servers for Claude Desktop: ${MCP_SERVERS_DESKTOP}"
|
||||
bashio::log.info "MCP servers for Claude Code: ${MCP_SERVERS_CODE}"
|
||||
|
||||
HEADROOM_ENABLED="$HEADROOM_ENABLED" HEADROOM_BIN="$(command -v headroom || echo headroom)" \
|
||||
HEADROOM_HF_HOME="${HOME}/.headroom/hf" \
|
||||
TOKENSAVE_ENABLED="$TOKENSAVE_ENABLED" TOKENSAVE_BIN="$(command -v tokensave || echo tokensave)" \
|
||||
CODEX_ENABLED="$CODEX_ENABLED" CODEX_BIN="$CODEX_BIN" CODEX_SANDBOX_MODE="$CODEX_SANDBOX_MODE" \
|
||||
HA_MCP_ENABLED="$HA_MCP_ENABLED" HA_MCP_URL="$HA_MCP_URL" HA_MCP_TOKEN="$HA_MCP_TOKEN" \
|
||||
MCP_PROXY_BIN="$(command -v mcp-proxy || echo mcp-proxy)" \
|
||||
MCP_SERVERS_DESKTOP="$MCP_SERVERS_DESKTOP" MCP_SERVERS_CODE="$MCP_SERVERS_CODE" \
|
||||
CLAUDE_DESKTOP_CONFIG="$CLAUDE_DESKTOP_CONFIG" CLAUDE_CODE_CONFIG="$CLAUDE_CODE_CONFIG" \
|
||||
python3 - <<'PY' || bashio::log.warning "Unable to update the MCP server registrations automatically"
|
||||
import json
|
||||
@@ -385,6 +449,23 @@ if os.environ["HA_MCP_ENABLED"] == "true":
|
||||
"env": {"API_ACCESS_TOKEN": os.environ["HA_MCP_TOKEN"]},
|
||||
}
|
||||
|
||||
# Claude Code speaks Streamable HTTP MCP natively, so pointing it straight at Home Assistant
|
||||
# removes the mcp-proxy bridge process entirely — it exists only to translate stdio to the HTTP
|
||||
# transport Home Assistant already serves. That bridge was the most expensive duplicate
|
||||
# measured (~45 MB of private RSS per copy, one per Claude Code session).
|
||||
#
|
||||
# Claude Desktop keeps the stdio bridge. Its bundled MCP SDK does contain a remote transport,
|
||||
# but the shape `claude_desktop_config.json` accepts for a remote entry — and whether it
|
||||
# persists a static bearer header — could not be confirmed, and a wrong guess would silently
|
||||
# break Home Assistant access in Desktop. Revisit once that schema is verified upstream.
|
||||
HA_MCP_CODE_ENTRY = None
|
||||
if os.environ["HA_MCP_ENABLED"] == "true":
|
||||
HA_MCP_CODE_ENTRY = {
|
||||
"type": "http",
|
||||
"url": os.environ["HA_MCP_URL"],
|
||||
"headers": {"Authorization": "Bearer " + os.environ["HA_MCP_TOKEN"]},
|
||||
}
|
||||
|
||||
# An entry is add-on-managed when its command is one of our binaries living outside the
|
||||
# persistent home. Matching on the basename (rather than the exact path recorded at write
|
||||
# time) keeps entries updatable when a base-image upgrade moves the binary, while commands
|
||||
@@ -392,17 +473,77 @@ if os.environ["HA_MCP_ENABLED"] == "true":
|
||||
HOME_PREFIX = os.path.expanduser("~") + os.sep
|
||||
|
||||
|
||||
def is_managed(name, entry):
|
||||
# Ownership record for the HTTP Home Assistant entry.
|
||||
#
|
||||
# The stdio entries can be recognised on sight, because their `command` points at a binary this
|
||||
# image installs outside $HOME. An HTTP entry has no such tell: it is just a URL plus a bearer
|
||||
# header, and a user who configured `homeassistant` by hand — very plausibly at the same default
|
||||
# http://homeassistant:8123/api/mcp — would be indistinguishable from ours. Inferring ownership
|
||||
# from shape or URL would let this script delete or overwrite that entry, including their token.
|
||||
#
|
||||
# So ownership is recorded rather than guessed: the URL of an entry this script actually wrote is
|
||||
# remembered here, and only an entry matching that record is ever modified or removed. Anything
|
||||
# this script did not write is untouchable, whatever it looks like. The file holds no secrets —
|
||||
# just the endpoint — but is written 0600 to match the configs it describes.
|
||||
STATE_PATH = Path(os.path.expanduser("~")) / ".config" / "claude_desktop_addon" / "managed-mcp.json"
|
||||
|
||||
|
||||
def load_state():
|
||||
try:
|
||||
state = json.loads(STATE_PATH.read_text())
|
||||
return state if isinstance(state, dict) else {}
|
||||
except Exception:
|
||||
return {}
|
||||
|
||||
|
||||
def save_state(state):
|
||||
try:
|
||||
STATE_PATH.parent.mkdir(parents=True, exist_ok=True)
|
||||
STATE_PATH.write_text(json.dumps(state, indent=2, sort_keys=True) + "\n")
|
||||
STATE_PATH.chmod(0o600)
|
||||
except Exception:
|
||||
# Losing the record only costs us the ability to clean up later; never fail the boot.
|
||||
pass
|
||||
|
||||
|
||||
def is_managed_http_ha(entry, owned_url):
|
||||
"""True only for an HTTP entry this script previously wrote."""
|
||||
return (
|
||||
bool(owned_url)
|
||||
and entry.get("url") == owned_url
|
||||
and set(entry) == {"type", "url", "headers"}
|
||||
and entry.get("type") == "http"
|
||||
and isinstance(entry.get("headers"), dict)
|
||||
and set(entry["headers"]) == {"Authorization"}
|
||||
)
|
||||
|
||||
|
||||
def is_managed(name, entry, owned_url):
|
||||
if not isinstance(entry, dict):
|
||||
return False
|
||||
command = entry.get("command")
|
||||
if not isinstance(command, str) or command.startswith(HOME_PREFIX):
|
||||
# A commandless entry is ours only when it is an HTTP Home Assistant registration this
|
||||
# script recorded writing. Anything else — including a user's own remote server that
|
||||
# reuses the name, even on the same URL — is left alone.
|
||||
if command is None and name == "homeassistant":
|
||||
return is_managed_http_ha(entry, owned_url)
|
||||
return False
|
||||
return os.path.basename(command) == MANAGED_BASENAMES[name]
|
||||
|
||||
|
||||
SELECTED = {
|
||||
"CLAUDE_DESKTOP_CONFIG": set(os.environ["MCP_SERVERS_DESKTOP"].split()),
|
||||
"CLAUDE_CODE_CONFIG": set(os.environ["MCP_SERVERS_CODE"].split()),
|
||||
}
|
||||
|
||||
state = load_state()
|
||||
state_changed = False
|
||||
|
||||
for config_var, stdio_type in (("CLAUDE_DESKTOP_CONFIG", False), ("CLAUDE_CODE_CONFIG", True)):
|
||||
path = Path(os.environ[config_var])
|
||||
selected = SELECTED[config_var]
|
||||
owned_url = state.get(str(path), {}).get("homeassistant_http_url", "")
|
||||
try:
|
||||
data = json.loads(path.read_text()) if path.exists() else {}
|
||||
if not isinstance(data, dict):
|
||||
@@ -417,17 +558,35 @@ for config_var, stdio_type in (("CLAUDE_DESKTOP_CONFIG", False), ("CLAUDE_CODE_C
|
||||
changed = False
|
||||
for name in MANAGED_BASENAMES:
|
||||
existing = servers.get(name)
|
||||
if name in desired:
|
||||
entry = dict(desired[name])
|
||||
if stdio_type:
|
||||
entry["type"] = "stdio"
|
||||
if existing is None or is_managed(name, existing):
|
||||
if name in desired and name in selected:
|
||||
if stdio_type and name == "homeassistant" and HA_MCP_CODE_ENTRY is not None:
|
||||
# Claude Code talks to Home Assistant over HTTP directly; no bridge process.
|
||||
entry = dict(HA_MCP_CODE_ENTRY)
|
||||
else:
|
||||
entry = dict(desired[name])
|
||||
if stdio_type:
|
||||
entry["type"] = "stdio"
|
||||
# An entry we did not write is never overwritten, so a user's own HTTP
|
||||
# `homeassistant` survives even when it sits on the configured URL.
|
||||
claimable = existing is None or is_managed(name, existing, owned_url)
|
||||
if claimable:
|
||||
if existing != entry:
|
||||
servers[name] = entry
|
||||
changed = True
|
||||
elif existing is not None and is_managed(name, existing):
|
||||
if name == "homeassistant" and entry.get("type") == "http":
|
||||
if owned_url != entry["url"]:
|
||||
state.setdefault(str(path), {})["homeassistant_http_url"] = entry["url"]
|
||||
owned_url = entry["url"]
|
||||
state_changed = True
|
||||
elif existing is not None and is_managed(name, existing, owned_url):
|
||||
# Covers both "feature disabled" and "deselected for this client".
|
||||
del servers[name]
|
||||
changed = True
|
||||
if name == "homeassistant" and state.get(str(path), {}).pop(
|
||||
"homeassistant_http_url", None
|
||||
):
|
||||
owned_url = ""
|
||||
state_changed = True
|
||||
if changed:
|
||||
if servers:
|
||||
data["mcpServers"] = servers
|
||||
@@ -440,6 +599,9 @@ for config_var, stdio_type in (("CLAUDE_DESKTOP_CONFIG", False), ("CLAUDE_CODE_C
|
||||
# permissions between merges.
|
||||
if path.exists():
|
||||
path.chmod(0o600)
|
||||
|
||||
if state_changed:
|
||||
save_state(state)
|
||||
PY
|
||||
|
||||
# Guide Claude to actually use the Headroom compression tools so the MCP integration produces
|
||||
|
||||
@@ -15,6 +15,26 @@ set -e
|
||||
# regardless, so every boot picks up the current /defaults/autostart content; ownership/mode
|
||||
# is left in the normal abc-writable state that init-selkies-config itself uses when
|
||||
# RESTART_APP is unset, and re-locked by that oneshot afterward if RESTART_APP is set.
|
||||
# The autostart decides whether to hand Chromium the ANGLE/EGL flags, but it runs as abc under
|
||||
# openbox where bashio is not available. Publish the resolved option to a file it can read.
|
||||
# /run is tmpfs, so this is rewritten on every boot and never goes stale.
|
||||
GPU_MODE="$(bashio::config 'gpu_acceleration' 'auto')"
|
||||
case "$GPU_MODE" in
|
||||
auto | on | off) ;;
|
||||
*)
|
||||
bashio::log.warning "Unknown gpu_acceleration '${GPU_MODE}'; falling back to auto"
|
||||
GPU_MODE="auto"
|
||||
;;
|
||||
esac
|
||||
# Best-effort: the autostart falls back to "auto" when the file is absent, so a failure here
|
||||
# must not abort this script under errexit and leave the openbox autostart unsynced.
|
||||
if printf '%s\n' "$GPU_MODE" > /run/claude-desktop-gpu-mode 2> /dev/null; then
|
||||
chmod 0644 /run/claude-desktop-gpu-mode
|
||||
bashio::log.info "GPU acceleration mode: ${GPU_MODE}"
|
||||
else
|
||||
bashio::log.warning "Could not write /run/claude-desktop-gpu-mode; Claude Desktop will probe for GPU support (auto)"
|
||||
fi
|
||||
|
||||
if [ -f /defaults/autostart ]; then
|
||||
mkdir -p "$HOME/.config/openbox"
|
||||
cp -f /defaults/autostart "$HOME/.config/openbox/autostart"
|
||||
|
||||
@@ -1,43 +1,32 @@
|
||||
#!/usr/bin/with-contenv bashio
|
||||
# Headroom optimization proxy — lazy backend for Claude Desktop MCP and Claude Code.
|
||||
# Headroom optimization proxy — local backend for Claude Desktop MCP and Claude Code.
|
||||
declare port=8787
|
||||
declare backend_port=8789
|
||||
declare host=127.0.0.1
|
||||
|
||||
# The dashboard is unauthenticated. Keep the gate container-local by default and
|
||||
# bind all interfaces only when the user explicitly opts in and maps port 8787.
|
||||
# The dashboard is unauthenticated. Keep it container-local by default and bind all
|
||||
# interfaces only when the user explicitly opts in and maps port 8787.
|
||||
if bashio::config.true 'expose_headroom_dashboard'; then
|
||||
host=0.0.0.0
|
||||
fi
|
||||
|
||||
if bashio::config.true 'install_headroom'; then
|
||||
real_headroom=""
|
||||
for candidate in /usr/bin/headroom /lsiopy/bin/headroom; do
|
||||
if [ -x "$candidate" ]; then
|
||||
real_headroom="$candidate"
|
||||
break
|
||||
fi
|
||||
done
|
||||
if [ -n "$real_headroom" ]; then
|
||||
# Keep model artifacts persistent, but do not import Headroom or load the
|
||||
# model in this longrun. The standard-library gate starts the real proxy
|
||||
# on the first request and terminates it after the idle timeout, releasing
|
||||
# Python/ONNX/model allocations. HEADROOM_IDLE_TIMEOUT_SECONDS is
|
||||
# overridable through env_vars; 900 seconds is the default.
|
||||
export HF_HOME="${HOME}/.headroom/hf"
|
||||
mkdir -p "$HF_HOME" "${HOME}/.headroom"
|
||||
chown abc:abc "${HOME}/.headroom" "$HF_HOME" 2> /dev/null || true
|
||||
bashio::log.info "svc-headroom: starting lazy gate on ${host}:${port} (backend ${backend_port}, idle timeout ${HEADROOM_IDLE_TIMEOUT_SECONDS:-900}s)"
|
||||
exec s6-setuidgid abc env \
|
||||
HEADROOM_REAL_BIN="$real_headroom" \
|
||||
HEADROOM_GATE_HOST="$host" \
|
||||
HEADROOM_GATE_PORT="$port" \
|
||||
HEADROOM_BACKEND_HOST=127.0.0.1 \
|
||||
HEADROOM_BACKEND_PORT="$backend_port" \
|
||||
HEADROOM_IDLE_TIMEOUT_SECONDS="${HEADROOM_IDLE_TIMEOUT_SECONDS:-900}" \
|
||||
HF_HOME="$HF_HOME" \
|
||||
/lsiopy/bin/python3 /usr/local/bin/headroom-proxy-gate.py
|
||||
fi
|
||||
if bashio::config.true 'install_headroom' && command -v headroom > /dev/null 2>&1; then
|
||||
# Kompress (the ONNX compression engine) needs its model in the local HF cache: the
|
||||
# proxy's startup preload is deliberately cache-only, and the default HF cache lands
|
||||
# under ~/.cache, which the add-on points at tmpfs (/tmp/cache) — wiped on every
|
||||
# restart. Without a warm persistent cache the proxy ran forever in "deferred" mode
|
||||
# and recorded zero compression savings. Point the cache at persistent storage;
|
||||
# nothing else is needed here — the proxy's own request path already downloads a
|
||||
# missing model in the background on first use (ensure_background_load) and passes
|
||||
# requests through uncompressed until it lands, so this self-heals within a couple of
|
||||
# requests on the first boot and loads instantly (eager preload) on every boot after.
|
||||
# A synchronous pre-warm was tried here and removed: it blocked the port bind for up
|
||||
# to the download's duration, which left the settings-managed ANTHROPIC_BASE_URL
|
||||
# (see 82-claude_tools.sh) pointing at a proxy that wasn't listening yet.
|
||||
export HF_HOME="${HOME}/.headroom/hf"
|
||||
mkdir -p "$HF_HOME"
|
||||
chown abc:abc "$HF_HOME" 2> /dev/null || true
|
||||
bashio::log.info "svc-headroom: starting local Headroom proxy on ${host}:${port}"
|
||||
exec s6-setuidgid abc headroom proxy --host "${host}" --port "${port}" --code-aware
|
||||
fi
|
||||
|
||||
bashio::log.info "svc-headroom: install_headroom disabled or headroom not found; idling"
|
||||
|
||||
196
claude_desktop/rootfs/usr/local/bin/claude-gpu-probe
Executable file
@@ -0,0 +1,196 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Decide whether Claude Desktop can render on the GPU on this host.
|
||||
|
||||
Claude Desktop is Electron/Chromium. Left to itself under Xvfb it probes GLX, finds
|
||||
only the indirect/software path Xvfb offers, and gives up: the GPU process ends up
|
||||
running `--use-gl=disabled` and the renderer `--disable-gpu-compositing`, so every
|
||||
frame is rastered and composited on the CPU. On a small Home Assistant host that is
|
||||
the single largest CPU consumer the add-on has.
|
||||
|
||||
The fix is to point Chromium at ANGLE's OpenGL backend over EGL instead of GLX, but
|
||||
those flags are only safe where they actually work — forcing them on a host with no
|
||||
render node, or where Mesa falls back to a software rasterizer, trades a working
|
||||
software desktop for a black window or a GPU-process crash loop.
|
||||
|
||||
So rather than guessing from the presence of /dev/dri, this probe exercises the exact
|
||||
code path Chromium will use: it loads Claude Desktop's *own bundled ANGLE* libEGL,
|
||||
initializes the OpenGL backend, creates a real pbuffer context, and reads GL_RENDERER
|
||||
back. Exit 0 means Chromium's GL stack is known-good here; any other exit means the
|
||||
caller must leave Chromium alone and keep today's software rendering.
|
||||
|
||||
Notes for future readers:
|
||||
* ANGLE's OpenGL backend needs a reachable X display, so this must run after Xorg is
|
||||
up (i.e. from the openbox autostart, not from cont-init.d).
|
||||
* `gles-egl` is deliberately not attempted: Mesa reports "Intel or NVIDIA OpenGL ES
|
||||
drivers are not supported" and ANGLE refuses to initialize.
|
||||
* A renderer string naming SwiftShader/llvmpipe/softpipe is a *failure* here. That is
|
||||
software rendering wearing a GL hat, and forcing the flags for it would add ANGLE
|
||||
translation overhead on top of the CPU rasterization we are trying to avoid.
|
||||
"""
|
||||
|
||||
import ctypes
|
||||
import os
|
||||
import sys
|
||||
|
||||
LIBDIR = "/usr/lib/claude-desktop"
|
||||
|
||||
# EGL/ANGLE constants (see ANGLE's eglext.h); hardcoded to avoid a build dependency.
|
||||
EGL_NONE = 0x3038
|
||||
EGL_PLATFORM_ANGLE_ANGLE = 0x3202
|
||||
EGL_PLATFORM_ANGLE_TYPE_ANGLE = 0x3203
|
||||
EGL_PLATFORM_ANGLE_TYPE_OPENGL_ANGLE = 0x320D
|
||||
EGL_OPENGL_ES_API = 0x30A0
|
||||
EGL_SURFACE_TYPE = 0x3033
|
||||
EGL_PBUFFER_BIT = 0x0001
|
||||
EGL_RENDERABLE_TYPE = 0x3040
|
||||
EGL_OPENGL_ES2_BIT = 0x0004
|
||||
EGL_WIDTH = 0x3057
|
||||
EGL_HEIGHT = 0x3056
|
||||
EGL_CONTEXT_CLIENT_VERSION = 0x3098
|
||||
GL_VENDOR = 0x1F00
|
||||
GL_RENDERER = 0x1F01
|
||||
|
||||
SOFTWARE_MARKERS = ("swiftshader", "llvmpipe", "softpipe", "lavapipe", "software rasterizer")
|
||||
|
||||
|
||||
class ProbeFailure(Exception):
|
||||
"""Raised when this host cannot give Chromium a hardware GL context."""
|
||||
|
||||
|
||||
def log(message):
|
||||
"""Write a probe diagnostic to stderr, where it lands in the add-on log."""
|
||||
sys.stderr.write(f"claude-gpu-probe: {message}\n")
|
||||
|
||||
|
||||
def load_angle():
|
||||
"""Load Claude Desktop's bundled ANGLE and declare the signatures we call."""
|
||||
egl_path = os.path.join(LIBDIR, "libEGL.so")
|
||||
gles_path = os.path.join(LIBDIR, "libGLESv2.so")
|
||||
if not (os.path.exists(egl_path) and os.path.exists(gles_path)):
|
||||
raise ProbeFailure(f"bundled ANGLE libraries not found under {LIBDIR}")
|
||||
|
||||
try:
|
||||
egl = ctypes.CDLL(egl_path, mode=ctypes.RTLD_GLOBAL)
|
||||
gles = ctypes.CDLL(gles_path, mode=ctypes.RTLD_GLOBAL)
|
||||
except OSError as err:
|
||||
raise ProbeFailure(f"could not load bundled ANGLE: {err}") from err
|
||||
|
||||
egl.eglGetProcAddress.restype = ctypes.c_void_p
|
||||
egl.eglGetError.restype = ctypes.c_int
|
||||
egl.eglInitialize.argtypes = [
|
||||
ctypes.c_void_p,
|
||||
ctypes.POINTER(ctypes.c_int),
|
||||
ctypes.POINTER(ctypes.c_int),
|
||||
]
|
||||
egl.eglCreatePbufferSurface.restype = ctypes.c_void_p
|
||||
egl.eglCreateContext.restype = ctypes.c_void_p
|
||||
gles.glGetString.restype = ctypes.c_char_p
|
||||
gles.glGetString.argtypes = [ctypes.c_uint]
|
||||
return egl, gles
|
||||
|
||||
|
||||
def open_angle_display(egl):
|
||||
"""Initialize ANGLE's OpenGL backend and return its EGL display."""
|
||||
addr = egl.eglGetProcAddress(b"eglGetPlatformDisplayEXT")
|
||||
if not addr:
|
||||
raise ProbeFailure("bundled ANGLE has no eglGetPlatformDisplayEXT")
|
||||
get_platform_display = ctypes.CFUNCTYPE(
|
||||
ctypes.c_void_p, ctypes.c_uint, ctypes.c_void_p, ctypes.POINTER(ctypes.c_int)
|
||||
)(addr)
|
||||
|
||||
attrs = (ctypes.c_int * 3)(
|
||||
EGL_PLATFORM_ANGLE_TYPE_ANGLE, EGL_PLATFORM_ANGLE_TYPE_OPENGL_ANGLE, EGL_NONE
|
||||
)
|
||||
display = get_platform_display(EGL_PLATFORM_ANGLE_ANGLE, None, attrs)
|
||||
if not display:
|
||||
raise ProbeFailure(f"no ANGLE OpenGL display (egl error 0x{egl.eglGetError():x})")
|
||||
|
||||
major, minor = ctypes.c_int(), ctypes.c_int()
|
||||
if not egl.eglInitialize(ctypes.c_void_p(display), ctypes.byref(major), ctypes.byref(minor)):
|
||||
raise ProbeFailure(
|
||||
f"ANGLE OpenGL backend failed to initialize (egl error 0x{egl.eglGetError():x})"
|
||||
)
|
||||
return display
|
||||
|
||||
|
||||
def make_current_context(egl, display):
|
||||
"""Bring up a real pbuffer context.
|
||||
|
||||
Initialization alone is not proof of anything: only a current context makes
|
||||
GL_RENDERER report the driver Chromium would actually be handed.
|
||||
"""
|
||||
egl.eglBindAPI(EGL_OPENGL_ES_API)
|
||||
config = ctypes.c_void_p()
|
||||
count = ctypes.c_int()
|
||||
config_attrs = (ctypes.c_int * 5)(
|
||||
EGL_SURFACE_TYPE, EGL_PBUFFER_BIT, EGL_RENDERABLE_TYPE, EGL_OPENGL_ES2_BIT, EGL_NONE
|
||||
)
|
||||
chosen = egl.eglChooseConfig(
|
||||
ctypes.c_void_p(display), config_attrs, ctypes.byref(config), 1, ctypes.byref(count)
|
||||
)
|
||||
if not chosen or count.value == 0:
|
||||
raise ProbeFailure(f"no usable EGL config (egl error 0x{egl.eglGetError():x})")
|
||||
|
||||
surface_attrs = (ctypes.c_int * 5)(EGL_WIDTH, 64, EGL_HEIGHT, 64, EGL_NONE)
|
||||
surface = egl.eglCreatePbufferSurface(ctypes.c_void_p(display), config, surface_attrs)
|
||||
if not surface:
|
||||
raise ProbeFailure(f"could not create pbuffer surface (egl error 0x{egl.eglGetError():x})")
|
||||
|
||||
context_attrs = (ctypes.c_int * 3)(EGL_CONTEXT_CLIENT_VERSION, 2, EGL_NONE)
|
||||
context = egl.eglCreateContext(ctypes.c_void_p(display), config, None, context_attrs)
|
||||
if not context:
|
||||
raise ProbeFailure(f"could not create GL context (egl error 0x{egl.eglGetError():x})")
|
||||
|
||||
if not egl.eglMakeCurrent(
|
||||
ctypes.c_void_p(display),
|
||||
ctypes.c_void_p(surface),
|
||||
ctypes.c_void_p(surface),
|
||||
ctypes.c_void_p(context),
|
||||
):
|
||||
raise ProbeFailure(
|
||||
f"could not make the GL context current (egl error 0x{egl.eglGetError():x})"
|
||||
)
|
||||
|
||||
|
||||
def describe_renderer(gles):
|
||||
"""Return (renderer, vendor), rejecting software rasterizers."""
|
||||
renderer = (gles.glGetString(GL_RENDERER) or b"").decode(errors="replace")
|
||||
vendor = (gles.glGetString(GL_VENDOR) or b"").decode(errors="replace")
|
||||
if not renderer:
|
||||
raise ProbeFailure("GL context reported no renderer")
|
||||
|
||||
lowered = renderer.lower()
|
||||
if any(marker in lowered for marker in SOFTWARE_MARKERS):
|
||||
raise ProbeFailure(
|
||||
f"software renderer ({renderer}); leaving Chromium on its own software path"
|
||||
)
|
||||
return renderer, vendor
|
||||
|
||||
|
||||
def main():
|
||||
"""Exit 0 only when Chromium's GL stack is known-good on this host."""
|
||||
if not os.environ.get("DISPLAY"):
|
||||
log("no DISPLAY; ANGLE's OpenGL backend needs an X server")
|
||||
return 1
|
||||
|
||||
try:
|
||||
egl, gles = load_angle()
|
||||
display = open_angle_display(egl)
|
||||
make_current_context(egl, display)
|
||||
renderer, vendor = describe_renderer(gles)
|
||||
except ProbeFailure as err:
|
||||
log(str(err))
|
||||
return 1
|
||||
# A probe is advisory: whatever goes wrong in these native calls, the desktop must still
|
||||
# start. Any unexpected failure is reported and treated as "no GPU".
|
||||
# pylint: disable=broad-exception-caught
|
||||
except Exception as err:
|
||||
log(f"unexpected probe error: {err}")
|
||||
return 1
|
||||
|
||||
log(f"hardware GL available: {renderer} | {vendor}")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -1,142 +0,0 @@
|
||||
#!/lsiopy/bin/python3
|
||||
"""Run Headroom MCP with compression delegated to the single proxy backend.
|
||||
|
||||
Upstream Headroom MCP normally performs ``headroom_compress`` in its own
|
||||
process. That imports the compression pipeline and can load a second copy of the
|
||||
Kompress ONNX model in addition to the HTTP proxy. This adapter preserves the
|
||||
upstream MCP protocol and retrieve/stats implementations, but replaces only its
|
||||
local compression method with a call to the proxy's loopback-only
|
||||
``/v1/compress`` endpoint.
|
||||
|
||||
The proxy gate starts the heavy backend on this first request and later unloads
|
||||
it after the configured idle timeout. The MCP process therefore remains a
|
||||
lightweight protocol bridge and never imports ``headroom.compress``.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import asyncio
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from types import MethodType
|
||||
from typing import Any
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(description="Headroom MCP single-runtime adapter")
|
||||
parser.add_argument(
|
||||
"--proxy-url",
|
||||
default=os.environ.get("HEADROOM_PROXY_URL", "http://127.0.0.1:8787"),
|
||||
)
|
||||
parser.add_argument("--transport", default="stdio")
|
||||
parser.add_argument("--host", default="127.0.0.1")
|
||||
parser.add_argument("--port", type=int, default=8788)
|
||||
parser.add_argument("--path", default="/mcp")
|
||||
parser.add_argument("--debug", action="store_true")
|
||||
parser.add_argument("--direct", action="store_true")
|
||||
args, unknown = parser.parse_known_args()
|
||||
if unknown:
|
||||
print(f"headroom-mcp-proxy: ignoring unsupported arguments: {unknown}", file=sys.stderr)
|
||||
if args.transport.lower() != "stdio":
|
||||
parser.error("only stdio transport is supported by the add-on adapter")
|
||||
return args
|
||||
|
||||
|
||||
def _number(data: dict[str, Any], key: str) -> int:
|
||||
value = data.get(key, 0)
|
||||
try:
|
||||
return int(value)
|
||||
except (TypeError, ValueError):
|
||||
return 0
|
||||
|
||||
|
||||
def make_proxy_compressor(proxy_url: str):
|
||||
endpoint = f"{proxy_url.rstrip('/')}/v1/compress"
|
||||
model = os.environ.get("HEADROOM_MCP_MODEL", "claude-sonnet-4-5-20250929")
|
||||
|
||||
def compress_via_proxy(_self, content: str) -> dict[str, Any]:
|
||||
# Imported here, not at module import, so MCP startup stays small. httpx is
|
||||
# already a dependency of Headroom's MCP server for retrieve/stats.
|
||||
import httpx
|
||||
|
||||
response = httpx.post(
|
||||
endpoint,
|
||||
json={
|
||||
"messages": [{"role": "tool", "content": content}],
|
||||
"model": model,
|
||||
},
|
||||
timeout=httpx.Timeout(180.0, connect=75.0),
|
||||
)
|
||||
response.raise_for_status()
|
||||
data = response.json()
|
||||
if not isinstance(data, dict):
|
||||
raise RuntimeError("Headroom proxy returned a non-object compression response")
|
||||
|
||||
messages = data.get("messages")
|
||||
compressed: Any = content
|
||||
if isinstance(messages, list) and messages:
|
||||
last = messages[-1]
|
||||
if isinstance(last, dict) and "content" in last:
|
||||
compressed = last["content"]
|
||||
if not isinstance(compressed, str):
|
||||
compressed = json.dumps(compressed, ensure_ascii=False)
|
||||
|
||||
hashes = data.get("ccr_hashes")
|
||||
hash_key = next((item for item in hashes if isinstance(item, str)), None) if isinstance(hashes, list) else None
|
||||
before = _number(data, "tokens_before")
|
||||
after = _number(data, "tokens_after")
|
||||
saved = _number(data, "tokens_saved")
|
||||
if saved <= 0:
|
||||
saved = max(0, before - after)
|
||||
savings_percent = round(saved / before * 100, 1) if before > 0 else 0.0
|
||||
transforms = data.get("transforms_applied")
|
||||
if not isinstance(transforms, list):
|
||||
transforms = []
|
||||
|
||||
note = "Compression was executed by the shared Headroom proxy runtime."
|
||||
if hash_key:
|
||||
note += (
|
||||
f" Original stored with hash={hash_key}. "
|
||||
"Use mcp__headroom__headroom_retrieve to recover it."
|
||||
)
|
||||
return {
|
||||
"compressed": compressed,
|
||||
"hash": hash_key,
|
||||
"original_tokens": before,
|
||||
"compressed_tokens": after,
|
||||
"tokens_saved": saved,
|
||||
"savings_percent": savings_percent,
|
||||
"transforms": transforms,
|
||||
"note": note,
|
||||
}
|
||||
|
||||
return compress_via_proxy
|
||||
|
||||
|
||||
async def run() -> None:
|
||||
args = parse_args()
|
||||
|
||||
# Defense in depth. The adapter never calls the local compressor, but keep
|
||||
# the MCP process explicitly unable to initialize Kompress if upstream code
|
||||
# changes or an unrelated import probes the compression pipeline.
|
||||
os.environ.pop("HF_HOME", None)
|
||||
os.environ["HEADROOM_DISABLE_KOMPRESS"] = "1"
|
||||
os.environ["HEADROOM_PROXY_URL"] = args.proxy_url
|
||||
|
||||
from headroom.ccr.mcp_server import HeadroomMCPServer
|
||||
|
||||
server = HeadroomMCPServer(proxy_url=args.proxy_url, check_proxy=True)
|
||||
server._compress_content = MethodType(make_proxy_compressor(args.proxy_url), server)
|
||||
try:
|
||||
await server.run_stdio()
|
||||
finally:
|
||||
await server.cleanup()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
asyncio.run(run())
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
@@ -1,388 +0,0 @@
|
||||
#!/lsiopy/bin/python3
|
||||
"""Lazy TCP gate for the Headroom HTTP proxy.
|
||||
|
||||
The gate remains resident on the public Headroom port while the heavy Headroom
|
||||
proxy (and its optional ONNX Kompress model) is started only for real traffic.
|
||||
After an idle period the backend process is terminated, releasing its Python,
|
||||
ONNX and model allocations. The next connection starts a fresh backend.
|
||||
|
||||
Only the Python standard library is imported here deliberately: the idle path
|
||||
must not import Headroom, ONNX Runtime, transformers or the MCP SDK.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import contextlib
|
||||
import json
|
||||
import os
|
||||
import signal
|
||||
import socket
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def _int_env(name: str, default: int, minimum: int = 1) -> int:
|
||||
try:
|
||||
value = int(os.environ.get(name, str(default)))
|
||||
except (TypeError, ValueError):
|
||||
value = default
|
||||
return max(minimum, value)
|
||||
|
||||
|
||||
GATE_HOST = os.environ.get("HEADROOM_GATE_HOST", "127.0.0.1")
|
||||
GATE_PORT = _int_env("HEADROOM_GATE_PORT", 8787)
|
||||
BACKEND_HOST = os.environ.get("HEADROOM_BACKEND_HOST", "127.0.0.1")
|
||||
BACKEND_PORT = _int_env("HEADROOM_BACKEND_PORT", 8789)
|
||||
HEADROOM_BIN = os.environ.get("HEADROOM_REAL_BIN", "/usr/bin/headroom")
|
||||
IDLE_TIMEOUT = _int_env("HEADROOM_IDLE_TIMEOUT_SECONDS", 900)
|
||||
START_TIMEOUT = _int_env("HEADROOM_START_TIMEOUT_SECONDS", 60)
|
||||
STOP_TIMEOUT = _int_env("HEADROOM_STOP_TIMEOUT_SECONDS", 10)
|
||||
HF_HOME = os.environ.get("HF_HOME", str(Path.home() / ".headroom/hf"))
|
||||
MAX_HEADER_BYTES = 128 * 1024
|
||||
COPY_CHUNK = 64 * 1024
|
||||
|
||||
|
||||
def log(message: str) -> None:
|
||||
stamp = time.strftime("%Y-%m-%d %H:%M:%S")
|
||||
print(f"[{stamp}] headroom-gate: {message}", file=sys.stderr, flush=True)
|
||||
|
||||
|
||||
class BackendUnavailable(RuntimeError):
|
||||
pass
|
||||
|
||||
|
||||
class ProxyGate:
|
||||
def __init__(self) -> None:
|
||||
self._lock = asyncio.Lock()
|
||||
self._process: asyncio.subprocess.Process | None = None
|
||||
self._last_activity = time.monotonic()
|
||||
self._active_connections = 0
|
||||
self._stopping = False
|
||||
self._watch_task: asyncio.Task[None] | None = None
|
||||
|
||||
@property
|
||||
def state(self) -> str:
|
||||
process = self._process
|
||||
if process is None:
|
||||
return "dormant"
|
||||
if process.returncode is None:
|
||||
return "running"
|
||||
return "stopped"
|
||||
|
||||
def touch(self) -> None:
|
||||
self._last_activity = time.monotonic()
|
||||
|
||||
async def _backend_healthy(self) -> bool:
|
||||
for path in ("/livez", "/health"):
|
||||
try:
|
||||
reader, writer = await asyncio.wait_for(
|
||||
asyncio.open_connection(BACKEND_HOST, BACKEND_PORT), timeout=1.0
|
||||
)
|
||||
request = (
|
||||
f"GET {path} HTTP/1.1\r\n"
|
||||
f"Host: {BACKEND_HOST}:{BACKEND_PORT}\r\n"
|
||||
"Connection: close\r\n\r\n"
|
||||
).encode()
|
||||
writer.write(request)
|
||||
await writer.drain()
|
||||
line = await asyncio.wait_for(reader.readline(), timeout=1.0)
|
||||
writer.close()
|
||||
with contextlib.suppress(Exception):
|
||||
await writer.wait_closed()
|
||||
if line.startswith(b"HTTP/") and b" 2" in line[:16]:
|
||||
return True
|
||||
except (OSError, asyncio.TimeoutError):
|
||||
continue
|
||||
return False
|
||||
|
||||
async def ensure_backend(self) -> None:
|
||||
if await self._backend_healthy():
|
||||
self.touch()
|
||||
return
|
||||
|
||||
async with self._lock:
|
||||
if await self._backend_healthy():
|
||||
self.touch()
|
||||
return
|
||||
|
||||
process = self._process
|
||||
if process is not None and process.returncode is None:
|
||||
await self._wait_until_healthy()
|
||||
self.touch()
|
||||
return
|
||||
|
||||
if not os.path.isfile(HEADROOM_BIN) or not os.access(HEADROOM_BIN, os.X_OK):
|
||||
raise BackendUnavailable(f"Headroom executable is unavailable: {HEADROOM_BIN}")
|
||||
|
||||
Path(HF_HOME).mkdir(parents=True, exist_ok=True)
|
||||
|
||||
environment = os.environ.copy()
|
||||
environment.update(
|
||||
{
|
||||
"HF_HOME": HF_HOME,
|
||||
"HEADROOM_PROXY_GATE": "1",
|
||||
}
|
||||
)
|
||||
command = [
|
||||
HEADROOM_BIN,
|
||||
"proxy",
|
||||
"--host",
|
||||
BACKEND_HOST,
|
||||
"--port",
|
||||
str(BACKEND_PORT),
|
||||
"--code-aware",
|
||||
]
|
||||
log(f"starting backend on {BACKEND_HOST}:{BACKEND_PORT}")
|
||||
try:
|
||||
self._process = await asyncio.create_subprocess_exec(
|
||||
*command,
|
||||
env=environment,
|
||||
start_new_session=True,
|
||||
)
|
||||
except Exception as exc:
|
||||
self._process = None
|
||||
raise BackendUnavailable(f"unable to start Headroom: {exc}") from exc
|
||||
|
||||
self._watch_task = asyncio.create_task(self._watch_backend(self._process))
|
||||
try:
|
||||
await self._wait_until_healthy()
|
||||
except Exception:
|
||||
await self._terminate_backend_locked("startup failure")
|
||||
raise
|
||||
self.touch()
|
||||
|
||||
async def _wait_until_healthy(self) -> None:
|
||||
deadline = time.monotonic() + START_TIMEOUT
|
||||
while time.monotonic() < deadline:
|
||||
process = self._process
|
||||
if process is not None and process.returncode is not None:
|
||||
raise BackendUnavailable(
|
||||
f"Headroom exited during startup with status {process.returncode}; "
|
||||
"see the add-on log"
|
||||
)
|
||||
if await self._backend_healthy():
|
||||
log("backend is ready")
|
||||
return
|
||||
await asyncio.sleep(0.25)
|
||||
raise BackendUnavailable(
|
||||
f"Headroom did not become ready within {START_TIMEOUT}s; see the add-on log"
|
||||
)
|
||||
|
||||
async def _watch_backend(self, process: asyncio.subprocess.Process) -> None:
|
||||
returncode = await process.wait()
|
||||
async with self._lock:
|
||||
was_current = self._process is process
|
||||
if was_current:
|
||||
self._process = None
|
||||
if not self._stopping and was_current:
|
||||
log(f"backend exited with status {returncode}")
|
||||
|
||||
async def stop_backend(self, reason: str) -> None:
|
||||
async with self._lock:
|
||||
await self._terminate_backend_locked(reason)
|
||||
|
||||
async def _terminate_backend_locked(self, reason: str) -> None:
|
||||
process = self._process
|
||||
if process is None:
|
||||
return
|
||||
if process.returncode is not None:
|
||||
self._process = None
|
||||
return
|
||||
|
||||
log(f"stopping backend ({reason})")
|
||||
try:
|
||||
os.killpg(process.pid, signal.SIGTERM)
|
||||
except ProcessLookupError:
|
||||
pass
|
||||
try:
|
||||
await asyncio.wait_for(process.wait(), timeout=STOP_TIMEOUT)
|
||||
except asyncio.TimeoutError:
|
||||
log("backend did not stop after SIGTERM; sending SIGKILL")
|
||||
with contextlib.suppress(ProcessLookupError):
|
||||
os.killpg(process.pid, signal.SIGKILL)
|
||||
with contextlib.suppress(Exception):
|
||||
await process.wait()
|
||||
self._process = None
|
||||
|
||||
async def idle_monitor(self) -> None:
|
||||
interval = max(5, min(30, IDLE_TIMEOUT // 4))
|
||||
while not self._stopping:
|
||||
await asyncio.sleep(interval)
|
||||
process = self._process
|
||||
idle_for = time.monotonic() - self._last_activity
|
||||
if (
|
||||
process is not None
|
||||
and process.returncode is None
|
||||
and idle_for >= IDLE_TIMEOUT
|
||||
):
|
||||
await self.stop_backend(f"idle for {int(idle_for)}s")
|
||||
|
||||
def status_payload(self) -> bytes:
|
||||
process = self._process
|
||||
payload = {
|
||||
"status": self.state,
|
||||
"backend_pid": process.pid if process is not None and process.returncode is None else None,
|
||||
"active_connections": self._active_connections,
|
||||
"idle_seconds": round(time.monotonic() - self._last_activity, 1),
|
||||
"idle_timeout_seconds": IDLE_TIMEOUT,
|
||||
"backend": f"{BACKEND_HOST}:{BACKEND_PORT}",
|
||||
}
|
||||
body = json.dumps(payload, separators=(",", ":")).encode()
|
||||
return (
|
||||
b"HTTP/1.1 200 OK\r\n"
|
||||
b"Content-Type: application/json\r\n"
|
||||
+ f"Content-Length: {len(body)}\r\n".encode()
|
||||
+ b"Connection: close\r\n\r\n"
|
||||
+ body
|
||||
)
|
||||
|
||||
async def handle_client(
|
||||
self,
|
||||
client_reader: asyncio.StreamReader,
|
||||
client_writer: asyncio.StreamWriter,
|
||||
) -> None:
|
||||
self._active_connections += 1
|
||||
real_traffic = False
|
||||
peer = client_writer.get_extra_info("peername")
|
||||
try:
|
||||
try:
|
||||
initial = await asyncio.wait_for(
|
||||
client_reader.readuntil(b"\r\n\r\n"), timeout=15.0
|
||||
)
|
||||
except (
|
||||
asyncio.IncompleteReadError,
|
||||
asyncio.LimitOverrunError,
|
||||
asyncio.TimeoutError,
|
||||
):
|
||||
return
|
||||
if len(initial) > MAX_HEADER_BYTES:
|
||||
await self._send_error(client_writer, 431, "request headers too large")
|
||||
return
|
||||
|
||||
first_line = initial.split(b"\r\n", 1)[0]
|
||||
parts = first_line.split(b" ")
|
||||
target = parts[1].split(b"?", 1)[0] if len(parts) >= 2 else b""
|
||||
if target == b"/gate/status":
|
||||
client_writer.write(self.status_payload())
|
||||
await client_writer.drain()
|
||||
return
|
||||
|
||||
real_traffic = True
|
||||
self.touch()
|
||||
try:
|
||||
await self.ensure_backend()
|
||||
backend_reader, backend_writer = await asyncio.wait_for(
|
||||
asyncio.open_connection(BACKEND_HOST, BACKEND_PORT), timeout=5.0
|
||||
)
|
||||
except (BackendUnavailable, OSError, asyncio.TimeoutError) as exc:
|
||||
log(f"backend unavailable for {peer}: {exc}")
|
||||
await self._send_error(client_writer, 503, str(exc))
|
||||
return
|
||||
|
||||
backend_writer.write(initial)
|
||||
await backend_writer.drain()
|
||||
self.touch()
|
||||
|
||||
upstream = asyncio.create_task(self._pipe(client_reader, backend_writer))
|
||||
downstream = asyncio.create_task(self._pipe(backend_reader, client_writer))
|
||||
done, pending = await asyncio.wait(
|
||||
{upstream, downstream}, return_when=asyncio.FIRST_COMPLETED
|
||||
)
|
||||
for task in pending:
|
||||
task.cancel()
|
||||
for task in done | pending:
|
||||
with contextlib.suppress(
|
||||
asyncio.CancelledError,
|
||||
ConnectionError,
|
||||
OSError,
|
||||
):
|
||||
await task
|
||||
backend_writer.close()
|
||||
with contextlib.suppress(Exception):
|
||||
await backend_writer.wait_closed()
|
||||
finally:
|
||||
self._active_connections = max(0, self._active_connections - 1)
|
||||
if real_traffic:
|
||||
self.touch()
|
||||
client_writer.close()
|
||||
with contextlib.suppress(Exception):
|
||||
await client_writer.wait_closed()
|
||||
|
||||
async def _pipe(
|
||||
self,
|
||||
reader: asyncio.StreamReader,
|
||||
writer: asyncio.StreamWriter,
|
||||
) -> None:
|
||||
while True:
|
||||
data = await reader.read(COPY_CHUNK)
|
||||
if not data:
|
||||
with contextlib.suppress(Exception):
|
||||
writer.write_eof()
|
||||
return
|
||||
writer.write(data)
|
||||
await writer.drain()
|
||||
self.touch()
|
||||
|
||||
@staticmethod
|
||||
async def _send_error(
|
||||
writer: asyncio.StreamWriter,
|
||||
status: int,
|
||||
detail: str,
|
||||
) -> None:
|
||||
reason = (
|
||||
"Service Unavailable"
|
||||
if status == 503
|
||||
else "Request Header Fields Too Large"
|
||||
)
|
||||
body = json.dumps({"error": detail}).encode()
|
||||
response = (
|
||||
f"HTTP/1.1 {status} {reason}\r\n"
|
||||
"Content-Type: application/json\r\n"
|
||||
f"Content-Length: {len(body)}\r\n"
|
||||
"Connection: close\r\n\r\n"
|
||||
).encode() + body
|
||||
writer.write(response)
|
||||
with contextlib.suppress(Exception):
|
||||
await writer.drain()
|
||||
|
||||
async def shutdown(self) -> None:
|
||||
self._stopping = True
|
||||
await self.stop_backend("gate shutdown")
|
||||
|
||||
|
||||
async def async_main() -> None:
|
||||
gate = ProxyGate()
|
||||
loop = asyncio.get_running_loop()
|
||||
stop_event = asyncio.Event()
|
||||
for sig in (signal.SIGTERM, signal.SIGINT):
|
||||
with contextlib.suppress(NotImplementedError):
|
||||
loop.add_signal_handler(sig, stop_event.set)
|
||||
|
||||
server = await asyncio.start_server(
|
||||
gate.handle_client,
|
||||
GATE_HOST,
|
||||
GATE_PORT,
|
||||
limit=MAX_HEADER_BYTES + 1,
|
||||
family=socket.AF_INET,
|
||||
)
|
||||
addresses = ", ".join(str(sock.getsockname()) for sock in server.sockets or [])
|
||||
log(
|
||||
f"listening on {addresses}; backend is lazy and stops after {IDLE_TIMEOUT}s idle; "
|
||||
"status endpoint: /gate/status"
|
||||
)
|
||||
monitor = asyncio.create_task(gate.idle_monitor())
|
||||
async with server:
|
||||
await stop_event.wait()
|
||||
monitor.cancel()
|
||||
with contextlib.suppress(asyncio.CancelledError):
|
||||
await monitor
|
||||
await gate.shutdown()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
asyncio.run(async_main())
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
|
Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 1.3 KiB |
@@ -1,3 +1,5 @@
|
||||
- Migrate legacy add-on configuration map names to current app configuration terminology.
|
||||
- Fix persistent data directory: the entrypoint now writes to the `/config` mount (the container-side path of the `app_config` map) instead of an unmounted `/app_configs/cleanuparr` path, so Cleanuparr's data survives container recreation.
|
||||
|
||||
## 2.10.2 (2026-08-01)
|
||||
- Update to latest version from Cleanuparr/Cleanuparr (changelog : https://github.com/Cleanuparr/Cleanuparr/releases)
|
||||
|
||||
@@ -63,7 +63,7 @@ COPY bashio-standalone.sh /usr/local/lib/bashio-standalone.sh
|
||||
RUN chmod 0755 /usr/local/lib/bashio-standalone.sh
|
||||
|
||||
# Bypass the original /entrypoint.sh (which mounts /config as VOLUME).
|
||||
# Our wrapper symlinks /app/config → HA addon_config and starts ./Cleanuparr directly.
|
||||
# Our wrapper symlinks /app/config → HA app_config and starts ./Cleanuparr directly.
|
||||
ENTRYPOINT ["/ha_entrypoint.sh"]
|
||||
CMD ["./Cleanuparr"]
|
||||
|
||||
|
||||
@@ -71,7 +71,7 @@ ingress_stream: true
|
||||
init: false
|
||||
panel_icon: mdi:movie-search
|
||||
map:
|
||||
- addon_config:rw
|
||||
- app_config:rw
|
||||
name: Cleanuparr
|
||||
options:
|
||||
env_vars: []
|
||||
|
||||
@@ -5,7 +5,7 @@ set -e
|
||||
###############################################################################
|
||||
# Home Assistant Addon entrypoint for Cleanuparr
|
||||
# The .NET app uses /app/config as its data directory.
|
||||
# We symlink /app/config → /addon_configs/cleanuparr (HA persistent storage)
|
||||
# We symlink /app/config → /config (the HA app_config mount, HA persistent storage)
|
||||
# and start ./Cleanuparr directly, bypassing the original /entrypoint.sh
|
||||
# which would trigger the /config Docker VOLUME mount.
|
||||
###############################################################################
|
||||
@@ -26,7 +26,7 @@ if [ -d /etc/cont-init.d ]; then
|
||||
fi
|
||||
|
||||
# ─── Setup persistent data directory ─────────────────────────────────────────
|
||||
HA_DATA_DIR="/addon_configs/cleanuparr"
|
||||
HA_DATA_DIR="/config"
|
||||
echo "[Cleanuparr] Setting up data directory: $HA_DATA_DIR"
|
||||
mkdir -p "$HA_DATA_DIR"
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 1.3 KiB |
@@ -1,3 +1,4 @@
|
||||
- Migrate legacy add-on configuration map names to current app configuration terminology.
|
||||
|
||||
## 19.19.1 (2026-06-17)
|
||||
- Update to latest version from coderaiser/cloudcmd (changelog : https://github.com/coderaiser/cloudcmd/releases)
|
||||
@@ -225,7 +226,7 @@
|
||||
## 16.17.7-3 (2023-11-19)
|
||||
|
||||
- Minor bugs fixed
|
||||
- MAJOR CHANGE : new HA config logic implemented. Files are now located in the addon config file, that can be accessed from the addon_configs folder from my cloudcommander or cloudcommander addons. Migration of data, custom configs, and custom scripts should be automatic. Please be sure to update all your links however ! For more information, see here : https://developers.home-assistant.io/blog/2023/11/06/public-addon-config/
|
||||
- MAJOR CHANGE : new HA config logic implemented. Files are now located in the addon config file, that can be accessed from the app_configs folder from my cloudcommander or cloudcommander addons. Migration of data, custom configs, and custom scripts should be automatic. Please be sure to update all your links however ! For more information, see here : https://developers.home-assistant.io/blog/2023/11/06/public-addon-config/
|
||||
- Homeassistant config accessible in /homeassistant folder ; all addons config in /addons_config ; this addon config in /config
|
||||
|
||||
## 16.17.7-2 (2023-10-30)
|
||||
|
||||
@@ -69,8 +69,8 @@ image: ghcr.io/alexbelgium/cloudcommander-{arch}
|
||||
ingress: true
|
||||
ingress_stream: true
|
||||
map:
|
||||
- addon_config:rw
|
||||
- all_addon_configs:rw
|
||||
- app_config:rw
|
||||
- all_app_configs:rw
|
||||
- homeassistant_config:rw
|
||||
- ssl:rw
|
||||
- share:rw
|
||||
|
||||
@@ -35,7 +35,7 @@ fi
|
||||
# Create symlinks with legacy folders
|
||||
if [ -d /homeassistant/addons_config ]; then
|
||||
ln -s /homeassistant/addons_config /config
|
||||
find /addon_configs/ -maxdepth 1 -mindepth 1 -type d -not -name "*cloudcommander*" -exec ln -s {} /config/addons_config/ \;
|
||||
find /app_configs/ -maxdepth 1 -mindepth 1 -type d -not -name "*cloudcommander*" -exec ln -s {} /config/addons_config/ \;
|
||||
fi
|
||||
if [ -d /homeassistant/addons_autoscripts ]; then
|
||||
ln -s /homeassistant/addons_autoscripts /config
|
||||
|
||||
|
Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 1.4 KiB |
@@ -1,3 +1,4 @@
|
||||
- Migrate legacy add-on configuration map names to current app configuration terminology.
|
||||
|
||||
## 2.2.5 (2026-08-02)
|
||||
- Update to latest version from ajslater/codex (changelog : https://github.com/ajslater/codex/releases)
|
||||
|
||||