Compare commits

..

12 Commits

Author SHA1 Message Date
copilot-swe-agent[bot]
fc33543d4b Merge remote-tracking branch 'origin/master' into create-pull-request/patch-1788655917
# Conflicts:
#	.claude/skills/hassio-addon-workflow/scripts/pr_review.sh

Co-authored-by: alexbelgium <44178713+alexbelgium@users.noreply.github.com>
2026-09-07 11:26:55 +00:00
Alexandre
bcde474c15 skill(hassio-addon-workflow): stop pr_review.sh watch reporting a false pass (#3052)
* skill(hassio-addon-workflow): stop pr_review.sh watch reporting a false pass

`gh pr checks` emits TAB-separated columns, but `watch` parsed it with awk's
default field splitting. Every check name containing a space was truncated to
its first word and the state column was never read:

    Codacy Static Code Analysis<TAB>fail   ->  Codacy=Static
    Addon linting (wger)<TAB>pass          ->  Addon=linting
    Test addon build (wger)<TAB>pending    ->  Test=addon

All three blocking gates have multi-word names, so the `case` matched neither
*fail* nor *pending* and fell through to "settled - all passing". That is a
false pass from the one command whose job is to report CI truthfully: #3044
was called green with Codacy red, and #3042 was called green while the HA
add-on linter was failing. A build that had not started would also have read
as a pass.

- parse with `awk -F'\t'`
- judge the state column alone, never the joined name=state text, so a check
  named e.g. `flaky-fail-detector` cannot read as a failure
- allowlist the good states (pass/skipping/pending) and treat anything
  unrecognised as a failure, so a new state cannot reach the passing branch
- name the checks that failed instead of only saying FAILURES
- keep waiting when only advisory checks have reported

Codacy is red on essentially every add-on PR here (#3019, #3044 and #3050 all
merged with it failing; master has no branch protection), so it is excluded
from the verdict but printed every poll and called out explicitly on settle.
Agreed with the maintainer. It is a denylist of known noise rather than an
allowlist of gates, so a job added to CI later counts as blocking by default.

Verified against real PRs: #3042 (blocking linter failure) now exits 1 and
names the check where it previously exited 0; #3018/#3019/#3044/#3050 report
correctly; pending, advisory-only, unknown-state and empty-output cases
checked against a stubbed gh. shellcheck clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* skill: record PR number in the traps entry

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* skill: address review — skipped-checks wording, document TSV contract

CodeRabbit (correct): the header claimed exit 0 means "every blocking check
passed", but a skipped gate also yields 0. A PR touching no add-on skips all
three gates, so that wording overstated what a 0 means. Reworded; the runtime
warning about skipped jobs was already there.

Copilot recommended switching to `gh pr checks --json`. Not applied: that flag
does not exist before gh 2.36 and 2.23 ships in this add-on, where it fails
with `unknown flag: --json`. Its premises are also wrong for the path the
script takes — piped output carries no header and uses real tabs; the aligned
ANSI table is the TTY renderer, which $(... | awk) never gets. Documented the
non-TTY contract and the gh-version constraint in the comment and traps.md so
this is not "corrected" back into a break later.

The underlying worry — a format change reintroducing a false pass — is already
answered by the allowlist design, now verified explicitly: a header row lands
in the failure branch (exit 1) and a space-aligned table parses to zero rows,
so watch keeps waiting. Neither can return 0.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: BirdNET-Go Addon Builder <addon-builder@users.noreply.github.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-07 13:23:28 +02:00
GitHub Actions
15b6736818 Revert "birdnet-go-dev: rebuild after upstream sync and conflict fixes in fork PRs #6 and #36"
This reverts commit 96526dc31f.
2026-09-07 08:42:55 +00:00
alexbelgium
96526dc31f birdnet-go-dev: rebuild after upstream sync and conflict fixes in fork PRs #6 and #36 2026-09-07 10:38:44 +02:00
Alexandre
89654e4f13 fix(omni-tools): let the add-on stop by backgrounding nginx and running the entrypoint as PID 1 (#3050)
The add-on could not be stopped: Home Assistant showed an Error status
after a few seconds and the container kept running and still served the
web UI (#3049).

cont-init.d/99-run.sh started nginx in the foreground -- '&>' is a
redirect, not a background operator -- and ha_entrypoint.sh runs every
cont-init.d script sequentially in the foreground. That script therefore
never returned, so the entrypoint never reached the code that installs
the terminate() handler forwarding SIGTERM to the application. The
reporter's log shows both halves of this: it prints 'Starting custom
scripts' and never reaches 'Everything started!'.

The add-on also shipped no 'init:' key, so Supervisor's default of true
made Docker inject its own init as PID 1 and left ha_entrypoint.sh as
PID 2, where the 'if $PID1' block holding the trap is skipped outright.

Background the launch and set init: false. The script then returns, the
entrypoint installs its trap, and nginx -- orphaned by the exiting
script -- is reparented to the entrypoint as PID 1, where terminate()'s
'pgrep -P $$' finds it and signals it directly.

Backgrounding from cont-init.d is what 24 other add-ons here already do
(autobrr runs a bare 'nginx &'). Moving the launch to services.d was
considered and rejected: ha_entrypoint.sh runs each services.d/*/run
inside a restart subshell, so the application ends up a grandchild of
PID 1 while terminate() enumerates direct children only. Reproduced --
the app survives that path unsignalled -- and it is the larger change.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-07 10:25:33 +02:00
github-actions
607487cfb8 Github bot : image compressed 2026-09-06 23:12:31 +00:00
github-actions[bot]
6c5e2c171a Update stargazer map & cache 2026-09-06 00:55:35 +00:00
alexbelgium
df62d357d8 fix: auto-fix linting issues 2026-09-06 00:51:57 +00:00
GitHub Actions
c6a55ea5f4 Revert "fix(linkwarden): stop installing packages Debian 11 no longer serves (#3047)"
This reverts commit 296bb3767a.
2026-09-05 05:51:42 +00:00
Alexandre
a237a6820c fix(wger): set the database env vars the upstream image stopped shipping (#3044)
* fix(wger): set the database env vars the upstream image stopped shipping

wger/server:latest no longer defines DJANGO_DB_ENGINE or DJANGO_DB_DATABASE in
its image environment, and upstream settings/main.py reads both with no
fallback. Every fresh install therefore died at startup with
"ImproperlyConfigured: Set the DJANGO_DB_ENGINE environment variable".

Set both explicitly in the Dockerfile, pointing at the sqlite database in
/data/database.sqlite that the add-on already persists, and add
DJANGO_PERFORM_MIGRATIONS=True so an existing database picks up new migrations
when the image is rebuilt against a newer upstream release.

With the path now set through the environment, the cont-init rewrite of the
database path in the Python settings is dead code — upstream no longer
hardcodes /home/wger/db/database.sqlite anywhere, so it only logged a warning.

Also move the add-on to the addon_configs location, as the issue asks: the
shared 01-config_yaml.sh template migrates an existing
/homeassistant/addons_config/wger/config.yaml on the first start.

Fixes #3043

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs(wger): use the ISO date format the rest of this CHANGELOG uses

The 2.6.4 heading was written 04-09-2026 while every other dated heading
in this file, and 22937 of the 23999 dated headings in the repo, use ISO
YYYY-MM-DD. Copilot flagged the inconsistency on #3044.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-05 07:49:00 +02:00
Alexandre
296bb3767a fix(linkwarden): stop installing packages Debian 11 no longer serves (#3047)
* fix(linkwarden): stop installing packages Debian 11 no longer serves

The 2.16.2 updater build failed in the first RUN layer:

  E: Failed to fetch .../sudo_1.9.5p2-3%2bdeb11u4_amd64.deb  404  Not Found
  E: Failed to fetch .../vim-runtime_8.2.2434-3%2bdeb11u3_all.deb  404  Not Found

Debian 11 reached LTS end on 2026-08-31. Its bullseye-security index is frozen
at that date and still lists debs that deb.debian.org no longer serves; sudo is
one of them and still 404s on every deb.debian.org edge checked today, so the
build fails deterministically rather than transiently. Every package in
postgresql-16's own dependency chain that comes from bullseye-security was
checked and does fetch, so removing this first install unblocks the build.

None of the four packages is needed:

  - vim was never used by the add-on.
  - gnupg2 was only there for "gpg --dearmor"; apt reads the ASCII-armoured key
    from /etc/apt/trusted.gpg.d/postgresql.asc directly.
  - lsb-release was only there for "lsb_release -cs"; /etc/os-release carries
    VERSION_CODENAME.
  - sudo is replaced by su in the Postgres bootstrap, which is what the ente and
    postgres_15 add-ons already use for the same job.

curl is already present in the upstream linkwarden image, so no install step is
needed before the PGDG repository is configured.

The su rewrite keeps the argv psql receives identical. Because "su -" starts a
login shell, the service call now uses an absolute path (the login PATH has no
/usr/sbin) and the bootstrap SQL is written to and read from /tmp rather than
the script's working directory.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(linkwarden): fetch bullseye-security from its origin, not the CDN

Dropping vim/gnupg2/lsb-release/sudo got the build past the first RUN, but
"apt-get install -y postgresql-16" then 404'd on its own dependencies, on arm64:

  E: Failed to fetch .../glibc/libc-l10n_2.31-13%2bdeb11u14_all.deb          404
  E: Failed to fetch .../exim4/exim4-base_4.94.2-7%2bdeb11u6_arm64.deb       404
  E: Failed to fetch .../python3.9/libpython3.9-minimal_3.9.2-1%2bdeb11u7_arm64.deb  404

All three are 200 on security.debian.org, the origin that deb.debian.org is a
CDN alias for. The rot is per-file and moves: exim4-base was 404 during the
build and 200 minutes later, so retrying is a coin flip rather than a fix.

Rewrite the security suite in /etc/apt/sources.list to security.debian.org
before "apt-get update". The main suite is left on the CDN; it is intact, and
bullseye main is already on archive.debian.org whereas bullseye-security is not.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(linkwarden): feed the bootstrap SQL on stdin, and fix two comments

Review follow-up on the temp file, the Dockerfile comment and the CHANGELOG
wording.

The bootstrap SQL no longer goes through a file at all. Both reviewers objected
to the predictable root-written /tmp path; passing the statements to psql on
stdin removes the file rather than defending it, and is less code than either
the version being reviewed or the suggested mktemp. It also restores what the
original did before this branch: sudo ran "cat file | psql", so psql read the
statements from stdin then too.

The Dockerfile comment said "PGDATA repository" where it meant the PGDG apt
repository; PGDATA is the data-directory env var set two lines above, so the
wording was actively misleading.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* chore: record the shipped upstream release in updater.json

Each PR publishes an upstream version the updater bot had already selected
before CI reverted its commit, but updater.json still recorded the previous one.
The updater reads upstream_version as CURRENT and enters its update path
whenever it differs from the latest tag, so its next run would process the same
release again and derive a synthetic trailing-.1 version, producing a redundant
release, a duplicate CHANGELOG entry and a wasted build.

These values are exactly what the bot itself wrote in the reverted commit; this
restores its own record for a release now being shipped rather than choosing a
new one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(linkwarden): keep the Postgres password out of process arguments

CodeRabbit flagged that the password appears in the command string of the
password-setting call, and that the database-creation call reaches Postgres over
a TCP URI carrying the same password with sslmode=prefer. Both predate this
branch, but both lines are touched here.

Sending each statement to psql on stdin removes the password and the URI from
argv, and is shorter than either form it replaces: the escaped-quote nesting on
the ALTER USER call disappears with it.

The connection method is unchanged for the ALTER USER call, which already went
over the local socket as the postgres user. The database-creation call moves
from TCP to that same socket. This is safe by construction rather than by
assumption: the ALTER USER call runs first under "set -e" with no "|| true", so
the container cannot reach the second call unless socket access already worked.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-05 07:47:54 +02:00
Alexandre
62bbecb465 fix(mealie): build the frontend with pnpm and the upstream lockfile (#3046)
* fix(mealie): build the frontend with pnpm and the upstream lockfile

Upstream mealie migrated frontend/ from yarn to pnpm in v3.24.0 and deleted
frontend/yarn.lock. Our builder stage kept running "yarn install
--frozen-lockfile", which silently degraded to a fresh, unpinned resolution of
every dependency. That worked until a newer vuetify 4.x release dropped the
"vuetify/labs/rules" entry point, at which point "nuxt generate" failed with:

  Rolldown failed to resolve import "vuetify/labs/rules" from
  "virtual:nuxt:.nuxt%2Fvuetify-nuxt-plugin.client.mjs"

and the v3.25.1 updater build was reverted.

Mirror upstream's docker/Dockerfile frontend stage instead: node:24, a global
pnpm@11, and "pnpm install --frozen-lockfile" against the committed
pnpm-lock.yaml, so the dependency set is the one upstream tests. Also copy the
frontend tree with "cp -a frontend/." so dotfiles such as .nuxtignore come
across, and shallow-clone the tag.

Bumps the add-on to v3.25.1, the version the updater bot could not build.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* chore: record the shipped upstream release in updater.json

Each PR publishes an upstream version the updater bot had already selected
before CI reverted its commit, but updater.json still recorded the previous one.
The updater reads upstream_version as CURRENT and enters its update path
whenever it differs from the latest tag, so its next run would process the same
release again and derive a synthetic trailing-.1 version, producing a redundant
release, a duplicate CHANGELOG entry and a wasted build.

These values are exactly what the bot itself wrote in the reverted commit; this
restores its own record for a release now being shipped rather than choosing a
new one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-05 07:47:32 +02:00
441 changed files with 2802 additions and 2601 deletions

View File

@@ -254,13 +254,38 @@ without your involvement — another reason a shared checkout goes stale mid-tas
**Reviewers**: CodeRabbit (deepest — often runs scripts to prove a claim; reviews ~9 minutes
after the PR opens, or on `@coderabbitai review`), chatgpt-codex-connector, Copilot, Codacy.
**Codacy `action_required` is this repo's normal state.** Other open PRs show the same. It
exposes no annotations via the API, so its findings are only visible in the maintainer's Codacy
account. Note it and move on rather than guessing.
**Codacy is red on essentially every add-on PR and gates nothing.** `gh pr checks` reports it as
`fail` (older runs showed `action_required`); #3019, #3044 and #3050 all merged with it failing,
and `master` carries no branch protection, so no check is required in the GitHub sense. It exposes
no annotations via the API, so its findings are only visible in the maintainer's Codacy account.
Note it and move on rather than guessing. `pr_review.sh watch` therefore prints it every poll but
keeps it out of the verdict — the one check on that list, which is a denylist of known noise, not
an allowlist of gates, so a job added to CI later counts as blocking until someone exempts it.
**Resolving a review thread requires GraphQL** (`resolveReviewThread`); the REST API cannot do it.
`scripts/pr_review.sh` wraps fetch / reply / resolve.
**`gh pr checks` output is TAB-separated, and every blocking gate here has spaces in its name.**
Parsing it with awk's default field splitting truncates each check to its first word and reads the
wrong column as the state: `Codacy Static Code Analysis<TAB>fail` becomes `Codacy=Static`, and
`Test addon build (wger)<TAB>pending` becomes `Test=addon`. A `case` over that string then matches
neither `*fail*` nor `*pending*` and falls through to the "all passing" branch — the failure mode
that makes a CI-reporting command lie. `pr_review.sh watch` called #3044 green while Codacy was
red, and on #3042 printed "settled — all passing" while the HA add-on linter was failing; it would
also have called a build that had not started a pass. Use `awk -F'\t'`, judge the state column
alone (never the joined `name=state` text, or a check named `flaky-fail-detector` reads as a
failure), and treat an unrecognised state as a failure instead of letting it reach the passing
branch. Fixed in #3052.
The TSV is gh's *non-TTY* renderer, which is what `$(gh pr checks ... | awk)` always gets; attached
to a terminal the same command prints a coloured, aligned table with a summary line, so never
sanity-check the format by eye in a shell and assume the script sees that. `gh pr checks --json`
would be sturdier, and Copilot recommends it (#3052), but it does not exist before gh 2.36 and the
add-on ships 2.23 — it fails with `unknown flag: --json`. The parse is therefore built to fail
safe instead: states are allowlisted, so a header row would land in the failure branch and a
space-aligned table would parse to zero rows and keep `watch` waiting. Either way it cannot
return a false pass.
**CHANGELOG heading dates are ISO, whatever the bots' defaults say.** Match the format already in
the add-on's file. Repo-wide that is `## <version> (YYYY-MM-DD)`: 7705 dated headings against 363
in `DD-MM-YYYY`, and the newest entry is ISO in 125 of 135 add-ons. Copilot flags an ISO file that

View File

@@ -30,8 +30,14 @@ VAR="${1:?usage: env_trace.sh <VAR> [process-name-or-pid]}"
TARGET="${2:-}"
# VAR is interpolated into grep/sed patterns below — restrict it to a valid env var name
case "$VAR" in
[A-Za-z_]*) [ -z "${VAR//[A-Za-z0-9_]/}" ] || { echo "invalid env var name: $VAR" >&2; exit 1; } ;;
*) echo "invalid env var name: $VAR" >&2; exit 1 ;;
[A-Za-z_]*) [ -z "${VAR//[A-Za-z0-9_]/}" ] || {
echo "invalid env var name: $VAR" >&2
exit 1
} ;;
*)
echo "invalid env var name: $VAR" >&2
exit 1
;;
esac
echo "== tracing ${VAR} =="
@@ -39,7 +45,7 @@ echo
echo "1. /data/options.json (the user's saved options)"
if [ -f /data/options.json ]; then
python3 - "$VAR" <<'PY'
python3 - "$VAR" << 'PY'
import json, sys
var = sys.argv[1]
try:
@@ -80,7 +86,8 @@ echo "3. s6 container_environment (only read by services using #!/usr/bin/with-c
seen3=0
for d in /var/run/s6/container_environment /run/s6/container_environment; do
if [ -f "$d/$VAR" ]; then
echo " $d/$VAR = [$(cat "$d/$VAR")]"; seen3=1
echo " $d/$VAR = [$(cat "$d/$VAR")]"
seen3=1
fi
done
[ "$seen3" -eq 0 ] && echo " not present in either envdir"
@@ -122,8 +129,8 @@ else
done
fi
# What it was actually launched with beats any theory about its environment.
tr '\0' '\n' < "/proc/$pid/cmdline" 2> /dev/null | tail -n +2 |
grep -iE "res|screen|${VAR}" | head -3 | sed 's/^/ argv: /'
tr '\0' '\n' < "/proc/$pid/cmdline" 2> /dev/null | tail -n +2 \
| grep -iE "res|screen|${VAR}" | head -3 | sed 's/^/ argv: /'
done
fi
fi

View File

@@ -31,7 +31,7 @@ awk '{t+=$2} END{printf " threads=%d\n", t}' <(ps -eo pid,nlwp --no-headers 2>
echo
echo "== per-process memory (top 20 by PSS) =="
printf ' %-28s %8s %8s %8s\n' COMMAND RSS PSS PRIVATE
python3 - "$OUT" <<'PY'
python3 - "$OUT" << 'PY'
import os, sys
rows = []
for pid in filter(str.isdigit, os.listdir('/proc')):
@@ -62,17 +62,17 @@ PY
echo
echo "== reserved-but-not-resident (lazy allocations, NOT leaks) =="
ipcs -m 2>/dev/null | awk 'NR>3 && $5 ~ /^[0-9]+$/ && $5 > 50000000 {printf " SysV shm %.0f MB (owner %s) — check Rss in /proc/<pid>/smaps before calling it used\n", $5/1048576, $3}'
ipcs -m 2> /dev/null | awk 'NR>3 && $5 ~ /^[0-9]+$/ && $5 > 50000000 {printf " SysV shm %.0f MB (owner %s) — check Rss in /proc/<pid>/smaps before calling it used\n", $5/1048576, $3}'
echo
echo "== CPU over ${SAMPLE}s (idle unless you are driving the UI) =="
# utime+stime. Parsed after the LAST ')' because field 2 is (comm) and may contain spaces —
# a plain $14+$15 is wrong for anything like 'npm exec @foo' and silently reports a fabricated
# number rather than failing.
jiffies() { awk -F') ' '{n=split($NF,a," "); print a[12]+a[13]}' "/proc/$1/stat" 2>/dev/null; }
jiffies() { awk -F') ' '{n=split($NF,a," "); print a[12]+a[13]}' "/proc/$1/stat" 2> /dev/null; }
# jiffies are USER_HZ units — almost always 100, but read it rather than assume it
HZ=$(getconf CLK_TCK 2>/dev/null) && [ "$HZ" -gt 0 ] 2>/dev/null || HZ=100
HZ=$(getconf CLK_TCK 2> /dev/null) && [ "$HZ" -gt 0 ] 2> /dev/null || HZ=100
# Sample EVERY readable process, not the top-N of ps.txt: that list is sorted by RSS,
# and the busiest process is not necessarily a big one.
@@ -86,14 +86,14 @@ for pid in "${!t0[@]}"; do
[ -r "/proc/$pid/stat" ] || continue
t1=$(jiffies "$pid") || continue
[ -n "$t1" ] && [ -n "${t0[$pid]}" ] || continue
delta=$(( t1 - ${t0[$pid]} ))
delta=$((t1 - ${t0[$pid]}))
[ "$delta" -gt 0 ] || continue
pct=$(awk -v d="$delta" -v s="$SAMPLE" -v hz="$HZ" 'BEGIN{printf "%.2f", d*100/(hz*s)}')
comm=$(tr -d '\0' < "/proc/$pid/comm" 2>/dev/null)
comm=$(tr -d '\0' < "/proc/$pid/comm" 2> /dev/null)
echo "$pct $pid $comm"
done | sort -rn | head -12 | awk '{printf " %6s%% %-8s %s\n", $1, $2, $3}'
echo
echo " established conns on :8082/:3000/:3001 = $(ss -tn 2>/dev/null | grep -cE 'ESTAB.*:(8082|3000|3001)')"
echo " established conns on :8082/:3000/:3001 = $(ss -tn 2> /dev/null | grep -cE 'ESTAB.*:(8082|3000|3001)')"
echo " (those are claude_desktop/webtop viewer ports; 0 here means CPU above is idle burn)"
echo " raw ps: $OUT/ps.txt"

View File

@@ -8,6 +8,10 @@
# pr_review.sh resolve <PR> <THREAD_ID...|--all> --all = every unresolved, asks first
# pr_review.sh watch <PR> [minutes] poll checks (run this backgrounded)
#
# watch exits 0 when every blocking check passed *or was skipped* — a PR touching no add-on
# skips all three gates, and it says so — 1 on failure, 2 if it ran out of minutes. Codacy is
# advisory here: printed every poll, excluded from the verdict.
#
# Reviewers seen here: coderabbitai (deepest; reviews ~9 min after open, or on
# "@coderabbitai review"), chatgpt-codex-connector, Copilot, Codacy.
#
@@ -21,7 +25,7 @@ REPO="${HASSIO_REPO:-}"
[ -z "$REPO" ] && { echo "cannot determine repo; set HASSIO_REPO=owner/name" >&2; exit 1; }
echo "repo: $REPO" >&2
CMD="${1:-}"; PR="${2:-}"
[ -z "$CMD" ] || [ -z "$PR" ] && { sed -n '2,16p' "$0" | sed 's/^# \?//'; exit 1; }
[ -z "$CMD" ] || [ -z "$PR" ] && { sed -n '2,20p' "$0" | sed 's/^# \?//'; exit 1; }
case "$CMD" in
list)
@@ -90,25 +94,72 @@ resolve)
;;
watch)
MINS="${3:-180}" # the addon build alone has taken ~3h; 20 was far too short
# Checks that are red on essentially every add-on PR here and gate nothing: master carries no
# branch protection, and #3019, #3044 and #3050 all merged with Codacy failing. They are kept
# out of the verdict but always printed, so the reader still sees them and can judge. This is
# deliberately a denylist of known noise, not an allowlist of blocking checks — a job added to
# CI later counts as blocking until someone puts it here on purpose.
ADVISORY_CHECKS="Codacy Static Code Analysis" # one per line if more are ever added
wfail=2 # not 0: running out of minutes with checks still pending is not a pass
c=""; bstates=""; adv=""
for i in $(seq 1 "$MINS"); do
c=$(gh pr checks "$PR" 2> /dev/null | awk '{print $1"="$2}' | tr '\n' ' ')
if [ -z "$c" ]; then
# gh pr checks emits TAB-separated columns with no header when its output is not a TTY,
# which inside this $(... | awk) it never is. (Attached to a terminal it prints a wholly
# different ANSI table; --json would be sturdier still but does not exist before gh 2.36,
# and 2.23 ships here.) Every blocking gate has spaces in its name — "Addon linting
# (wger)", "Test addon build (wger)" — so awk's default separator split them on
# whitespace: "Codacy Static Code Analysis<TAB>fail" became "Codacy=Static" and the state
# column was never read at all. watch printed "all passing" on a red #3044 and on #3042
# with the linter failing, and could not see a pending build either.
# If that format ever does change, the allowlist below fails safe rather than passing: a
# header row lands in the failure branch, and a space-aligned table parses to no rows,
# which keeps watch waiting instead of returning 0.
rows=$(gh pr checks "$PR" 2> /dev/null | awk -F'\t' -v ADV="$ADVISORY_CHECKS" '
BEGIN { n = split(ADV, a, "\n"); for (j = 1; j <= n; j++) adv[a[j]] = 1 }
NF >= 2 { print (($1 in adv) ? "A" : "B") "\t" $1 "=" $2 "\t" $2 }')
if [ -z "$rows" ]; then
# Normal in the first minutes after `gh pr create`, and also whenever gh errors.
# Calling that "settled" would report success for checks that never ran.
echo "[$i] no checks reported yet (gh returned nothing) — still waiting"
sleep 60; continue
fi
c=$(printf '%s\n' "$rows" | cut -f2 | tr '\n' ' ')
echo "[$i] $c"
case "$c" in
*pending*) sleep 60 ;;
*fail* | *error* | *cancel*) echo "settled — with FAILURES (see above)"; wfail=1; break ;;
*) echo "settled — all passing"; wfail=0; break ;;
esac
# Judge the state column only, never the joined name=state line: a check whose NAME
# contains "fail" must not read as a failure.
bstates=$(printf '%s\n' "$rows" | awk -F'\t' '$1 == "B" { print $3 }' | tr '\n' ' ')
adv=$(printf '%s\n' "$rows" | awk -F'\t' '$1 == "A" { print $2 }' | tr '\n' ' ')
if [ -z "$bstates" ]; then
echo " only advisory checks have reported — no blocking check has run yet"
sleep 60; continue
fi
# Allowlist the good states rather than denylisting the bad ones: an unrecognised state
# must land in the failure branch, because falling through to "passing" is this command's
# worst outcome.
nbad=0; npend=0
for s in $bstates; do
case "$s" in
pass | skipping) ;;
pending) npend=$((npend + 1)) ;;
*) nbad=$((nbad + 1)) ;;
esac
done
if [ "$nbad" -gt 0 ]; then
echo "settled — blocking checks FAILED:"
printf '%s\n' "$rows" |
awk -F'\t' '$1 == "B" && $3 != "pass" && $3 != "skipping" && $3 != "pending" { print " " $2 }'
wfail=1; break
elif [ "$npend" -gt 0 ]; then
sleep 60; continue
else
echo "settled — blocking checks passing"; wfail=0; break
fi
done
[ "$wfail" -eq 2 ] && echo "gave up after ${MINS}m, checks still unsettled — NOT a pass"
# Printed on pass and on failure alike: it is excluded from the verdict, not hidden.
[ -n "$adv" ] && echo " advisory (non-blocking, not counted in the verdict): $adv"
# A PR touching no */config.* skips the CHANGELOG, linter and build jobs outright (#3018).
case "${c:-}" in *skipping*) echo " ...of which some were SKIPPED — a skipped job tested nothing" ;; esac
case " $bstates " in *" skipping "*) echo " ...of which some were SKIPPED — a skipped job tested nothing" ;; esac
echo "note: long queues here are usually account runner contention, not your diff."
exit "$wfail"
;;

View File

@@ -47,7 +47,10 @@ if [ -n "${BUILD_VERSION:-}" ]; then
if [ -z "$SLUG" ] && [ -n "${HOSTNAME:-}" ]; then
base=$(printf '%s' "$HOSTNAME" | sed 's/^[0-9a-f]\{8\}-//')
for cand in "$(printf '%s' "$base" | tr '-' '_')" "$base"; do
[ -f "$REPO/$cand/config.yaml" ] && { SLUG="$cand"; break; }
[ -f "$REPO/$cand/config.yaml" ] && {
SLUG="$cand"
break
}
done
[ -z "$SLUG" ] && SLUG="$base"
fi
@@ -69,8 +72,8 @@ if [ -n "${BUILD_VERSION:-}" ]; then
else
echo " MISMATCH — this branch is NOT what is running."
git fetch origin master --quiet 2> /dev/null
master=$(git show origin/master:"$SLUG/config.yaml" 2> /dev/null |
grep -E '^version:' | head -1 | tr -d "\"'" | awk '{print $2}')
master=$(git show origin/master:"$SLUG/config.yaml" 2> /dev/null \
| grep -E '^version:' | head -1 | tr -d "\"'" | awk '{print $2}')
echo " origin/master version = ${master:-unknown}"
echo " -> work from origin/master; analysing this branch will mislead you."
echo

View File

@@ -13,22 +13,31 @@ set -uo pipefail
if root=$(git rev-parse --show-toplevel 2> /dev/null); then cd "$root" || exit 1; fi
ADDON="${1:-}"
[ "${ADDON:-}" = "--vs-master" ] && { ADDON=""; set -- --vs-master; }
[ "${ADDON:-}" = "--vs-master" ] && {
ADDON=""
set -- --vs-master
}
VS_MASTER=false
for a in "$@"; do [ "$a" = "--vs-master" ] && VS_MASTER=true; done
if [ -z "$ADDON" ]; then
mapfile -t _dirs < <(git diff --name-only origin/master...HEAD 2> /dev/null |
cut -d/ -f1 | sort -u | grep -vE '^\.' )
mapfile -t _dirs < <(git diff --name-only origin/master...HEAD 2> /dev/null \
| cut -d/ -f1 | sort -u | grep -vE '^\.')
if [ "${#_dirs[@]}" -gt 1 ]; then
echo "several changed dirs: ${_dirs[*]}"
echo "pass one explicitly: validate.sh <addon-dir>"; exit 1
echo "pass one explicitly: validate.sh <addon-dir>"
exit 1
fi
ADDON="${_dirs[0]:-}"
fi
[ -z "$ADDON" ] && { echo "usage: validate.sh <addon-dir> [--vs-master]"; exit 1; }
[ -z "$ADDON" ] && {
echo "usage: validate.sh <addon-dir> [--vs-master]"
exit 1
}
git rev-parse --verify origin/master > /dev/null 2>&1 || {
echo "origin/master missing — run: git fetch origin master"; exit 1; }
echo "origin/master missing — run: git fetch origin master"
exit 1
}
export PYTHONDONTWRITEBYTECODE=1
echo "== validating $ADDON =="
@@ -36,14 +45,22 @@ fail=0
note() { printf ' %-13s %s\n' "$1" "$2"; }
# execline `run`/`finish` files are not shell (25 of them here, across 21 add-ons). Neither
# linter below can read one, so anything either says about it is noise.
is_execline() { local l; IFS= read -r l < "$1" 2> /dev/null; [[ $l == '#!'*execlineb* ]]; }
is_execline() {
local l
IFS= read -r l < "$1" 2> /dev/null
[[ $l == '#!'*execlineb* ]]
}
# Shell: bash -n then shellcheck -x (follows sourced files, as CI does). One list for both.
files=()
while IFS= read -r f; do is_execline "$f" || files+=("$f"); done \
< <(find "$ADDON" -type f \( -name '*.sh' -o -name 'run' -o -name 'finish' -o -name 'autostart' \) 2> /dev/null)
for f in "${files[@]}"; do
if ! out=$(bash -n "$f" 2>&1); then note "bash -n" "FAIL $f"; echo "$out" | sed 's/^/ /'; fail=1; fi
if ! out=$(bash -n "$f" 2>&1); then
note "bash -n" "FAIL $f"
echo "$out" | sed 's/^/ /'
fail=1
fi
done
[ "$fail" -eq 0 ] && note "bash -n" "${#files[@]} file(s) checked"
@@ -65,13 +82,16 @@ command -v hadolint > /dev/null 2>&1 && [ -f "$ADDON/Dockerfile" ] && {
if [ -f "$ADDON/config.yaml" ]; then
# path passed as argv, never interpolated into Python source
python3 - "$ADDON/config.yaml" <<'PY' || { note "config.yaml" "FAIL parse"; fail=1; }
python3 - "$ADDON/config.yaml" << 'PY' || {
import yaml,sys
d=yaml.safe_load(open(sys.argv[1]))
print(' %-13s ok (version=%s, %d options)' % ('config.yaml', d.get('version'), len(d.get('options') or {})))
missing=[k for k in (d.get('options') or {}) if k not in (d.get('schema') or {})]
if missing: print(' %-13s options with no schema entry: %s' % ('WARN', missing)); sys.exit(0)
PY
note "config.yaml" "FAIL parse"
fail=1
}
command -v yamllint > /dev/null 2>&1 && {
yl=$(yamllint -f parsable "$ADDON/config.yaml" 2>&1 | grep -c .)
note "yamllint" "$yl finding(s) (compare with --vs-master)"
@@ -79,7 +99,10 @@ PY
fi
while IFS= read -r f; do
python3 -m py_compile "$f" 2> /dev/null || { note "py_compile" "FAIL $f"; fail=1; }
python3 -m py_compile "$f" 2> /dev/null || {
note "py_compile" "FAIL $f"
fail=1
}
done < <(find "$ADDON" -type f -name '*.py' 2> /dev/null)
$VS_MASTER && command -v npx > /dev/null 2>&1 && [ -f "$ADDON/CHANGELOG.md" ] && {
@@ -96,7 +119,8 @@ if git diff --name-only origin/master...HEAD 2> /dev/null | grep -Fxq "$ADDON/CH
note "CHANGELOG" "updated"
else
# This one IS gated: onpr_check-pr.yaml exits 1 without it.
note "CHANGELOG" "NOT UPDATED for $ADDON — CI hard-gates this"; fail=1
note "CHANGELOG" "NOT UPDATED for $ADDON — CI hard-gates this"
fail=1
fi
if git diff origin/master...HEAD -- "$ADDON/config.yaml" 2> /dev/null | grep -q '^+version:'; then
note "version" "bumped"
@@ -110,7 +134,8 @@ note "docker build" "NOT tested locally (dockerd unavailable) — CI is the only
if $VS_MASTER; then
echo
echo "== findings ADDED by this diff (pre-existing ones filtered out) =="
tmp=$(mktemp -d); trap 'rm -rf "$tmp"' EXIT
tmp=$(mktemp -d)
trap 'rm -rf "$tmp"' EXIT
added=0
# Fed by process substitution, not a pipe: a pipeline runs this in a subshell, where the
# findings below could never reach $fail and the verdict would contradict the list.
@@ -125,14 +150,26 @@ if $VS_MASTER; then
# cancel it out and report a false clean.
case "$f" in
*.sh | *autostart | */run | */finish)
command -v shellcheck > /dev/null 2>&1 || { echo " $f: SKIPPED (shellcheck not installed)"; continue; }
cmd() { shellcheck -x -f gcc "$1" 2>&1 | sed 's/^[^:]*:[0-9]*:[0-9]*://'; } ;;
command -v shellcheck > /dev/null 2>&1 || {
echo " $f: SKIPPED (shellcheck not installed)"
continue
}
cmd() { shellcheck -x -f gcc "$1" 2>&1 | sed 's/^[^:]*:[0-9]*:[0-9]*://'; }
;;
*.yaml | *.yml)
command -v yamllint > /dev/null 2>&1 || { echo " $f: SKIPPED (yamllint not installed)"; continue; }
cmd() { yamllint -f parsable "$1" 2>&1 | sed 's/^[^:]*//; s/^:[0-9]*:[0-9]*//'; } ;;
command -v yamllint > /dev/null 2>&1 || {
echo " $f: SKIPPED (yamllint not installed)"
continue
}
cmd() { yamllint -f parsable "$1" 2>&1 | sed 's/^[^:]*//; s/^:[0-9]*:[0-9]*//'; }
;;
*Dockerfile)
command -v hadolint > /dev/null 2>&1 || { echo " $f: SKIPPED (hadolint not installed)"; continue; }
cmd() { hadolint "$1" 2>&1 | sed 's/^[^:]*//; s/^:[0-9]*//'; } ;;
command -v hadolint > /dev/null 2>&1 || {
echo " $f: SKIPPED (hadolint not installed)"
continue
}
cmd() { hadolint "$1" 2>&1 | sed 's/^[^:]*//; s/^:[0-9]*//'; }
;;
*) continue ;;
esac
b=$(cmd "$tmp/base" | sort)
@@ -141,7 +178,8 @@ if $VS_MASTER; then
[ -n "$new" ] && {
echo " $f: $(printf '%s\n' "$new" | grep -c .) NEW finding(s)"
printf '%s\n' "$new" | sed 's/^/ /' | head -5
added=1; fail=1
added=1
fail=1
}
done < <(git diff --name-only origin/master...HEAD -- "$ADDON" 2> /dev/null)
[ "$added" -eq 0 ] && echo " (none — this diff introduced no new lint findings)"

View File

@@ -40,7 +40,7 @@ the check is a guard, not a common case.)
## Rule 1 — pick exactly one verdict
| verdict | when |
|---|---|
| --- | --- |
| `duplicate` | An existing open or closed issue reports the same thing. Set `duplicate_of`. |
| `needs-info` | You cannot tell what is wrong without the add-on version, HA version, architecture, config, or the actual log output. |
| `question` | A usage question answerable from `DOCS.md`, the wiki, or the add-on config. Not a defect. |

View File

@@ -39,7 +39,7 @@ rest are on you: nothing checks them, so breaking one ships silently.
that value, and compare:
| `version` vs `U` | what to do | example |
|---|---|---|
| --- | --- | --- |
| identical | **append** `.1` | sonarr `4.0.19.3001` -> `4.0.19.3001.1` |
| `U` + `.` + digits | **increment** those digits | radarr `6.3.0.10514.1` -> `6.3.0.10514.2` |
| anything else | **leave it alone** | plex, readarr, joal |

View File

@@ -44,7 +44,7 @@ for pr in $PRS; do
echo "$BAD"
# Ensure the label exists before adding it — with set -e a missing
# label would abort the whole loop and skip any PRs behind this one.
gh label create "ai:blocked" --repo "$REPO" --color ededed >/dev/null 2>&1 || true
gh label create "ai:blocked" --repo "$REPO" --color ededed > /dev/null 2>&1 || true
gh pr ready "$pr" --repo "$REPO" --undo || true
gh pr edit "$pr" --repo "$REPO" --add-label "ai:blocked"
gh pr comment "$pr" --repo "$REPO" --body \

View File

@@ -15,110 +15,116 @@ CTX="$OUT/context.md"
: > "$CTX"
gh issue view "$ISSUE_NUMBER" --repo "$REPO" \
--json number,title,body,author,labels,createdAt,comments > "$OUT/issue.json"
--json number,title,body,author,labels,createdAt,comments > "$OUT/issue.json"
TITLE=$(jq -r '.title' "$OUT/issue.json")
# ---------------------------------------------------------------- addon slug
# Titles follow "🐛 [Immich Frame] ENV_VARS arent being picked up".
RAW=$(sed -n 's/.*\[\([^]]*\)\].*/\1/p' <<<"$TITLE" | head -n1)
RAW=$(sed -n 's/.*\[\([^]]*\)\].*/\1/p' <<< "$TITLE" | head -n1)
ADDON=""
if [ -n "$RAW" ]; then
CAND=$(tr '[:upper:] ' '[:lower:]_' <<<"$RAW")
# Directory list without checking out any of them.
git ls-tree -d --name-only HEAD > "$OUT/dirs.txt"
for guess in "$CAND" "${CAND//_/-}" "${CAND//_/.}"; do
if grep -qxF "$guess" "$OUT/dirs.txt"; then ADDON="$guess"; break; fi
done
# Separator-insensitive exact match: a title like "[Calibre-web]" (hyphen)
# against a directory named calibre_web (underscore) matches neither exact
# guess above, and would otherwise fall through to the substring fallback
# below, which picks the shorter "calibre" instead — the wrong add-on.
# Stripping -, _, . from both sides before comparing catches this case.
if [ -z "$ADDON" ]; then
CAND_STRIPPED=$(tr -d '_.-' <<<"$CAND")
while IFS= read -r dir; do
if [ "$(tr -d '_.-' <<<"$dir")" = "$CAND_STRIPPED" ]; then ADDON="$dir"; break; fi
done < "$OUT/dirs.txt"
fi
# Last resort: longest directory name contained in the candidate.
if [ -z "$ADDON" ]; then
ADDON=$(awk -v c="$CAND" 'length($0)>2 && index(c,$0){print length($0)"\t"$0}' \
CAND=$(tr '[:upper:] ' '[:lower:]_' <<< "$RAW")
# Directory list without checking out any of them.
git ls-tree -d --name-only HEAD > "$OUT/dirs.txt"
for guess in "$CAND" "${CAND//_/-}" "${CAND//_/.}"; do
if grep -qxF "$guess" "$OUT/dirs.txt"; then
ADDON="$guess"
break
fi
done
# Separator-insensitive exact match: a title like "[Calibre-web]" (hyphen)
# against a directory named calibre_web (underscore) matches neither exact
# guess above, and would otherwise fall through to the substring fallback
# below, which picks the shorter "calibre" instead — the wrong add-on.
# Stripping -, _, . from both sides before comparing catches this case.
if [ -z "$ADDON" ]; then
CAND_STRIPPED=$(tr -d '_.-' <<< "$CAND")
while IFS= read -r dir; do
if [ "$(tr -d '_.-' <<< "$dir")" = "$CAND_STRIPPED" ]; then
ADDON="$dir"
break
fi
done < "$OUT/dirs.txt"
fi
# Last resort: longest directory name contained in the candidate.
if [ -z "$ADDON" ]; then
ADDON=$(awk -v c="$CAND" 'length($0)>2 && index(c,$0){print length($0)"\t"$0}' \
"$OUT/dirs.txt" | sort -rn | head -n1 | cut -f2)
fi
fi
fi
{
echo "# Issue #${ISSUE_NUMBER}"
echo
echo "Repo: ${REPO}"
echo "Addon resolved from title: ${ADDON:-UNRESOLVED}"
echo
echo "## Title"
echo "$TITLE"
echo
echo "## Author"
jq -r '.author.login' "$OUT/issue.json"
echo
echo "## Body"
echo '```'
jq -r '.body // "(empty)"' "$OUT/issue.json"
echo '```'
echo
echo "## Existing comments (in order)"
jq -r '.comments[]? | "### @\(.author.login)\n\(.body)\n"' "$OUT/issue.json"
echo
echo "## Existing labels"
jq -r '[.labels[]?.name] | join(", ")' "$OUT/issue.json"
echo "# Issue #${ISSUE_NUMBER}"
echo
echo "Repo: ${REPO}"
echo "Addon resolved from title: ${ADDON:-UNRESOLVED}"
echo
echo "## Title"
echo "$TITLE"
echo
echo "## Author"
jq -r '.author.login' "$OUT/issue.json"
echo
echo "## Body"
echo '```'
jq -r '.body // "(empty)"' "$OUT/issue.json"
echo '```'
echo
echo "## Existing comments (in order)"
jq -r '.comments[]? | "### @\(.author.login)\n\(.body)\n"' "$OUT/issue.json"
echo
echo "## Existing labels"
jq -r '[.labels[]?.name] | join(", ")' "$OUT/issue.json"
} >> "$CTX"
# ------------------------------------------------------------- addon sources
if [ -n "$ADDON" ]; then
{
echo
echo "## Addon files: ${ADDON}/"
# `set` REPLACES the checkout list, so .templates has to be repeated here
# or the workflow's sparse-checkout of it is silently undone at this point
# — which is exactly the state that starved #2949 of its turn budget.
if ! git sparse-checkout set --no-cone .github/prompts .github/scripts .templates "$ADDON" 2>&1; then
# Swallowing this used to leave ADDON resolved with no files behind it,
# so the classifier could still reach high confidence off the addon
# name alone. Say so explicitly, in the same word Rule 2 already keys
# its low-confidence check on.
echo
echo "**Could not check out this add-on's source. Treat as UNRESOLVED for confidence purposes.**"
else
for f in config.yaml config.json Dockerfile CHANGELOG.md DOCS.md README.md; do
[ -f "$ADDON/$f" ] || continue
{
echo
echo "### ${ADDON}/${f}"
echo '```'
head -c 8000 "$ADDON/$f"
echo '```'
done
echo "## Addon files: ${ADDON}/"
# `set` REPLACES the checkout list, so .templates has to be repeated here
# or the workflow's sparse-checkout of it is silently undone at this point
# — which is exactly the state that starved #2949 of its turn budget.
if ! git sparse-checkout set --no-cone .github/prompts .github/scripts .templates "$ADDON" 2>&1; then
# Swallowing this used to leave ADDON resolved with no files behind it,
# so the classifier could still reach high confidence off the addon
# name alone. Say so explicitly, in the same word Rule 2 already keys
# its low-confidence check on.
echo
echo "**Could not check out this add-on's source. Treat as UNRESOLVED for confidence purposes.**"
else
for f in config.yaml config.json Dockerfile CHANGELOG.md DOCS.md README.md; do
[ -f "$ADDON/$f" ] || continue
echo
echo "### ${ADDON}/${f}"
echo '```'
head -c 8000 "$ADDON/$f"
echo '```'
done
echo
echo "## Recent commits touching ${ADDON}/"
git log -n 15 --date=short --pretty='- %ad %h %s' -- "$ADDON" 2>/dev/null || true
fi
} >> "$CTX"
echo
echo "## Recent commits touching ${ADDON}/"
git log -n 15 --date=short --pretty='- %ad %h %s' -- "$ADDON" 2> /dev/null || true
fi
} >> "$CTX"
fi
# -------------------------------------------------------- possible duplicates
{
echo
echo "## Similar existing issues (candidate duplicates)"
KEYWORDS=$(tr -cs '[:alnum:]' ' ' <<<"$TITLE" \
| tr '[:upper:]' '[:lower:]' \
| tr ' ' '\n' | awk 'length($0)>3' | head -n6 | paste -sd' ')
# Excludes the issue being triaged: if it's already indexed by GitHub search
# by the time this runs, keyword overlap with its own title would otherwise
# list it as a "candidate duplicate" of itself.
gh search issues --repo "$REPO" --limit 15 \
--json number,title,state,url -- "$KEYWORDS" 2>/dev/null \
| jq -r --argjson self "$ISSUE_NUMBER" \
'.[] | select(.number != $self) | "- #\(.number) [\(.state)] \(.title)"' \
|| echo "(search unavailable)"
echo
echo "## Similar existing issues (candidate duplicates)"
KEYWORDS=$(tr -cs '[:alnum:]' ' ' <<< "$TITLE" \
| tr '[:upper:]' '[:lower:]' \
| tr ' ' '\n' | awk 'length($0)>3' | head -n6 | paste -sd' ')
# Excludes the issue being triaged: if it's already indexed by GitHub search
# by the time this runs, keyword overlap with its own title would otherwise
# list it as a "candidate duplicate" of itself.
gh search issues --repo "$REPO" --limit 15 \
--json number,title,state,url -- "$KEYWORDS" 2> /dev/null \
| jq -r --argjson self "$ISSUE_NUMBER" \
'.[] | select(.number != $self) | "- #\(.number) [\(.state)] \(.title)"' \
|| echo "(search unavailable)"
} >> "$CTX"
echo "context bundle: $(wc -c < "$CTX") bytes, addon=${ADDON:-none}"

View File

@@ -418,6 +418,7 @@ KuchenKavalier,,2026-08-10
KuerbisK,,2026-08-10
Kvasenok,,2026-08-10
KyleGolfer,,2026-08-10
L00PERY,,2026-09-06
L0rdShrek,Germany,
L1nKinc,Germany,
LaLaBer,,2026-08-10
@@ -444,6 +445,7 @@ LoginByCall,,2026-08-10
Lolekpolek,,2026-08-10
LonelySoul7X,,2026-08-10
Loong-He,China,2026-08-30
Loong-OvO,China,2026-09-06
Lorsel,Italy,
Luca2165801154,,2026-08-10
Lucius-Waverly,,2026-08-16
@@ -1390,6 +1392,7 @@ flostingapplesauce,,2026-08-10
flozi00,Germany,
flue17,,2026-08-10
fmcglinn,Australia,
foodstampou812,United States,2026-09-06
forming,,2026-08-10
forreggbor,Hungary,
foundbobby,United States,
@@ -1834,6 +1837,7 @@ m1kethai,Canada,
m23l,,2026-08-10
m2sh,"Iran, Islamic Republic of",
m4rcSA,,2026-08-10
m4rkolson,United States,2026-09-06
mProwler,United States,
mStrangers,,2026-08-10
mabt,,2026-08-10
@@ -2085,6 +2089,7 @@ p0wertiger,Poland,
paalwilliams,Germany,
pace6666,,2026-08-10
pafnow,France,
pahanitsch,Germany,2026-09-06
pandabreads,,2026-08-10
pankaj151,India,
papafrank66,,2026-08-10
@@ -2664,6 +2669,7 @@ yycsdm,,2026-08-10
yyll2233,,2026-08-10
z2833,,2026-08-10
z307917424,,2026-08-10
zacharee,Panama,2026-09-06
zachgilliam,United States,
zanyraspi,,2026-08-10
zdaar,,2026-08-10
1 username country last_checked
418 KuerbisK 2026-08-10
419 Kvasenok 2026-08-10
420 KyleGolfer 2026-08-10
421 L00PERY 2026-09-06
422 L0rdShrek Germany
423 L1nKinc Germany
424 LaLaBer 2026-08-10
445 Lolekpolek 2026-08-10
446 LonelySoul7X 2026-08-10
447 Loong-He China 2026-08-30
448 Loong-OvO China 2026-09-06
449 Lorsel Italy
450 Luca2165801154 2026-08-10
451 Lucius-Waverly 2026-08-16
1392 flozi00 Germany
1393 flue17 2026-08-10
1394 fmcglinn Australia
1395 foodstampou812 United States 2026-09-06
1396 forming 2026-08-10
1397 forreggbor Hungary
1398 foundbobby United States
1837 m23l 2026-08-10
1838 m2sh Iran, Islamic Republic of
1839 m4rcSA 2026-08-10
1840 m4rkolson United States 2026-09-06
1841 mProwler United States
1842 mStrangers 2026-08-10
1843 mabt 2026-08-10
2089 paalwilliams Germany
2090 pace6666 2026-08-10
2091 pafnow France
2092 pahanitsch Germany 2026-09-06
2093 pandabreads 2026-08-10
2094 pankaj151 India
2095 papafrank66 2026-08-10
2669 yyll2233 2026-08-10
2670 z2833 2026-08-10
2671 z307917424 2026-08-10
2672 zacharee Panama 2026-09-06
2673 zachgilliam United States
2674 zanyraspi 2026-08-10
2675 zdaar 2026-08-10

Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

After

Width:  |  Height:  |  Size: 66 KiB

BIN
.github/stats.png vendored

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 4.4 KiB

View File

@@ -6,7 +6,7 @@ set -e
# Banner
# ======================================================================
if ! bashio::supervisor.ping 2>/dev/null; then
if ! bashio::supervisor.ping 2> /dev/null; then
# Source standalone bashio first to provide function definitions
if [ -f /usr/local/lib/bashio-standalone.sh ]; then
source /usr/local/lib/bashio-standalone.sh
@@ -19,11 +19,11 @@ if ! bashio::supervisor.ping 2>/dev/null; then
if [ ! -f /usr/bin/bashio ]; then
cp -rf /usr/local/lib/bashio-standalone.sh /usr/bin/bashio
fi
grep -rl "^#!.*bashio" /etc |
while IFS= read -r f; do
grep -qF "source /usr/local/lib/bashio-standalone.sh" "$f" && continue
sed -i '1a source /usr/local/lib/bashio-standalone.sh' "$f"
done
grep -rl "^#!.*bashio" /etc \
| while IFS= read -r f; do
grep -qF "source /usr/local/lib/bashio-standalone.sh" "$f" && continue
sed -i '1a source /usr/local/lib/bashio-standalone.sh' "$f"
done
else
bashio::log.blue '-----------------------------------------------------------'
bashio::log.blue " Add-on: $(bashio::addon.name)"
@@ -49,7 +49,7 @@ bashio::log.green ' Provided by: https://github.com/alexbelgium/hassio-addons '
bashio::log.blue '-----------------------------------------------------------'
# Adapt user abc
if command -v id &>/dev/null && id abc &>/dev/null; then
if command -v id &> /dev/null && id abc &> /dev/null; then
if bashio::config.has_value "PUID" && bashio::config.has_value "PGID"; then
PUID="$(bashio::config "PUID")"
PGID="$(bashio::config "PGID")"

View File

@@ -152,15 +152,15 @@ if [[ "${1:-}" == "--self-test" ]]; then
done
fi
[[ "$self_test_rc" -eq 0 ]] &&
echo "${#self_test_values[@]} values round-tripped unchanged (export block + dotenv)"
[[ "$self_test_rc" -eq 0 ]] \
&& echo "${#self_test_values[@]} values round-tripped unchanged (export block + dotenv)"
exit "$self_test_rc"
fi
################################################################################
# Guard: only run inside Supervisor-managed add-ons
################################################################################
if ! bashio::supervisor.ping 2>/dev/null; then
if ! bashio::supervisor.ping 2> /dev/null; then
echo "..."
exit 0
fi
@@ -179,7 +179,7 @@ ENV_FILE="/.env"
ETC_ENV_FILE="/etc/environment"
[[ -f "$JSONSOURCE" ]] || bashio::exit.nok "Missing $JSONSOURCE"
command -v jq >/dev/null || bashio::exit.nok "jq is required"
command -v jq > /dev/null || bashio::exit.nok "jq is required"
mkdir -p /etc
touch "$ETC_ENV_FILE"
@@ -193,7 +193,10 @@ SECRETSOURCE=""
resolve_secret() {
local v="$1" name line
[[ "$v" =~ ^[[:space:]]*\!secret[[:space:]]+(.+)$ ]] || { printf '%s' "$v"; return; }
[[ "$v" =~ ^[[:space:]]*\!secret[[:space:]]+(.+)$ ]] || {
printf '%s' "$v"
return
}
name="${BASH_REMATCH[1]}"
[[ -n "$SECRETSOURCE" ]] || bashio::exit.nok "Secrets not mounted"

View File

@@ -4,9 +4,9 @@
set -e
if ! bashio::supervisor.ping 2>/dev/null; then
bashio::log.blue "Disabled : please use another method"
exit 0
if ! bashio::supervisor.ping 2> /dev/null; then
bashio::log.blue "Disabled : please use another method"
exit 0
fi
bashio::log.notice "This script is used to mount remote smb/cifs/nfs shares. Instructions here : https://github.com/alexbelgium/hassio-addons/wiki/Mounting-remote-shares-in-Addons"
@@ -16,167 +16,167 @@ bashio::log.notice "This script is used to mount remote smb/cifs/nfs shares. Ins
####################
cleanup_cred() {
if [[ -n "${CRED_FILE:-}" && -f "${CRED_FILE:-}" ]]; then
rm -f "$CRED_FILE" || true
fi
CRED_FILE=""
if [[ -n "${CRED_FILE:-}" && -f "${CRED_FILE:-}" ]]; then
rm -f "$CRED_FILE" || true
fi
CRED_FILE=""
}
test_mount() {
MOUNTED=false
ERROR_MOUNT=false
mountpoint="/mnt/$diskname"
if ! mountpoint -q "$mountpoint"; then
return 0
fi
_test_write() {
local testfile="$mountpoint/.writetest_$$"
if : >"$testfile" 2>/dev/null; then
rm -f "$testfile" 2>/dev/null || true
return 0
else
rm -f "$testfile" 2>/dev/null || true
return 1
fi
}
if _test_write 2>/dev/null; then
MOUNTED=true
return 0
fi
local orig_mountoptions="$MOUNTOPTIONS"
if [[ "$FSTYPE" == "cifs" && "$orig_mountoptions" != *"noserverino"* ]]; then
echo "... retrying mount with noserverino"
MOUNTOPTIONS="${orig_mountoptions},noserverino"
umount "$mountpoint" 2>/dev/null || true
if mount_drive "$MOUNTOPTIONS"; then
if _test_write 2>/dev/null; then
MOUNTED=true
return 0
fi
fi
fi
if [[ "$FSTYPE" == "cifs" && "$orig_mountoptions" != *"noperm"* ]]; then
echo "... retrying mount with noperm"
MOUNTOPTIONS="${orig_mountoptions},noperm"
umount "$mountpoint" 2>/dev/null || true
if mount_drive "$MOUNTOPTIONS"; then
if _test_write 2>/dev/null; then
MOUNTED=true
return 0
fi
fi
fi
if mountpoint -q "$mountpoint"; then
MOUNTED="readonly"
else
MOUNTED=false
fi
return 0
ERROR_MOUNT=false
mountpoint="/mnt/$diskname"
if ! mountpoint -q "$mountpoint"; then
return 0
fi
_test_write() {
local testfile="$mountpoint/.writetest_$$"
if : > "$testfile" 2> /dev/null; then
rm -f "$testfile" 2> /dev/null || true
return 0
else
rm -f "$testfile" 2> /dev/null || true
return 1
fi
}
if _test_write 2> /dev/null; then
MOUNTED=true
return 0
fi
local orig_mountoptions="$MOUNTOPTIONS"
if [[ "$FSTYPE" == "cifs" && "$orig_mountoptions" != *"noserverino"* ]]; then
echo "... retrying mount with noserverino"
MOUNTOPTIONS="${orig_mountoptions},noserverino"
umount "$mountpoint" 2> /dev/null || true
if mount_drive "$MOUNTOPTIONS"; then
if _test_write 2> /dev/null; then
MOUNTED=true
return 0
fi
fi
fi
if [[ "$FSTYPE" == "cifs" && "$orig_mountoptions" != *"noperm"* ]]; then
echo "... retrying mount with noperm"
MOUNTOPTIONS="${orig_mountoptions},noperm"
umount "$mountpoint" 2> /dev/null || true
if mount_drive "$MOUNTOPTIONS"; then
if _test_write 2> /dev/null; then
MOUNTED=true
return 0
fi
fi
fi
if mountpoint -q "$mountpoint"; then
MOUNTED="readonly"
else
MOUNTED=false
fi
return 0
}
mount_drive() {
MOUNTED=true
MOUNTOPTIONS="$1"
MOUNTED=true
MOUNTOPTIONS="$1"
if [[ "$FSTYPE" == "cifs" ]]; then
mount -t cifs -o "$MOUNTOPTIONS" "$disk" "/mnt/$diskname" 2>"$ERRORCODE_FILE" || MOUNTED=false
elif [[ "$FSTYPE" == "nfs" ]]; then
mount -t nfs -o "$MOUNTOPTIONS" "$disk" "/mnt/$diskname" 2>"$ERRORCODE_FILE" || MOUNTED=false
fi
if [[ "$FSTYPE" == "cifs" ]]; then
mount -t cifs -o "$MOUNTOPTIONS" "$disk" "/mnt/$diskname" 2> "$ERRORCODE_FILE" || MOUNTED=false
elif [[ "$FSTYPE" == "nfs" ]]; then
mount -t nfs -o "$MOUNTOPTIONS" "$disk" "/mnt/$diskname" 2> "$ERRORCODE_FILE" || MOUNTED=false
fi
if [[ "$MOUNTED" == "true" ]]; then
test_mount
fi
if [[ "$MOUNTED" == "true" ]]; then
test_mount
fi
}
# Retry ladder: SMB3 -> SMB2 when mount fails due to dialect/negotiation issues
retry_cifs_with_vers_ladder_on_dialect_failure() {
[[ "${FSTYPE:-}" == "cifs" ]] || return 0
[[ "${MOUNTED:-false}" == "false" ]] || return 0
[[ "${CIFS_LADDER_ATTEMPTED:-false}" == "false" ]] || return 0
[[ "${FSTYPE:-}" == "cifs" ]] || return 0
[[ "${MOUNTED:-false}" == "false" ]] || return 0
[[ "${CIFS_LADDER_ATTEMPTED:-false}" == "false" ]] || return 0
local err mountpoint
mountpoint="/mnt/$diskname"
err="$(cat "$ERRORCODE_FILE" 2>/dev/null || true)"
local err mountpoint
mountpoint="/mnt/$diskname"
err="$(cat "$ERRORCODE_FILE" 2> /dev/null || true)"
if echo "$err" | grep -Eq 'mount error\(13\)|Permission denied|NT_STATUS_(LOGON_FAILURE|ACCESS_DENIED)|STATUS_(LOGON_FAILURE|ACCESS_DENIED)'; then
return 0
fi
if echo "$err" | grep -Eq 'mount error\(13\)|Permission denied|NT_STATUS_(LOGON_FAILURE|ACCESS_DENIED)|STATUS_(LOGON_FAILURE|ACCESS_DENIED)'; then
return 0
fi
if ! echo "$err" | grep -Eq 'mount error\(22\)|mount error\(95\)|mount error\(112\)|Server abruptly closed the connection|does not support the SMB version|Protocol negotiation|NT_STATUS_CONNECTION_DISCONNECTED'; then
return 0
fi
if ! echo "$err" | grep -Eq 'mount error\(22\)|mount error\(95\)|mount error\(112\)|Server abruptly closed the connection|does not support the SMB version|Protocol negotiation|NT_STATUS_CONNECTION_DISCONNECTED'; then
return 0
fi
CIFS_LADDER_ATTEMPTED=true
bashio::log.warning "...... CIFS negotiation/dialect failure: trying SMB dialect ladder (3.x -> 2.x -> 1.0)."
CIFS_LADDER_ATTEMPTED=true
bashio::log.warning "...... CIFS negotiation/dialect failure: trying SMB dialect ladder (3.x -> 2.x -> 1.0)."
local base_opts try_opts vers vopt sectry
local base_opts try_opts vers vopt sectry
base_opts="$MOUNTOPTIONS"
base_opts="$(echo "$base_opts" | sed -E 's/,vers=[^,]+//g; s/,sec=[^,]+//g')"
local -a opt_variants=("" ",nounix" ",noserverino" ",nounix,noserverino")
local -a sec_variants=("" ",sec=ntlmssp" ",sec=ntlmv2" ",sec=ntlm")
local -a vers_variants=("3.1.1" "3.02" "3.0" "2.1" "2.0" "1.0")
for vopt in "${opt_variants[@]}"; do
for vers in "${vers_variants[@]}"; do
for sectry in "${sec_variants[@]}"; do
[[ "$MOUNTED" == "false" ]] || break
umount "$mountpoint" 2>/dev/null || true
try_opts="${base_opts}${vopt},vers=${vers}${sectry}"
mount_drive "$try_opts"
done
[[ "$MOUNTED" == "false" ]] || break
done
[[ "$MOUNTED" == "false" ]] || break
done
if [[ "$MOUNTED" == "false" ]]; then
bashio::log.warning "...... still failing after vers ladder; retrying with reduced CIFS options."
base_opts="$MOUNTOPTIONS"
base_opts="$(echo "$base_opts" | sed -E 's/,vers=[^,]+//g; s/,sec=[^,]+//g')"
base_opts="${base_opts//,mfsymlinks/}"
base_opts="${base_opts//,nobrl/}"
base_opts="$(echo "$base_opts" | sed -E 's/,iocharset=[^,]+//g')"
local -a vers_variants2=("2.1" "2.0" "1.0")
local -a opt_variants=("" ",nounix" ",noserverino" ",nounix,noserverino")
local -a sec_variants=("" ",sec=ntlmssp" ",sec=ntlmv2" ",sec=ntlm")
local -a vers_variants=("3.1.1" "3.02" "3.0" "2.1" "2.0" "1.0")
for vopt in "${opt_variants[@]}"; do
for vers in "${vers_variants2[@]}"; do
for sectry in "${sec_variants[@]}"; do
[[ "$MOUNTED" == "false" ]] || break
umount "$mountpoint" 2>/dev/null || true
try_opts="${base_opts}${vopt},vers=${vers}${sectry}"
mount_drive "$try_opts"
for vers in "${vers_variants[@]}"; do
for sectry in "${sec_variants[@]}"; do
[[ "$MOUNTED" == "false" ]] || break
umount "$mountpoint" 2> /dev/null || true
try_opts="${base_opts}${vopt},vers=${vers}${sectry}"
mount_drive "$try_opts"
done
[[ "$MOUNTED" == "false" ]] || break
done
[[ "$MOUNTED" == "false" ]] || break
done
[[ "$MOUNTED" == "false" ]] || break
done
if [[ "$MOUNTED" == "false" ]]; then
for vopt in "${opt_variants[@]}"; do
for vers in "${vers_variants2[@]}"; do
[[ "$MOUNTED" == "false" ]] || break
umount "$mountpoint" 2>/dev/null || true
try_opts="${base_opts}${vopt},vers=${vers},sec=ntlmssp"
mount_drive "$try_opts"
done
[[ "$MOUNTED" == "false" ]] || break
done
fi
fi
bashio::log.warning "...... still failing after vers ladder; retrying with reduced CIFS options."
base_opts="$MOUNTOPTIONS"
base_opts="$(echo "$base_opts" | sed -E 's/,vers=[^,]+//g; s/,sec=[^,]+//g')"
base_opts="${base_opts//,mfsymlinks/}"
base_opts="${base_opts//,nobrl/}"
base_opts="$(echo "$base_opts" | sed -E 's/,iocharset=[^,]+//g')"
return 0
local -a vers_variants2=("2.1" "2.0" "1.0")
for vopt in "${opt_variants[@]}"; do
for vers in "${vers_variants2[@]}"; do
for sectry in "${sec_variants[@]}"; do
[[ "$MOUNTED" == "false" ]] || break
umount "$mountpoint" 2> /dev/null || true
try_opts="${base_opts}${vopt},vers=${vers}${sectry}"
mount_drive "$try_opts"
done
[[ "$MOUNTED" == "false" ]] || break
done
[[ "$MOUNTED" == "false" ]] || break
done
if [[ "$MOUNTED" == "false" ]]; then
for vopt in "${opt_variants[@]}"; do
for vers in "${vers_variants2[@]}"; do
[[ "$MOUNTED" == "false" ]] || break
umount "$mountpoint" 2> /dev/null || true
try_opts="${base_opts}${vopt},vers=${vers},sec=ntlmssp"
mount_drive "$try_opts"
done
[[ "$MOUNTED" == "false" ]] || break
done
fi
fi
return 0
}
########################
@@ -185,289 +185,289 @@ retry_cifs_with_vers_ladder_on_dialect_failure() {
if bashio::config.has_value 'networkdisks'; then
echo "Mounting network share(s)..."
echo "Mounting network share(s)..."
MOREDISKS="$(bashio::config 'networkdisks')"
USERNAME="$(bashio::config 'cifsusername')"
PASSWORD="$(bashio::config 'cifspassword')"
MOREDISKS="$(bashio::config 'networkdisks')"
USERNAME="$(bashio::config 'cifsusername')"
PASSWORD="$(bashio::config 'cifspassword')"
SMBVERS=""
SECVERS=""
CHARSET=",iocharset=utf8"
MOREDISKS="${MOREDISKS//$'\r'/}"
MOREDISKS="${MOREDISKS//$'\n'/,}"
MOREDISKS="$(echo "$MOREDISKS" | sed -E 's/[[:space:]]*,[[:space:]]*/,/g; s/^[[:space:]]+//; s/[[:space:]]+$//')"
DOMAINCLIENT=""
CIFSDOMAIN=""
if bashio::config.has_value 'cifsdomain'; then
CIFSDOMAIN="$(bashio::config 'cifsdomain')"
echo "... using domain $CIFSDOMAIN"
DOMAINCLIENT="--workgroup=$CIFSDOMAIN"
fi
PUID=",uid=$(id -u)"
PGID=",gid=$(id -g)"
if bashio::config.has_value 'PUID' && bashio::config.has_value 'PGID'; then
echo "... using PUID $(bashio::config 'PUID') and PGID $(bashio::config 'PGID')"
PUID=",uid=$(bashio::config 'PUID')"
PGID=",gid=$(bashio::config 'PGID')"
fi
IFS=',' read -r -a DISK_LIST <<< "$MOREDISKS"
for disk in "${DISK_LIST[@]}"; do
CRED_FILE=""
cleanup_cred
disk="${disk//$'\r'/}"
disk="$(echo "$disk" | sed -E 's/^[[:space:]]+//; s/[[:space:]]+$//')"
[[ -z "$disk" ]] && continue
disk="$(echo "$disk" | sed 's,/$,,')"
disk="${disk//"\040"/ }"
FSTYPE="cifs"
if [[ "$disk" =~ ^nfs:// ]]; then
FSTYPE="nfs"
disk="${disk#nfs://}"
elif [[ "$disk" =~ ^[^/:]+:/.+ ]]; then
FSTYPE="nfs"
fi
if [[ "$FSTYPE" == "cifs" ]]; then
server="$(echo "$disk" | grep -E -o "[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+" | head -n 1)"
else
server="${disk%%:*}"
fi
diskname="$disk"
diskname="${diskname//\\//}"
diskname="${diskname##*/}"
if mountpoint -q "/mnt/$diskname" 2>/dev/null; then
bashio::log.warning "...... mount point /mnt/$diskname already in use (name collision for $disk). Skipping this share."
continue
fi
ERRORCODE_FILE="/tmp/mount_error_${diskname//[^a-zA-Z0-9._-]/_}.log"
: >"$ERRORCODE_FILE" || true
MOUNTED=false
CIFS_LADDER_ATTEMPTED=false
SMBVERS_FORCE=""
SECVERS_FORCE=""
SMBVERS=""
SECVERS=""
CHARSET=",iocharset=utf8"
echo "... mounting ($FSTYPE) $disk"
MOREDISKS="${MOREDISKS//$'\r'/}"
MOREDISKS="${MOREDISKS//$'\n'/,}"
MOREDISKS="$(echo "$MOREDISKS" | sed -E 's/[[:space:]]*,[[:space:]]*/,/g; s/^[[:space:]]+//; s/[[:space:]]+$//')"
if [[ "$FSTYPE" == "cifs" ]]; then
if [[ ! "$disk" =~ ^//[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+/.+ ]]; then
bashio::log.fatal "...... invalid CIFS path \"$disk\". Use //123.12.12.12/sharedfolder,//123.12.12.12/sharedfolder2"
echo "Invalid CIFS path structure: $disk" >"$ERRORCODE_FILE" || true
continue
fi
else
if [[ ! "$disk" =~ ^[^/:]+:/.+ ]]; then
bashio::log.fatal "...... invalid NFS path \"$disk\". Use server:/export/path or 123.12.12.12:/export/path"
echo "Invalid NFS path structure: $disk" >"$ERRORCODE_FILE" || true
continue
fi
DOMAINCLIENT=""
CIFSDOMAIN=""
if bashio::config.has_value 'cifsdomain'; then
CIFSDOMAIN="$(bashio::config 'cifsdomain')"
echo "... using domain $CIFSDOMAIN"
DOMAINCLIENT="--workgroup=$CIFSDOMAIN"
fi
mkdir -p "/mnt/$diskname"
chown root:root "/mnt/$diskname"
PUID=",uid=$(id -u)"
PGID=",gid=$(id -g)"
if bashio::config.has_value 'PUID' && bashio::config.has_value 'PGID'; then
echo "... using PUID $(bashio::config 'PUID') and PGID $(bashio::config 'PGID')"
PUID=",uid=$(bashio::config 'PUID')"
PGID=",gid=$(bashio::config 'PGID')"
fi
if [[ "$FSTYPE" == "cifs" ]]; then
CRED_FILE="$(mktemp /tmp/cifs-cred.XXXXXX)"
chmod 600 "$CRED_FILE"
{
printf 'username=%s\n' "$USERNAME"
printf 'password=%s\n' "$PASSWORD"
if [[ -n "${CIFSDOMAIN:-}" ]]; then
printf 'domain=%s\n' "$CIFSDOMAIN"
IFS=',' read -r -a DISK_LIST <<< "$MOREDISKS"
for disk in "${DISK_LIST[@]}"; do
CRED_FILE=""
cleanup_cred
disk="${disk//$'\r'/}"
disk="$(echo "$disk" | sed -E 's/^[[:space:]]+//; s/[[:space:]]+$//')"
[[ -z "$disk" ]] && continue
disk="$(echo "$disk" | sed 's,/$,,')"
disk="${disk//"\040"/ }"
FSTYPE="cifs"
if [[ "$disk" =~ ^nfs:// ]]; then
FSTYPE="nfs"
disk="${disk#nfs://}"
elif [[ "$disk" =~ ^[^/:]+:/.+ ]]; then
FSTYPE="nfs"
fi
} >"$CRED_FILE"
fi
if [[ "$FSTYPE" == "cifs" ]]; then
mount_drive "rw,file_mode=0775,dir_mode=0775,credentials=${CRED_FILE},nobrl,mfsymlinks${SMBVERS}${SECVERS}${PUID}${PGID}${CHARSET}"
if [[ "$MOUNTED" == "false" ]]; then
retry_cifs_with_vers_ladder_on_dialect_failure
fi
else
mount_drive "rw,nfsvers=4.2,proto=tcp,hard,timeo=600,retrans=2"
fi
if [[ "$FSTYPE" == "cifs" ]]; then
server="$(echo "$disk" | grep -E -o "[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+" | head -n 1)"
else
server="${disk%%:*}"
fi
if [[ "$MOUNTED" == "false" ]]; then
if [[ "$FSTYPE" == "cifs" ]]; then
if command -v nmap >/dev/null 2>&1; then
output="$(nmap -F "$server" -T5 -oG - 2>/dev/null || true)"
if ! echo "$output" | grep -q "445/open"; then
if echo "$output" | grep -q "/open"; then
bashio::log.fatal "...... $server is reachable but SMB port not opened, stopping script"
else
bashio::log.fatal "...... fatal : $server not reachable, is it correct"
fi
cleanup_cred
rm -f "$ERRORCODE_FILE" 2>/dev/null || true
diskname="$disk"
diskname="${diskname//\\//}"
diskname="${diskname##*/}"
if mountpoint -q "/mnt/$diskname" 2> /dev/null; then
bashio::log.warning "...... mount point /mnt/$diskname already in use (name collision for $disk). Skipping this share."
continue
fi
echo "...... $server is confirmed reachable"
else
bashio::log.warning "...... nmap not available; skipping SMB port reachability test"
fi
if command -v smbclient >/dev/null 2>&1; then
OUTPUT="$(smbclient -t 2 -L "$server" -U "$USERNAME%$PASSWORD" -c "exit" $DOMAINCLIENT 2>&1 || true)"
if echo "$OUTPUT" | grep -q "LOGON_FAILURE"; then
bashio::log.fatal "...... incorrect Username, Password, or Domain! Script will stop."
cleanup_cred
rm -f "$ERRORCODE_FILE" 2>/dev/null || true
bashio::addon.stop
elif echo "$OUTPUT" | grep -q "tree connect failed" || echo "$OUTPUT" | grep -q "NT_STATUS_CONNECTION_DISCONNECTED"; then
echo "... using SMBv1"
bashio::log.warning "...... share reachable only with legacy SMBv1 (NT1) negotiation. Forcing SMBv1 options."
SMBVERS_FORCE=",vers=1.0"
SECVERS_FORCE=",sec=ntlm"
elif ! echo "$OUTPUT" | grep -q "Disk"; then
bashio::log.fatal "...... no shares found. Invalid or inaccessible SMB path?"
else
echo "...... credentials are valid"
fi
else
bashio::log.warning "...... smbclient not available; skipping SMB credential test"
fi
SMBRAW=""
SMB1_DETECTED=false
if command -v nmap >/dev/null 2>&1; then
NMAP_OUTPUT="$(nmap --script smb-protocols -p 445 "$server" 2>/dev/null || true)"
SMBRAW="$(
echo "$NMAP_OUTPUT" \
| awk '/SMB2_DIALECT_/ {print $NF}' \
| sed 's/SMB2_DIALECT_//' \
| tr -d '_' \
| sort -V | tail -n 1 || true
)"
if [[ -z "$SMBRAW" ]] && echo "$NMAP_OUTPUT" | grep -Eiq 'NT LM 0\.12|SMBv1|NT1'; then
SMB1_DETECTED=true
fi
fi
ERRORCODE_FILE="/tmp/mount_error_${diskname//[^a-zA-Z0-9._-]/_}.log"
: > "$ERRORCODE_FILE" || true
MOUNTED=false
CIFS_LADDER_ATTEMPTED=false
SMBVERS_FORCE=""
SECVERS_FORCE=""
SMBVERS=""
case "$SMBRAW" in
311) SMBVERS=",vers=3.1.1" ;;
302) SMBVERS=",vers=3.02" ;;
300) SMBVERS=",vers=3.0" ;;
210) SMBVERS=",vers=2.1" ;;
202|200) SMBVERS=",vers=2.0" ;;
*) SMBVERS="" ;;
esac
SECVERS=""
if [[ -z "$SMBVERS" && "$SMB1_DETECTED" == "true" ]]; then
echo "...... SMB version detected via nmap : SMBv1 (NT LM 0.12)"
SMBVERS=",vers=1.0"
SECVERS=",sec=ntlm"
fi
echo "... mounting ($FSTYPE) $disk"
if [[ -n "$SMBVERS" ]]; then
echo "...... SMB version detected : ${SMBVERS#,vers=}"
elif command -v smbclient >/dev/null 2>&1 && smbclient -t 2 -L "$server" -m NT1 -U "$USERNAME%$PASSWORD" $DOMAINCLIENT -c "exit" &>/dev/null; then
echo "...... SMB version : only SMBv1 is supported, this can lead to issues"
SECVERS=",sec=ntlm"
SMBVERS=",vers=1.0"
if [[ "$FSTYPE" == "cifs" ]]; then
if [[ ! "$disk" =~ ^//[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+/.+ ]]; then
bashio::log.fatal "...... invalid CIFS path \"$disk\". Use //123.12.12.12/sharedfolder,//123.12.12.12/sharedfolder2"
echo "Invalid CIFS path structure: $disk" > "$ERRORCODE_FILE" || true
continue
fi
else
echo "...... SMB version : couldn't detect, falling back to SMB3->SMB2->SMB1 ladder on negotiation/dialect failure"
SMBVERS=",vers=3.1.1"
if [[ ! "$disk" =~ ^[^/:]+:/.+ ]]; then
bashio::log.fatal "...... invalid NFS path \"$disk\". Use server:/export/path or 123.12.12.12:/export/path"
echo "Invalid NFS path structure: $disk" > "$ERRORCODE_FILE" || true
continue
fi
fi
if [[ -n "$SMBVERS_FORCE" ]]; then
if [[ -n "$SMBVERS" && "$SMBVERS" != "$SMBVERS_FORCE" ]]; then
bashio::log.warning "...... overriding detected SMB version ${SMBVERS#,vers=} with forced ${SMBVERS_FORCE#,vers=} (server requires legacy protocol)"
fi
SMBVERS="$SMBVERS_FORCE"
[[ -z "$SECVERS" ]] && SECVERS="$SECVERS_FORCE"
mkdir -p "/mnt/$diskname"
chown root:root "/mnt/$diskname"
if [[ "$FSTYPE" == "cifs" ]]; then
CRED_FILE="$(mktemp /tmp/cifs-cred.XXXXXX)"
chmod 600 "$CRED_FILE"
{
printf 'username=%s\n' "$USERNAME"
printf 'password=%s\n' "$PASSWORD"
if [[ -n "${CIFSDOMAIN:-}" ]]; then
printf 'domain=%s\n' "$CIFSDOMAIN"
fi
} > "$CRED_FILE"
fi
SECVERS_BASE="$SECVERS"
for SECTRY in "$SECVERS_BASE" ",sec=ntlmv2" ",sec=ntlmssp" ",sec=ntlmsspi" ",sec=krb5i" ",sec=krb5" ",sec=ntlm" ",sec=ntlmv2i"; do
if [[ "$MOUNTED" == "false" ]]; then
mount_drive "rw,file_mode=0775,dir_mode=0775,credentials=${CRED_FILE},nobrl,mfsymlinks${SMBVERS}${SECTRY}${PUID}${PGID}${CHARSET}"
fi
done
if [[ "$FSTYPE" == "cifs" ]]; then
mount_drive "rw,file_mode=0775,dir_mode=0775,credentials=${CRED_FILE},nobrl,mfsymlinks${SMBVERS}${SECVERS}${PUID}${PGID}${CHARSET}"
if [[ "$MOUNTED" == "false" ]]; then
retry_cifs_with_vers_ladder_on_dialect_failure
fi
else
mount_drive "rw,nfsvers=4.2,proto=tcp,hard,timeo=600,retrans=2"
fi
if [[ "$MOUNTED" == "false" ]]; then
retry_cifs_with_vers_ladder_on_dialect_failure
if [[ "$FSTYPE" == "cifs" ]]; then
if command -v nmap > /dev/null 2>&1; then
output="$(nmap -F "$server" -T5 -oG - 2> /dev/null || true)"
if ! echo "$output" | grep -q "445/open"; then
if echo "$output" | grep -q "/open"; then
bashio::log.fatal "...... $server is reachable but SMB port not opened, stopping script"
else
bashio::log.fatal "...... fatal : $server not reachable, is it correct"
fi
cleanup_cred
rm -f "$ERRORCODE_FILE" 2> /dev/null || true
continue
fi
echo "...... $server is confirmed reachable"
else
bashio::log.warning "...... nmap not available; skipping SMB port reachability test"
fi
if command -v smbclient > /dev/null 2>&1; then
OUTPUT="$(smbclient -t 2 -L "$server" -U "$USERNAME%$PASSWORD" -c "exit" $DOMAINCLIENT 2>&1 || true)"
if echo "$OUTPUT" | grep -q "LOGON_FAILURE"; then
bashio::log.fatal "...... incorrect Username, Password, or Domain! Script will stop."
cleanup_cred
rm -f "$ERRORCODE_FILE" 2> /dev/null || true
bashio::addon.stop
elif echo "$OUTPUT" | grep -q "tree connect failed" || echo "$OUTPUT" | grep -q "NT_STATUS_CONNECTION_DISCONNECTED"; then
echo "... using SMBv1"
bashio::log.warning "...... share reachable only with legacy SMBv1 (NT1) negotiation. Forcing SMBv1 options."
SMBVERS_FORCE=",vers=1.0"
SECVERS_FORCE=",sec=ntlm"
elif ! echo "$OUTPUT" | grep -q "Disk"; then
bashio::log.fatal "...... no shares found. Invalid or inaccessible SMB path?"
else
echo "...... credentials are valid"
fi
else
bashio::log.warning "...... smbclient not available; skipping SMB credential test"
fi
SMBRAW=""
SMB1_DETECTED=false
if command -v nmap > /dev/null 2>&1; then
NMAP_OUTPUT="$(nmap --script smb-protocols -p 445 "$server" 2> /dev/null || true)"
SMBRAW="$(
echo "$NMAP_OUTPUT" \
| awk '/SMB2_DIALECT_/ {print $NF}' \
| sed 's/SMB2_DIALECT_//' \
| tr -d '_' \
| sort -V | tail -n 1 || true
)"
if [[ -z "$SMBRAW" ]] && echo "$NMAP_OUTPUT" | grep -Eiq 'NT LM 0\.12|SMBv1|NT1'; then
SMB1_DETECTED=true
fi
fi
SMBVERS=""
case "$SMBRAW" in
311) SMBVERS=",vers=3.1.1" ;;
302) SMBVERS=",vers=3.02" ;;
300) SMBVERS=",vers=3.0" ;;
210) SMBVERS=",vers=2.1" ;;
202 | 200) SMBVERS=",vers=2.0" ;;
*) SMBVERS="" ;;
esac
if [[ -z "$SMBVERS" && "$SMB1_DETECTED" == "true" ]]; then
echo "...... SMB version detected via nmap : SMBv1 (NT LM 0.12)"
SMBVERS=",vers=1.0"
SECVERS=",sec=ntlm"
fi
if [[ -n "$SMBVERS" ]]; then
echo "...... SMB version detected : ${SMBVERS#,vers=}"
elif command -v smbclient > /dev/null 2>&1 && smbclient -t 2 -L "$server" -m NT1 -U "$USERNAME%$PASSWORD" $DOMAINCLIENT -c "exit" &> /dev/null; then
echo "...... SMB version : only SMBv1 is supported, this can lead to issues"
SECVERS=",sec=ntlm"
SMBVERS=",vers=1.0"
else
echo "...... SMB version : couldn't detect, falling back to SMB3->SMB2->SMB1 ladder on negotiation/dialect failure"
SMBVERS=",vers=3.1.1"
fi
if [[ -n "$SMBVERS_FORCE" ]]; then
if [[ -n "$SMBVERS" && "$SMBVERS" != "$SMBVERS_FORCE" ]]; then
bashio::log.warning "...... overriding detected SMB version ${SMBVERS#,vers=} with forced ${SMBVERS_FORCE#,vers=} (server requires legacy protocol)"
fi
SMBVERS="$SMBVERS_FORCE"
[[ -z "$SECVERS" ]] && SECVERS="$SECVERS_FORCE"
fi
SECVERS_BASE="$SECVERS"
for SECTRY in "$SECVERS_BASE" ",sec=ntlmv2" ",sec=ntlmssp" ",sec=ntlmsspi" ",sec=krb5i" ",sec=krb5" ",sec=ntlm" ",sec=ntlmv2i"; do
if [[ "$MOUNTED" == "false" ]]; then
mount_drive "rw,file_mode=0775,dir_mode=0775,credentials=${CRED_FILE},nobrl,mfsymlinks${SMBVERS}${SECTRY}${PUID}${PGID}${CHARSET}"
fi
done
if [[ "$MOUNTED" == "false" ]]; then
retry_cifs_with_vers_ladder_on_dialect_failure
fi
else
if command -v nmap > /dev/null 2>&1; then
output="$(nmap -F "$server" -T5 -oG - 2> /dev/null || true)"
if ! echo "$output" | grep -Eq '(2049|111)/open'; then
bashio::log.fatal "...... $server is reachable but NFS ports not open"
fi
else
bashio::log.warning "...... nmap not available; skipping NFS port reachability test"
fi
for NFVER in 4.2 4.1 4 3; do
if [[ "$MOUNTED" == "false" ]]; then
mount_drive "rw,nfsvers=${NFVER},proto=tcp"
fi
done
fi
fi
else
if command -v nmap >/dev/null 2>&1; then
output="$(nmap -F "$server" -T5 -oG - 2>/dev/null || true)"
if ! echo "$output" | grep -Eq '(2049|111)/open'; then
bashio::log.fatal "...... $server is reachable but NFS ports not open"
fi
if [[ "$MOUNTED" == "true" ]]; then
bashio::log.info "...... $disk successfully mounted to /mnt/$diskname with options ${MOUNTOPTIONS/$PASSWORD/XXXXXXXXXX}"
rm -f "$ERRORCODE_FILE" 2> /dev/null || true
if [[ "$FSTYPE" == "cifs" && "$MOUNTOPTIONS" == *"vers=1.0"* ]]; then
bashio::log.warning ""
bashio::log.warning "Your SMB system requires SMBv1. This is an obsolete protocol. Please correct this to prevent issues."
bashio::log.warning ""
fi
cleanup_cred
elif [[ "$MOUNTED" == "readonly" ]]; then
bashio::log.warning "...... $disk mounted to /mnt/$diskname but is READ-ONLY or not writable by UID/GID ${PUID#,uid=}:${PGID#,gid=}."
bashio::log.warning "...... Check Samba share permissions, or try setting PUID/PGID to 0/0 (root), or adjust server ACLs."
rm -f "$ERRORCODE_FILE" 2> /dev/null || true
cleanup_cred
else
bashio::log.warning "...... nmap not available; skipping NFS port reachability test"
if [[ "$FSTYPE" == "cifs" ]]; then
bashio::log.fatal "Error, unable to mount $disk to /mnt/$diskname with username $USERNAME. Please check share path, username/password/domain; try UID/GID 0."
bashio::log.fatal "Here is some debugging info :"
if command -v smbclient > /dev/null 2>&1; then
smbclient -t 2 -L "$server" -U "$USERNAME%$PASSWORD" -c "exit" $DOMAINCLIENT || true
else
bashio::log.warning "smbclient not available; cannot print SMB debugging info"
fi
mount_drive "rw,credentials=${CRED_FILE}${PUID}${PGID}"
if [[ "$MOUNTED" == "false" ]]; then
retry_cifs_with_vers_ladder_on_dialect_failure
fi
else
bashio::log.fatal "Error, unable to mount NFS share $disk to /mnt/$diskname. Please check export path and allowlist for this client."
mount_drive "rw"
fi
ERR_READ="$(cat "$ERRORCODE_FILE" 2> /dev/null || true)"
bashio::log.fatal "Error read : ${ERR_READ:-unknown error}, addon will stop in 1 min"
umount "/mnt/$diskname" 2> /dev/null || true
rmdir "/mnt/$diskname" 2> /dev/null || true
cleanup_cred
rm -f "$ERRORCODE_FILE" 2> /dev/null || true
bashio::addon.stop
fi
for NFVER in 4.2 4.1 4 3; do
if [[ "$MOUNTED" == "false" ]]; then
mount_drive "rw,nfsvers=${NFVER},proto=tcp"
fi
done
fi
fi
if [[ "$MOUNTED" == "true" ]]; then
bashio::log.info "...... $disk successfully mounted to /mnt/$diskname with options ${MOUNTOPTIONS/$PASSWORD/XXXXXXXXXX}"
rm -f "$ERRORCODE_FILE" 2>/dev/null || true
if [[ "$FSTYPE" == "cifs" && "$MOUNTOPTIONS" == *"vers=1.0"* ]]; then
bashio::log.warning ""
bashio::log.warning "Your SMB system requires SMBv1. This is an obsolete protocol. Please correct this to prevent issues."
bashio::log.warning ""
fi
cleanup_cred
elif [[ "$MOUNTED" == "readonly" ]]; then
bashio::log.warning "...... $disk mounted to /mnt/$diskname but is READ-ONLY or not writable by UID/GID ${PUID#,uid=}:${PGID#,gid=}."
bashio::log.warning "...... Check Samba share permissions, or try setting PUID/PGID to 0/0 (root), or adjust server ACLs."
rm -f "$ERRORCODE_FILE" 2>/dev/null || true
cleanup_cred
else
if [[ "$FSTYPE" == "cifs" ]]; then
bashio::log.fatal "Error, unable to mount $disk to /mnt/$diskname with username $USERNAME. Please check share path, username/password/domain; try UID/GID 0."
bashio::log.fatal "Here is some debugging info :"
if command -v smbclient >/dev/null 2>&1; then
smbclient -t 2 -L "$server" -U "$USERNAME%$PASSWORD" -c "exit" $DOMAINCLIENT || true
else
bashio::log.warning "smbclient not available; cannot print SMB debugging info"
fi
mount_drive "rw,credentials=${CRED_FILE}${PUID}${PGID}"
if [[ "$MOUNTED" == "false" ]]; then
retry_cifs_with_vers_ladder_on_dialect_failure
fi
else
bashio::log.fatal "Error, unable to mount NFS share $disk to /mnt/$diskname. Please check export path and allowlist for this client."
mount_drive "rw"
fi
ERR_READ="$(cat "$ERRORCODE_FILE" 2>/dev/null || true)"
bashio::log.fatal "Error read : ${ERR_READ:-unknown error}, addon will stop in 1 min"
umount "/mnt/$diskname" 2>/dev/null || true
rmdir "/mnt/$diskname" 2>/dev/null || true
cleanup_cred
rm -f "$ERRORCODE_FILE" 2>/dev/null || true
bashio::addon.stop
fi
done
done
fi

View File

@@ -138,9 +138,9 @@ append_unique_line() {
# $1=file, $2=line
local _file="$1"
local _line="$2"
mkdir -p "$(dirname "$_file")" 2>/dev/null || true
touch "$_file" 2>/dev/null || true
grep -qxF -- "$_line" "$_file" 2>/dev/null || echo "$_line" >> "$_file"
mkdir -p "$(dirname "$_file")" 2> /dev/null || true
touch "$_file" 2> /dev/null || true
grep -qxF -- "$_line" "$_file" 2> /dev/null || echo "$_line" >> "$_file"
}
while IFS= read -r line; do

View File

@@ -7,27 +7,33 @@ set -e
##########################################
pick_exec_dir() {
local d
for d in /dev/shm /run /var/run /mnt /root /; do
if [ -d "$d" ] && [ -w "$d" ]; then
local t="${d%/}/.exec_test_$$"
printf '#!/bin/sh\necho ok\n' >"$t" 2>/dev/null || { rm -f "$t" 2>/dev/null || true; continue; }
chmod 700 "$t" 2>/dev/null || { rm -f "$t" 2>/dev/null || true; continue; }
if "$t" >/dev/null 2>&1; then
rm -f "$t" 2>/dev/null || true
echo "$d"
return 0
fi
rm -f "$t" 2>/dev/null || true
fi
done
return 1
local d
for d in /dev/shm /run /var/run /mnt /root /; do
if [ -d "$d" ] && [ -w "$d" ]; then
local t="${d%/}/.exec_test_$$"
printf '#!/bin/sh\necho ok\n' > "$t" 2> /dev/null || {
rm -f "$t" 2> /dev/null || true
continue
}
chmod 700 "$t" 2> /dev/null || {
rm -f "$t" 2> /dev/null || true
continue
}
if "$t" > /dev/null 2>&1; then
rm -f "$t" 2> /dev/null || true
echo "$d"
return 0
fi
rm -f "$t" 2> /dev/null || true
fi
done
return 1
}
EXEC_DIR="$(pick_exec_dir || true)"
if [ -z "${EXEC_DIR:-}" ]; then
echo "ERROR: Could not find an exec-capable writable directory."
exit 1
echo "ERROR: Could not find an exec-capable writable directory."
exit 1
fi
######################
@@ -35,14 +41,14 @@ fi
######################
candidate_shebangs=(
"/command/with-contenv bashio"
"/usr/bin/with-contenv bashio"
"/usr/bin/env bashio"
"/usr/bin/bashio"
"/usr/bin/bash"
"/bin/bash"
"/usr/bin/sh"
"/bin/sh"
"/command/with-contenv bashio"
"/usr/bin/with-contenv bashio"
"/usr/bin/env bashio"
"/usr/bin/bashio"
"/usr/bin/bash"
"/bin/bash"
"/usr/bin/sh"
"/bin/sh"
)
SHEBANG_ERRORS=()
@@ -85,76 +91,76 @@ echo "${_bv:-PROBE_OK}"
'
validate_shebang() {
local candidate="$1"
local tmp out rc
local errfile msg
local candidate="$1"
local tmp out rc
local errfile msg
# shellcheck disable=SC2206
local cmd=( $candidate )
local exe="${cmd[0]}"
# shellcheck disable=SC2206
local cmd=($candidate)
local exe="${cmd[0]}"
if [ ! -x "$exe" ]; then
SHEBANG_ERRORS+=(" - FAIL (not executable): #!$candidate")
if [ ! -x "$exe" ]; then
SHEBANG_ERRORS+=(" - FAIL (not executable): #!$candidate")
return 1
fi
tmp="${EXEC_DIR%/}/shebang_test.$$.$RANDOM"
errfile="${EXEC_DIR%/}/shebang_probe_err.$$"
{
printf '#!%s\n' "$candidate"
printf '%s\n' "$probe_script_content"
} > "$tmp"
chmod 700 "$tmp" 2> /dev/null || true
set +e
out="$("$tmp" 2> "$errfile")"
rc=$?
set -e
rm -f "$tmp" 2> /dev/null || true
if [ "$rc" -eq 0 ] && [ -n "${out:-}" ] && [ "$out" != "null" ]; then
rm -f "$errfile" 2> /dev/null || true
return 0
fi
msg=$' - FAIL: #!'"$candidate"$'\n'" rc=$rc, stdout='${out:-}'"$'\n'
if [ -s "$errfile" ]; then
msg+=$' stderr:\n'
msg+="$(sed -n '1,8p' "$errfile")"$'\n'
else
msg+=$' stderr: <empty>\n'
fi
SHEBANG_ERRORS+=("$msg")
rm -f "$errfile" 2> /dev/null || true
return 1
fi
tmp="${EXEC_DIR%/}/shebang_test.$$.$RANDOM"
errfile="${EXEC_DIR%/}/shebang_probe_err.$$"
{
printf '#!%s\n' "$candidate"
printf '%s\n' "$probe_script_content"
} >"$tmp"
chmod 700 "$tmp" 2>/dev/null || true
set +e
out="$("$tmp" 2>"$errfile")"
rc=$?
set -e
rm -f "$tmp" 2>/dev/null || true
if [ "$rc" -eq 0 ] && [ -n "${out:-}" ] && [ "$out" != "null" ]; then
rm -f "$errfile" 2>/dev/null || true
return 0
fi
msg=$' - FAIL: #!'"$candidate"$'\n'" rc=$rc, stdout='${out:-}'"$'\n'
if [ -s "$errfile" ]; then
msg+=$' stderr:\n'
msg+="$(sed -n '1,8p' "$errfile")"$'\n'
else
msg+=$' stderr: <empty>\n'
fi
SHEBANG_ERRORS+=("$msg")
rm -f "$errfile" 2>/dev/null || true
return 1
}
shebang=""
for candidate in "${candidate_shebangs[@]}"; do
if validate_shebang "$candidate"; then
shebang="$candidate"
break
fi
if validate_shebang "$candidate"; then
shebang="$candidate"
break
fi
done
if [ -z "$shebang" ]; then
echo "ERROR: No valid shebang found." >&2
printf ' - %s\n' "${candidate_shebangs[@]}" >&2
if [ "${#SHEBANG_ERRORS[@]}" -gt 0 ]; then
printf '%s\n' "${SHEBANG_ERRORS[@]}" >&2
fi
exit 1
echo "ERROR: No valid shebang found." >&2
printf ' - %s\n' "${candidate_shebangs[@]}" >&2
if [ "${#SHEBANG_ERRORS[@]}" -gt 0 ]; then
printf '%s\n' "${SHEBANG_ERRORS[@]}" >&2
fi
exit 1
fi
if ! command -v bashio::addon.version >/dev/null 2>&1; then
for f in /usr/lib/bashio/bashio.sh /usr/lib/bashio/lib.sh /usr/src/bashio/bashio.sh /usr/local/lib/bashio/bashio.sh /usr/local/lib/bashio-standalone.sh; do
if [ -f "$f" ]; then
# shellcheck disable=SC1090
. "$f"
break
fi
done
if ! command -v bashio::addon.version > /dev/null 2>&1; then
for f in /usr/lib/bashio/bashio.sh /usr/lib/bashio/lib.sh /usr/src/bashio/bashio.sh /usr/local/lib/bashio/bashio.sh /usr/local/lib/bashio-standalone.sh; do
if [ -f "$f" ]; then
# shellcheck disable=SC1090
. "$f"
break
fi
done
fi
##################
@@ -162,9 +168,9 @@ fi
##################
# Exit if /config is not mounted or HA not used
if [ ! -d /config ] || ! bashio::supervisor.ping 2>/dev/null; then
echo "..."
exit 0
if [ ! -d /config ] || ! bashio::supervisor.ping 2> /dev/null; then
echo "..."
exit 0
fi
# Define slug
@@ -173,11 +179,11 @@ 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"
CONFIGLOCATION="/config"
CONFIGFILEBROWSER="/addon_configs/${HOSTNAME/-/_}/$slug.sh"
else
CONFIGLOCATION="/config/addons_autoscripts"
CONFIGFILEBROWSER="/homeassistant/addons_autoscripts/$slug.sh"
CONFIGLOCATION="/config/addons_autoscripts"
CONFIGFILEBROWSER="/homeassistant/addons_autoscripts/$slug.sh"
fi
# Default location
@@ -189,28 +195,28 @@ bashio::log.green "Execute $CONFIGFILEBROWSER if existing"
# Download template if no script found and exit
if [ ! -f "$CONFIGSOURCE" ]; then
TEMPLATESOURCE="https://raw.githubusercontent.com/alexbelgium/hassio-addons/master/.templates/script.template"
curl -f -L -s -S "$TEMPLATESOURCE" --output "$CONFIGSOURCE" || true
exit 0
TEMPLATESOURCE="https://raw.githubusercontent.com/alexbelgium/hassio-addons/master/.templates/script.template"
curl -f -L -s -S "$TEMPLATESOURCE" --output "$CONFIGSOURCE" || true
exit 0
fi
# Convert scripts to linux
dos2unix "$CONFIGSOURCE" >/dev/null 2>&1 || true
dos2unix "$CONFIGSOURCE" > /dev/null 2>&1 || true
chmod +x "$CONFIGSOURCE"
if grep -q 'bashio' "$CONFIGSOURCE"; then
sed -i "1s|^.*|#!$shebang|" "$CONFIGSOURCE"
sed -i "1s|^.*|#!$shebang|" "$CONFIGSOURCE"
else
sed -i '1s|^.*|#!/bin/bash|' "$CONFIGSOURCE"
sed -i '1s|^.*|#!/bin/bash|' "$CONFIGSOURCE"
fi
# Check if there is actual commands
while IFS= read -r line; do
line="$(echo "$line" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')"
line="$(echo "$line" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')"
if [[ -n "$line" ]] && [[ ! "$line" =~ ^# ]]; then
bashio::log.green "... script found, executing"
"$CONFIGSOURCE"
break
fi
if [[ -n "$line" ]] && [[ ! "$line" =~ ^# ]]; then
bashio::log.green "... script found, executing"
"$CONFIGSOURCE"
break
fi
done < "$CONFIGSOURCE"

View File

@@ -9,12 +9,12 @@ set -e
# Avoid usage of local dns such as adguard home or pihole\n"
if bashio::config.has_value 'DNS_server'; then
if ! bashio::config.true "openvpn_enabled" && ! bashio::config.true "wireguard_enabled"; then
if ! bashio::config.true "openvpn_enabled" && ! bashio::config.true "wireguard_enabled"; then
# Define variables
DNSSERVER=$(bashio::config 'DNS_server')
DNS=""
DNSLIST=""
# Get DNS servers
# shellcheck disable=SC2086
for server in ${DNSSERVER//,/ }; do # Separate comma separated values
@@ -26,7 +26,7 @@ if bashio::config.has_value 'DNS_server'; then
bashio::log.warning "DNS $server was requested but can't be pinged. It won't be used"
fi
done
# Only add DNS if there are DNS set
# shellcheck disable=SC2236
if [[ -n "${DNS:-}" ]]; then

View File

@@ -33,293 +33,297 @@ _BASHIO_COLOR=1
[ "${TERM:-}" = "dumb" ] && _BASHIO_COLOR=0
_bashio_color() {
[ "$_BASHIO_COLOR" = "1" ] || return 0
case "${1:-}" in
blue) printf '\033[34m' ;;
green) printf '\033[32m' ;;
yellow) printf '\033[33m' ;;
red) printf '\033[31m' ;;
magenta) printf '\033[35m' ;;
reset) printf '\033[0m' ;;
*) printf '' ;;
esac
[ "$_BASHIO_COLOR" = "1" ] || return 0
case "${1:-}" in
blue) printf '\033[34m' ;;
green) printf '\033[32m' ;;
yellow) printf '\033[33m' ;;
red) printf '\033[31m' ;;
magenta) printf '\033[35m' ;;
reset) printf '\033[0m' ;;
*) printf '' ;;
esac
}
_bashio_log() {
local c="${1:-}"; shift || true
printf '%s%s%s\n' "$(_bashio_color "$c")" "$*" "$(_bashio_color reset)"
local c="${1:-}"
shift || true
printf '%s%s%s\n' "$(_bashio_color "$c")" "$*" "$(_bashio_color reset)"
}
# -----------------------------------------------------------------------------
# Helpers
# -----------------------------------------------------------------------------
_bashio_is_true() {
case "${1:-}" in
1|true|TRUE|True|yes|YES|Yes|on|ON|On) return 0 ;;
*) return 1 ;;
esac
case "${1:-}" in
1 | true | TRUE | True | yes | YES | Yes | on | ON | On) return 0 ;;
*) return 1 ;;
esac
}
# ENV mapping helper:
# tries variants + prefixes and prints the value if env var is defined (even empty),
# returning 0 when found, 1 when not found.
_bashio_env_get() {
local key="${1:-}"
[ -n "$key" ] || return 1
local key="${1:-}"
[ -n "$key" ] || return 1
local norm norm_uc raw_uc
norm="$(printf '%s' "$key" | tr '.-' '__')"
norm_uc="$(printf '%s' "$norm" | tr '[:lower:]' '[:upper:]')"
raw_uc="$(printf '%s' "$key" | tr '[:lower:]' '[:upper:]')"
local norm norm_uc raw_uc
norm="$(printf '%s' "$key" | tr '.-' '__')"
norm_uc="$(printf '%s' "$norm" | tr '[:lower:]' '[:upper:]')"
raw_uc="$(printf '%s' "$key" | tr '[:lower:]' '[:upper:]')"
local variants=(
"$key"
"$raw_uc"
"$norm"
"$norm_uc"
)
local variants=(
"$key"
"$raw_uc"
"$norm"
"$norm_uc"
)
local prefixes=("" "CFG_" "CONFIG_" "ADDON_" "OPTION_" "OPT_")
local prefixes=("" "CFG_" "CONFIG_" "ADDON_" "OPTION_" "OPT_")
local v p name
for v in "${variants[@]}"; do
for p in "${prefixes[@]}"; do
name="${p}${v}"
if [ -n "${!name+x}" ]; then
printf '%s' "${!name}"
return 0
fi
local v p name
for v in "${variants[@]}"; do
for p in "${prefixes[@]}"; do
name="${p}${v}"
if [ -n "${!name+x}" ]; then
printf '%s' "${!name}"
return 0
fi
done
done
done
return 1
return 1
}
# env presence (even if empty) used by config.exists
_bashio_env_has() {
local key="${1:-}"
[ -n "$key" ] || return 1
_bashio_env_get "$key" >/dev/null 2>&1
local key="${1:-}"
[ -n "$key" ] || return 1
_bashio_env_get "$key" > /dev/null 2>&1
}
# JSON options source (jq required). Prints value or empty; returns 0 always.
_bashio_json_get() {
local key="${1:-}"
local file="${STANDALONE_OPTIONS_JSON:-}"
local key="${1:-}"
local file="${STANDALONE_OPTIONS_JSON:-}"
[ -n "$key" ] || return 0
[ -n "$file" ] || return 0
[ -f "$file" ] || return 0
command -v jq >/dev/null 2>&1 || return 0
[ -n "$key" ] || return 0
[ -n "$file" ] || return 0
[ -f "$file" ] || return 0
command -v jq > /dev/null 2>&1 || return 0
# getpath(split(".")) supports nested access; missing => empty
jq -er --arg k "$key" 'getpath(($k|split("."))) // empty' "$file" 2>/dev/null || true
# getpath(split(".")) supports nested access; missing => empty
jq -er --arg k "$key" 'getpath(($k|split("."))) // empty' "$file" 2> /dev/null || true
}
# Net wait using /dev/tcp with a timeout
_bashio_tcp_wait() {
local host="${1:-}" port="${2:-}" to="${3:-30}"
[ -n "$host" ] && [ -n "$port" ] || return 1
local host="${1:-}" port="${2:-}" to="${3:-30}"
[ -n "$host" ] && [ -n "$port" ] || return 1
local start now
start="$(date +%s)"
while :; do
if exec 3<>"/dev/tcp/${host}/${port}" 2>/dev/null; then
exec 3>&- 3<&-
return 0
fi
now="$(date +%s)"
if [ $((now - start)) -ge "$to" ]; then
return 1
fi
sleep 1
done
local start now
start="$(date +%s)"
while :; do
if exec 3<> "/dev/tcp/${host}/${port}" 2> /dev/null; then
exec 3>&- 3<&-
return 0
fi
now="$(date +%s)"
if [ $((now - start)) -ge "$to" ]; then
return 1
fi
sleep 1
done
}
# Prefer nc if present, fallback to /dev/tcp
_bashio_tcp_wait_nc() {
command -v nc >/dev/null 2>&1 || return 1
local host="${1:-}" port="${2:-}" to="${3:-30}"
# BusyBox and OpenBSD nc differ; cover both styles
nc -z -w "$to" "$host" "$port" 2>/dev/null || nc -z "$host" "$port" 2>/dev/null
command -v nc > /dev/null 2>&1 || return 1
local host="${1:-}" port="${2:-}" to="${3:-30}"
# BusyBox and OpenBSD nc differ; cover both styles
nc -z -w "$to" "$host" "$port" 2> /dev/null || nc -z "$host" "$port" 2> /dev/null
}
# -----------------------------------------------------------------------------
# Logging API
# -----------------------------------------------------------------------------
bashio::log.blue() { _bashio_log blue "$*"; }
bashio::log.green() { _bashio_log green "$*"; }
bashio::log.yellow() { _bashio_log yellow "$*"; }
bashio::log.red() { _bashio_log red "$*"; }
bashio::log.blue() { _bashio_log blue "$*"; }
bashio::log.green() { _bashio_log green "$*"; }
bashio::log.yellow() { _bashio_log yellow "$*"; }
bashio::log.red() { _bashio_log red "$*"; }
bashio::log.magenta() { _bashio_log magenta "$*"; }
# Common aliases
bashio::log.info() { bashio::log.blue "$@"; }
bashio::log.info() { bashio::log.blue "$@"; }
bashio::log.warning() { bashio::log.yellow "$@"; }
bashio::log.error() { bashio::log.red "$@"; }
bashio::log.debug() { printf '%s\n' "$*"; }
bashio::log.error() { bashio::log.red "$@"; }
bashio::log.debug() { printf '%s\n' "$*"; }
# -----------------------------------------------------------------------------
# Supervisor shim
# -----------------------------------------------------------------------------
bashio::supervisor.ping() {
_bashio_is_true "${STANDALONE_FORCE_SUPERVISOR_PING:-}" && return 0
return 1
_bashio_is_true "${STANDALONE_FORCE_SUPERVISOR_PING:-}" && return 0
return 1
}
# -----------------------------------------------------------------------------
# Add-on metadata
# -----------------------------------------------------------------------------
bashio::addon.name() { printf '%s' "${ADDON_NAME:-Standalone container}"; }
bashio::addon.description() { printf '%s' "${ADDON_DESCRIPTION:-Running without Home Assistant Supervisor}"; }
bashio::addon.version() { printf '%s' "${BUILD_VERSION:-1.0}"; }
bashio::addon.version_latest(){ printf '%s' "${ADDON_VERSION_LATEST:-${BUILD_VERSION:-1.0}}"; }
bashio::addon.name() { printf '%s' "${ADDON_NAME:-Standalone container}"; }
bashio::addon.description() { printf '%s' "${ADDON_DESCRIPTION:-Running without Home Assistant Supervisor}"; }
bashio::addon.version() { printf '%s' "${BUILD_VERSION:-1.0}"; }
bashio::addon.version_latest() { printf '%s' "${ADDON_VERSION_LATEST:-${BUILD_VERSION:-1.0}}"; }
bashio::addon.update_available() {
if [ -n "${ADDON_VERSION_LATEST:-}" ] && [ "${ADDON_VERSION_LATEST:-}" != "${BUILD_VERSION:-}" ]; then
printf '%s' "true"
else
printf '%s' "false"
fi
if [ -n "${ADDON_VERSION_LATEST:-}" ] && [ "${ADDON_VERSION_LATEST:-}" != "${BUILD_VERSION:-}" ]; then
printf '%s' "true"
else
printf '%s' "false"
fi
}
bashio::addon.ingress_port() { printf '%s' "${ADDON_INGRESS_PORT:-}"; }
bashio::addon.ingress_port() { printf '%s' "${ADDON_INGRESS_PORT:-}"; }
bashio::addon.ingress_entry() { printf '%s' "${ADDON_INGRESS_ENTRY:-}"; }
bashio::addon.ip_address() { printf '%s' "${ADDON_IP_ADDRESS:-}"; }
bashio::addon.ip_address() { printf '%s' "${ADDON_IP_ADDRESS:-}"; }
# Ports:
# - numeric arg "8080" -> env PORT_8080 or ADDON_PORT_8080, fallback to the number
# - non-numeric "WEB_PORT" -> resolve as config/env key
bashio::addon.port() {
local arg="${1:-}"
if [[ "$arg" =~ ^[0-9]+$ ]]; then
local v=""
v="$(_bashio_env_get "PORT_${arg}" 2>/dev/null || true)"
[ -z "$v" ] && v="$(_bashio_env_get "ADDON_PORT_${arg}" 2>/dev/null || true)"
printf '%s' "${v:-$arg}"
else
printf '%s' "$(_bashio_env_get "$arg" 2>/dev/null || true)"
fi
local arg="${1:-}"
if [[ "$arg" =~ ^[0-9]+$ ]]; then
local v=""
v="$(_bashio_env_get "PORT_${arg}" 2> /dev/null || true)"
[ -z "$v" ] && v="$(_bashio_env_get "ADDON_PORT_${arg}" 2> /dev/null || true)"
printf '%s' "${v:-$arg}"
else
printf '%s' "$(_bashio_env_get "$arg" 2> /dev/null || true)"
fi
}
# addon.option : write/delete option in JSON when possible; fallback export env
bashio::addon.option() {
local key="${1:-}" value="${2-__BASHIO_UNSET__}" file="${STANDALONE_OPTIONS_JSON:-}"
[ -n "$key" ] || return 0
local key="${1:-}" value="${2-__BASHIO_UNSET__}" file="${STANDALONE_OPTIONS_JSON:-}"
[ -n "$key" ] || return 0
if [ -n "$file" ] && [ -f "$file" ] && command -v jq >/dev/null 2>&1; then
local tmp
tmp="$(mktemp)"
if [ "$value" = "__BASHIO_UNSET__" ]; then
jq --arg k "$key" 'delpath(($k|split(".")))' "$file" >"$tmp" && mv "$tmp" "$file"
else
jq --arg k "$key" --arg v "$value" 'setpath(($k|split(".")); $v)' "$file" >"$tmp" && mv "$tmp" "$file"
if [ -n "$file" ] && [ -f "$file" ] && command -v jq > /dev/null 2>&1; then
local tmp
tmp="$(mktemp)"
if [ "$value" = "__BASHIO_UNSET__" ]; then
jq --arg k "$key" 'delpath(($k|split(".")))' "$file" > "$tmp" && mv "$tmp" "$file"
else
jq --arg k "$key" --arg v "$value" 'setpath(($k|split(".")); $v)' "$file" > "$tmp" && mv "$tmp" "$file"
fi
return 0
fi
return 0
fi
# Fallback: export as env (dot/dash -> underscore). Delete becomes no-op.
if [ "$value" != "__BASHIO_UNSET__" ]; then
export "$(printf '%s' "$key" | tr '.-' '__')"="$value"
fi
# Fallback: export as env (dot/dash -> underscore). Delete becomes no-op.
if [ "$value" != "__BASHIO_UNSET__" ]; then
export "$(printf '%s' "$key" | tr '.-' '__')"="$value"
fi
}
# -----------------------------------------------------------------------------
# System info
# -----------------------------------------------------------------------------
bashio::info.operating_system() {
if [ -r /etc/os-release ]; then
# shellcheck disable=SC1091
. /etc/os-release
printf '%s' "${PRETTY_NAME:-${NAME:-Linux}}"
else
printf '%s' "Linux"
fi
if [ -r /etc/os-release ]; then
# shellcheck disable=SC1091
. /etc/os-release
printf '%s' "${PRETTY_NAME:-${NAME:-Linux}}"
else
printf '%s' "Linux"
fi
}
bashio::info.arch() { uname -m; }
bashio::info.machine() { uname -m; }
bashio::info.homeassistant(){ printf '%s' "standalone"; }
bashio::info.supervisor() { printf '%s' "standalone"; }
bashio::info.arch() { uname -m; }
bashio::info.machine() { uname -m; }
bashio::info.homeassistant() { printf '%s' "standalone"; }
bashio::info.supervisor() { printf '%s' "standalone"; }
# -----------------------------------------------------------------------------
# Config API
# -----------------------------------------------------------------------------
bashio::config() {
local key="${1:-}"
[ -n "$key" ] || { printf '%s' ""; return 0; }
local key="${1:-}"
[ -n "$key" ] || {
printf '%s' ""
return 0
}
local v=""
if _bashio_env_get "$key" >/dev/null 2>&1; then
v="$(_bashio_env_get "$key" 2>/dev/null || true)"
fi
[ -z "$v" ] && v="$(_bashio_json_get "$key")"
printf '%s' "${v:-}"
local v=""
if _bashio_env_get "$key" > /dev/null 2>&1; then
v="$(_bashio_env_get "$key" 2> /dev/null || true)"
fi
[ -z "$v" ] && v="$(_bashio_json_get "$key")"
printf '%s' "${v:-}"
}
bashio::config.has_value() { [ -n "$(bashio::config "$1")" ]; }
bashio::config.true() {
_bashio_is_true "$(bashio::config "$1")"
_bashio_is_true "$(bashio::config "$1")"
}
# config.exists : key is present (env or JSON), even if value is empty
bashio::config.exists() {
local key="${1:-}" file="${STANDALONE_OPTIONS_JSON:-}"
[ -n "$key" ] || return 1
local key="${1:-}" file="${STANDALONE_OPTIONS_JSON:-}"
[ -n "$key" ] || return 1
if _bashio_env_has "$key"; then
return 0
fi
if _bashio_env_has "$key"; then
return 0
fi
if [ -n "$file" ] && [ -f "$file" ] && command -v jq >/dev/null 2>&1; then
jq -e --arg k "$key" 'haspath(($k|split(".")))' "$file" >/dev/null 2>&1
return $?
fi
if [ -n "$file" ] && [ -f "$file" ] && command -v jq > /dev/null 2>&1; then
jq -e --arg k "$key" 'haspath(($k|split(".")))' "$file" > /dev/null 2>&1
return $?
fi
return 1
return 1
}
# Common "require.*" shims (advisory/no-op in standalone)
bashio::config.require.ssl() { printf '%s' "${REQUIRE_SSL:-true}"; }
bashio::config.require.ssl() { printf '%s' "${REQUIRE_SSL:-true}"; }
bashio::config.require.username() { :; }
bashio::config.require.password() { :; }
bashio::config.require.port() { :; }
bashio::config.require.port() { :; }
# config.array:
# Accepts CSV ("a,b,c"), space/newline-separated text, or JSON array ["a","b"].
# Prints one item per line.
bashio::config.array() {
local key="${1:-}" raw
raw="$(bashio::config "$key")"
[ -n "$raw" ] || return 0
local key="${1:-}" raw
raw="$(bashio::config "$key")"
[ -n "$raw" ] || return 0
if command -v jq >/dev/null 2>&1 && printf '%s' "$raw" | jq -e . >/dev/null 2>&1; then
printf '%s' "$raw" | jq -r '.[]' 2>/dev/null && return 0
fi
if command -v jq > /dev/null 2>&1 && printf '%s' "$raw" | jq -e . > /dev/null 2>&1; then
printf '%s' "$raw" | jq -r '.[]' 2> /dev/null && return 0
fi
if printf '%s' "$raw" | grep -q ','; then
printf '%s' "$raw" | tr ',' '\n'
return 0
fi
if printf '%s' "$raw" | grep -q ','; then
printf '%s' "$raw" | tr ',' '\n'
return 0
fi
printf '%s\n' "$raw"
printf '%s\n' "$raw"
}
# -----------------------------------------------------------------------------
# var helpers
# -----------------------------------------------------------------------------
bashio::var.true() { _bashio_is_true "${1:-}"; }
bashio::var.false() { ! _bashio_is_true "${1:-}"; }
bashio::var.true() { _bashio_is_true "${1:-}"; }
bashio::var.false() { ! _bashio_is_true "${1:-}"; }
bashio::var.has_value() { [ -n "${1:-}" ]; }
# -----------------------------------------------------------------------------
# Filesystem helpers
# -----------------------------------------------------------------------------
bashio::fs.file_exists() { [ -f "${1:-}" ]; }
bashio::fs.file_exists() { [ -f "${1:-}" ]; }
bashio::fs.directory_exists() { [ -d "${1:-}" ]; }
bashio::fs.file_contains() {
local f="${1:-}" p="${2:-}"
[ -f "$f" ] && grep -q -- "$p" "$f" 2>/dev/null
local f="${1:-}" p="${2:-}"
[ -f "$f" ] && grep -q -- "$p" "$f" 2> /dev/null
}
# -----------------------------------------------------------------------------
@@ -327,25 +331,25 @@ bashio::fs.file_contains() {
# -----------------------------------------------------------------------------
# Wait for TCP service: bashio::net.wait_for host port [timeout]
bashio::net.wait_for() {
local host="${1:-}" port="${2:-}" to="${3:-30}"
_bashio_tcp_wait_nc "$host" "$port" "$to" && return 0
_bashio_tcp_wait "$host" "$port" "$to"
local host="${1:-}" port="${2:-}" to="${3:-30}"
_bashio_tcp_wait_nc "$host" "$port" "$to" && return 0
_bashio_tcp_wait "$host" "$port" "$to"
}
# DNS helper: bashio::dns.host <hostname> -> prints an IP (or empty)
bashio::dns.host() {
local h="${1:-}"
[ -n "$h" ] || return 1
if command -v getent >/dev/null 2>&1; then
getent ahostsv4 "$h" | awk '{print $1; exit}'
else
nslookup "$h" 2>/dev/null | awk '/^Address: /{print $2; exit}'
fi
local h="${1:-}"
[ -n "$h" ] || return 1
if command -v getent > /dev/null 2>&1; then
getent ahostsv4 "$h" | awk '{print $1; exit}'
else
nslookup "$h" 2> /dev/null | awk '/^Address: /{print $2; exit}'
fi
}
# Hostname
bashio::host.hostname() {
command -v hostname >/dev/null 2>&1 && hostname || printf '%s' "${HOSTNAME:-unknown}"
command -v hostname > /dev/null 2>&1 && hostname || printf '%s' "${HOSTNAME:-unknown}"
}
# -----------------------------------------------------------------------------
@@ -355,40 +359,43 @@ bashio::host.hostname() {
# bashio::services "mqtt" "host"
# bashio::services.available "mqtt"
bashio::services() {
local svc="${1:-}" key="${2:-}"
[ -n "$svc" ] && [ -n "$key" ] || { printf '%s' ""; return 0; }
local svc="${1:-}" key="${2:-}"
[ -n "$svc" ] && [ -n "$key" ] || {
printf '%s' ""
return 0
}
local upper svc_upper var v=""
upper="$(printf '%s' "$key" | tr '[:lower:]' '[:upper:]')"
svc_upper="$(printf '%s' "$svc" | tr '[:lower:]' '[:upper:]')"
local upper svc_upper var v=""
upper="$(printf '%s' "$key" | tr '[:lower:]' '[:upper:]')"
svc_upper="$(printf '%s' "$svc" | tr '[:lower:]' '[:upper:]')"
# Common mappings
case "$svc_upper:$upper" in
MQTT:HOST) var="MQTT_HOST" ;;
MQTT:PORT) var="MQTT_PORT" ;;
MQTT:USERNAME) var="MQTT_USER" ;;
MQTT:PASSWORD) var="MQTT_PASSWORD" ;;
MQTT:TLS) var="MQTT_TLS" ;;
MYSQL:HOST|MARIADB:HOST) var="DB_HOST" ;;
MYSQL:PORT|MARIADB:PORT) var="DB_PORT" ;;
MYSQL:USERNAME|MARIADB:USERNAME) var="DB_USER" ;;
MYSQL:PASSWORD|MARIADB:PASSWORD) var="DB_PASSWORD" ;;
MYSQL:DATABASE|MARIADB:DATABASE) var="DB_NAME" ;;
*) var="${svc_upper}_${upper}" ;;
esac
# Common mappings
case "$svc_upper:$upper" in
MQTT:HOST) var="MQTT_HOST" ;;
MQTT:PORT) var="MQTT_PORT" ;;
MQTT:USERNAME) var="MQTT_USER" ;;
MQTT:PASSWORD) var="MQTT_PASSWORD" ;;
MQTT:TLS) var="MQTT_TLS" ;;
MYSQL:HOST | MARIADB:HOST) var="DB_HOST" ;;
MYSQL:PORT | MARIADB:PORT) var="DB_PORT" ;;
MYSQL:USERNAME | MARIADB:USERNAME) var="DB_USER" ;;
MYSQL:PASSWORD | MARIADB:PASSWORD) var="DB_PASSWORD" ;;
MYSQL:DATABASE | MARIADB:DATABASE) var="DB_NAME" ;;
*) var="${svc_upper}_${upper}" ;;
esac
v="$(_bashio_env_get "$var" 2>/dev/null || true)"
if [ -z "$v" ]; then
v="$(_bashio_json_get "services.${svc}.${key}")"
[ -z "$v" ] && v="$(_bashio_json_get "${svc}.${key}")"
fi
printf '%s' "${v:-}"
v="$(_bashio_env_get "$var" 2> /dev/null || true)"
if [ -z "$v" ]; then
v="$(_bashio_json_get "services.${svc}.${key}")"
[ -z "$v" ] && v="$(_bashio_json_get "${svc}.${key}")"
fi
printf '%s' "${v:-}"
}
bashio::services.available() {
local svc="${1:-}" host
host="$(bashio::services "$svc" "host")"
[ -n "$host" ]
local svc="${1:-}" host
host="$(bashio::services "$svc" "host")"
[ -n "$host" ]
}
# -----------------------------------------------------------------------------
@@ -396,39 +403,43 @@ bashio::services.available() {
# -----------------------------------------------------------------------------
mkdir -p "$BASHIO_CACHE_DIR"
bashio::cache.exists() { [ -f "$BASHIO_CACHE_DIR/${1}.cache" ]; }
bashio::cache.get() { [ -f "$BASHIO_CACHE_DIR/${1}.cache" ] && cat "$BASHIO_CACHE_DIR/${1}.cache"; }
bashio::cache.set() { printf '%s' "${2:-}" > "$BASHIO_CACHE_DIR/${1}.cache"; }
bashio::cache.get() { [ -f "$BASHIO_CACHE_DIR/${1}.cache" ] && cat "$BASHIO_CACHE_DIR/${1}.cache"; }
bashio::cache.set() { printf '%s' "${2:-}" > "$BASHIO_CACHE_DIR/${1}.cache"; }
# -----------------------------------------------------------------------------
# jq wrapper (some add-ons call bashio::jq)
# -----------------------------------------------------------------------------
bashio::jq() { command -v jq >/dev/null 2>&1 && jq "$@"; }
bashio::jq() { command -v jq > /dev/null 2>&1 && jq "$@"; }
# -----------------------------------------------------------------------------
# Home Assistant token
# -----------------------------------------------------------------------------
bashio::homeassistant.token() {
local t="${HOMEASSISTANT_TOKEN:-${HASS_TOKEN:-}}"
if [ -z "$t" ] && [ -n "${STANDALONE_OPTIONS_JSON:-}" ] && [ -f "${STANDALONE_OPTIONS_JSON:-}" ] && command -v jq >/dev/null 2>&1; then
t="$(jq -er '.homeassistant.token // empty' "$STANDALONE_OPTIONS_JSON" 2>/dev/null || true)"
fi
printf '%s' "${t:-}"
local t="${HOMEASSISTANT_TOKEN:-${HASS_TOKEN:-}}"
if [ -z "$t" ] && [ -n "${STANDALONE_OPTIONS_JSON:-}" ] && [ -f "${STANDALONE_OPTIONS_JSON:-}" ] && command -v jq > /dev/null 2>&1; then
t="$(jq -er '.homeassistant.token // empty' "$STANDALONE_OPTIONS_JSON" 2> /dev/null || true)"
fi
printf '%s' "${t:-}"
}
# -----------------------------------------------------------------------------
# Exit helpers
# -----------------------------------------------------------------------------
bashio::exit.ok() { exit 0; }
bashio::exit.nok() { local m="${1:-}"; [ -n "$m" ] && bashio::log.red "$m"; exit 1; }
bashio::exit.ok() { exit 0; }
bashio::exit.nok() {
local m="${1:-}"
[ -n "$m" ] && bashio::log.red "$m"
exit 1
}
# -----------------------------------------------------------------------------
# Core config check shim
# -----------------------------------------------------------------------------
# Set STANDALONE_CORE_CHECK_CMD="hass --script check_config -c /config" to enable
bashio::core.check() {
if [ -n "${STANDALONE_CORE_CHECK_CMD:-}" ]; then
eval "$STANDALONE_CORE_CHECK_CMD"
else
return 0
fi
if [ -n "${STANDALONE_CORE_CHECK_CMD:-}" ]; then
eval "$STANDALONE_CORE_CHECK_CMD"
else
return 0
fi
}

View File

@@ -41,14 +41,14 @@ restore_apt_sources_if_missing() {
mirror="http://ports.ubuntu.com/ubuntu-ports"
security_mirror="$mirror"
fi
cat > /etc/apt/sources.list <<EOF2
cat > /etc/apt/sources.list << EOF2
deb ${mirror} ${codename} main restricted universe multiverse
deb ${mirror} ${codename}-updates main restricted universe multiverse
deb ${security_mirror} ${codename}-security main restricted universe multiverse
EOF2
;;
debian)
cat > /etc/apt/sources.list <<EOF2
cat > /etc/apt/sources.list << EOF2
deb http://deb.debian.org/debian ${codename} main contrib non-free non-free-firmware
deb http://deb.debian.org/debian ${codename}-updates main contrib non-free non-free-firmware
deb http://deb.debian.org/debian-security ${codename}-security main contrib non-free non-free-firmware

View File

@@ -184,7 +184,7 @@ done
if [ "$PACKMANAGER" = "apt" ]; then apt-get update > /dev/null; fi
if [ "$PACKMANAGER" = "pacman" ]; then pacman -Sy > /dev/null; fi
if [ "$PACKMANAGER" = "apk" ] && [ -f /etc/apk/repositories ] && ! grep -q "community" /etc/apk/repositories; then
ALPINE_VER=$(cat /etc/alpine-release 2>/dev/null | cut -d. -f1,2)
ALPINE_VER=$(cat /etc/alpine-release 2> /dev/null | cut -d. -f1,2)
if [ -n "$ALPINE_VER" ]; then
echo "https://dl-cdn.alpinelinux.org/alpine/v${ALPINE_VER}/community" >> /etc/apk/repositories
apk update > /dev/null
@@ -228,7 +228,7 @@ for files in "/etc/services.d" "/etc/cont-init.d"; do
if ! ls $files 1> /dev/null 2>&1; then continue; fi
# Bashio
if grep -q -rnw "${files}" -e 'bashio' 2>/dev/null && [ ! -f "/usr/bin/bashio" ]; then
if grep -q -rnw "${files}" -e 'bashio' 2> /dev/null && [ ! -f "/usr/bin/bashio" ]; then
[ "$VERBOSE" = true ] && echo "install bashio"
BASHIO_VERSION="v0.17.5"
mkdir -p /tmp/bashio

View File

@@ -7,13 +7,13 @@
PID1=false
if [ "$$" -eq 1 ]; then
PID1=true
echo "Starting as entrypoint"
if [ -d /command ]; then
ln -sf /command/* /usr/bin/ 2>/dev/null || true
fi
PID1=true
echo "Starting as entrypoint"
if [ -d /command ]; then
ln -sf /command/* /usr/bin/ 2> /dev/null || true
fi
else
echo "Starting custom scripts"
echo "Starting custom scripts"
fi
##########################################
@@ -21,31 +21,37 @@ fi
##########################################
pick_exec_dir() {
# Prefer locations that are commonly exec-capable in containers
# and writable. Avoid /tmp because it may be mounted noexec.
local d
for d in /dev/shm /run /var/run /mnt /root /; do
if [ -d "$d" ] && [ -w "$d" ]; then
# Create a tiny test executable to confirm "exec" works
local t="${d%/}/.exec_test_$$"
printf '#!/bin/sh\necho ok\n' >"$t" 2>/dev/null || { rm -f "$t" 2>/dev/null || true; continue; }
chmod 700 "$t" 2>/dev/null || { rm -f "$t" 2>/dev/null || true; continue; }
if "$t" >/dev/null 2>&1; then
rm -f "$t" 2>/dev/null || true
echo "$d"
return 0
fi
rm -f "$t" 2>/dev/null || true
fi
done
return 1
# Prefer locations that are commonly exec-capable in containers
# and writable. Avoid /tmp because it may be mounted noexec.
local d
for d in /dev/shm /run /var/run /mnt /root /; do
if [ -d "$d" ] && [ -w "$d" ]; then
# Create a tiny test executable to confirm "exec" works
local t="${d%/}/.exec_test_$$"
printf '#!/bin/sh\necho ok\n' > "$t" 2> /dev/null || {
rm -f "$t" 2> /dev/null || true
continue
}
chmod 700 "$t" 2> /dev/null || {
rm -f "$t" 2> /dev/null || true
continue
}
if "$t" > /dev/null 2>&1; then
rm -f "$t" 2> /dev/null || true
echo "$d"
return 0
fi
rm -f "$t" 2> /dev/null || true
fi
done
return 1
}
EXEC_DIR="$(pick_exec_dir || true)"
if [ -z "${EXEC_DIR:-}" ]; then
echo "ERROR: Could not find an exec-capable writable directory (e.g., /dev/shm,/run)."
echo "Your environment likely mounts all writable dirs as noexec; shebang validation cannot run safely."
exit 1
echo "ERROR: Could not find an exec-capable writable directory (e.g., /dev/shm,/run)."
echo "Your environment likely mounts all writable dirs as noexec; shebang validation cannot run safely."
exit 1
fi
######################
@@ -53,14 +59,14 @@ fi
######################
candidate_shebangs=(
"/command/with-contenv bashio"
"/usr/bin/with-contenv bashio"
"/usr/bin/env bashio"
"/usr/bin/bashio"
"/usr/bin/bash"
"/bin/bash"
"/usr/bin/sh"
"/bin/sh"
"/command/with-contenv bashio"
"/usr/bin/with-contenv bashio"
"/usr/bin/env bashio"
"/usr/bin/bashio"
"/usr/bin/bash"
"/bin/bash"
"/usr/bin/sh"
"/bin/sh"
)
SHEBANG_ERRORS=()
@@ -104,68 +110,68 @@ echo "${_bv:-PROBE_OK}"
'
validate_shebang() {
local candidate="$1"
local tmp out rc
local errfile msg
local candidate="$1"
local tmp out rc
local errfile msg
# shellcheck disable=SC2206
local cmd=( $candidate )
local exe="${cmd[0]}"
# shellcheck disable=SC2206
local cmd=($candidate)
local exe="${cmd[0]}"
if [ ! -x "$exe" ]; then
SHEBANG_ERRORS+=(" - FAIL (not executable): #!$candidate")
if [ ! -x "$exe" ]; then
SHEBANG_ERRORS+=(" - FAIL (not executable): #!$candidate")
return 1
fi
tmp="${EXEC_DIR%/}/shebang_test.$$.$RANDOM"
errfile="${EXEC_DIR%/}/shebang_probe_err.$$"
{
printf '#!%s\n' "$candidate"
printf '%s\n' "$probe_script_content"
} > "$tmp"
chmod 700 "$tmp" 2> /dev/null || true
set +e
out="$("$tmp" 2> "$errfile")"
rc=$?
set -e
rm -f "$tmp" 2> /dev/null || true
if [ "$rc" -eq 0 ] && [ -n "${out:-}" ] && [ "$out" != "null" ]; then
rm -f "$errfile" 2> /dev/null || true
return 0
fi
msg=$' - FAIL: #!'"$candidate"$'\n'" rc=$rc, stdout='${out:-}'"$'\n'
if [ -s "$errfile" ]; then
msg+=$' stderr:\n'
msg+="$(sed -n '1,8p' "$errfile")"$'\n'
else
msg+=$' stderr: <empty>\n'
fi
SHEBANG_ERRORS+=("$msg")
rm -f "$errfile" 2> /dev/null || true
return 1
fi
tmp="${EXEC_DIR%/}/shebang_test.$$.$RANDOM"
errfile="${EXEC_DIR%/}/shebang_probe_err.$$"
{
printf '#!%s\n' "$candidate"
printf '%s\n' "$probe_script_content"
} >"$tmp"
chmod 700 "$tmp" 2>/dev/null || true
set +e
out="$("$tmp" 2>"$errfile")"
rc=$?
set -e
rm -f "$tmp" 2>/dev/null || true
if [ "$rc" -eq 0 ] && [ -n "${out:-}" ] && [ "$out" != "null" ]; then
rm -f "$errfile" 2>/dev/null || true
return 0
fi
msg=$' - FAIL: #!'"$candidate"$'\n'" rc=$rc, stdout='${out:-}'"$'\n'
if [ -s "$errfile" ]; then
msg+=$' stderr:\n'
msg+="$(sed -n '1,8p' "$errfile")"$'\n'
else
msg+=$' stderr: <empty>\n'
fi
SHEBANG_ERRORS+=("$msg")
rm -f "$errfile" 2>/dev/null || true
return 1
}
shebang=""
for candidate in "${candidate_shebangs[@]}"; do
if validate_shebang "$candidate"; then
shebang="$candidate"
break
fi
if validate_shebang "$candidate"; then
shebang="$candidate"
break
fi
done
if [ -z "$shebang" ]; then
echo "ERROR: No valid shebang found (unable to execute bashio::addon.version via candidates)." >&2
echo "Tried:" >&2
printf ' - %s\n' "${candidate_shebangs[@]}" >&2
if [ "${#SHEBANG_ERRORS[@]}" -gt 0 ]; then
echo "Probe failures:" >&2
printf '%s\n' "${SHEBANG_ERRORS[@]}" >&2
fi
exit 1
echo "ERROR: No valid shebang found (unable to execute bashio::addon.version via candidates)." >&2
echo "Tried:" >&2
printf ' - %s\n' "${candidate_shebangs[@]}" >&2
if [ "${#SHEBANG_ERRORS[@]}" -gt 0 ]; then
echo "Probe failures:" >&2
printf '%s\n' "${SHEBANG_ERRORS[@]}" >&2
fi
exit 1
fi
#####################################
@@ -197,28 +203,28 @@ S6_CONTAINER_ENV="/run/s6/container_environment"
# Only when this script is PID 1 -- under /init it is the stage-2 hook and stage 1 has already
# written the directory -- and only where with-contenv exists to care.
if $PID1 && { [ -x /command/with-contenv ] || [ -x /usr/bin/with-contenv ]; }; then
# Filled in a sibling and renamed into place, never written to live. A half-populated envdir is
# worse than an absent one: s6-envdir accepts it, so a with-contenv script starts and runs
# against an environment quietly missing SUPERVISOR_TOKEN, where an absent one stops it at its
# shebang. rename(2) means a concurrent reader -- a HEALTHCHECK can run alongside PID 1 -- sees
# the directory either absent or complete, never mid-dump.
#
# Cleared rather than written over: /run is not a tmpfs here, so an image layer can persist
# entries, and writing name by name would merge into them and leave variables PID 1 does not
# have, a stale SUPERVISOR_TOKEN among them. A failed rm has to abort the chain, because mkdir -p
# accepts a surviving symlink-to-directory and would let the dump follow it. rm does not traverse
# a symlink, but it would empty anything bind-mounted at this exact path -- not a configuration
# any add-on uses, and not one s6 would tolerate either.
if rm -rf "$S6_CONTAINER_ENV" "$S6_CONTAINER_ENV.tmp" && mkdir -p "$S6_CONTAINER_ENV.tmp" &&
s6-dumpenv -- "$S6_CONTAINER_ENV.tmp" && mv "$S6_CONTAINER_ENV.tmp" "$S6_CONTAINER_ENV"; then
echo "Populated $S6_CONTAINER_ENV for with-contenv"
else
# Leaves the directory absent, which is exactly how this fails today -- so the failure mode is
# unchanged, not newly degraded. Never fatal, a read-only /run must still let the add-on boot,
# but never silent either, since the shebang failure it leaves behind says nothing on its own.
rm -rf "$S6_CONTAINER_ENV" "$S6_CONTAINER_ENV.tmp" 2>/dev/null || true
echo -e "\e[38;5;214m$(date) WARNING: could not populate $S6_CONTAINER_ENV; scripts with a with-contenv shebang will fail at their shebang, as they did before this was attempted\e[0m"
fi
# Filled in a sibling and renamed into place, never written to live. A half-populated envdir is
# worse than an absent one: s6-envdir accepts it, so a with-contenv script starts and runs
# against an environment quietly missing SUPERVISOR_TOKEN, where an absent one stops it at its
# shebang. rename(2) means a concurrent reader -- a HEALTHCHECK can run alongside PID 1 -- sees
# the directory either absent or complete, never mid-dump.
#
# Cleared rather than written over: /run is not a tmpfs here, so an image layer can persist
# entries, and writing name by name would merge into them and leave variables PID 1 does not
# have, a stale SUPERVISOR_TOKEN among them. A failed rm has to abort the chain, because mkdir -p
# accepts a surviving symlink-to-directory and would let the dump follow it. rm does not traverse
# a symlink, but it would empty anything bind-mounted at this exact path -- not a configuration
# any add-on uses, and not one s6 would tolerate either.
if rm -rf "$S6_CONTAINER_ENV" "$S6_CONTAINER_ENV.tmp" && mkdir -p "$S6_CONTAINER_ENV.tmp" \
&& s6-dumpenv -- "$S6_CONTAINER_ENV.tmp" && mv "$S6_CONTAINER_ENV.tmp" "$S6_CONTAINER_ENV"; then
echo "Populated $S6_CONTAINER_ENV for with-contenv"
else
# Leaves the directory absent, which is exactly how this fails today -- so the failure mode is
# unchanged, not newly degraded. Never fatal, a read-only /run must still let the add-on boot,
# but never silent either, since the shebang failure it leaves behind says nothing on its own.
rm -rf "$S6_CONTAINER_ENV" "$S6_CONTAINER_ENV.tmp" 2> /dev/null || true
echo -e "\e[38;5;214m$(date) WARNING: could not populate $S6_CONTAINER_ENV; scripts with a with-contenv shebang will fail at their shebang, as they did before this was attempted\e[0m"
fi
fi
####################################
@@ -228,21 +234,21 @@ fi
BASHIO_LIB=""
BASHIO_LIB_FULL=false
for f in /usr/lib/bashio/bashio.sh /usr/lib/bashio/lib.sh /usr/src/bashio/bashio.sh /usr/local/lib/bashio/bashio.sh; do
if [ -f "$f" ]; then
BASHIO_LIB="$f"
# The real library, which talks to the Supervisor. The standalone shim below only reads
# environment variables, which matters to wait_for_supervisor().
BASHIO_LIB_FULL=true
break
fi
if [ -f "$f" ]; then
BASHIO_LIB="$f"
# The real library, which talks to the Supervisor. The standalone shim below only reads
# environment variables, which matters to wait_for_supervisor().
BASHIO_LIB_FULL=true
break
fi
done
if [ -z "$BASHIO_LIB" ]; then
for f in /usr/local/lib/bashio-standalone.sh /.bashio-standalone.sh; do
if [ -f "$f" ]; then
BASHIO_LIB="$f"
break
fi
done
for f in /usr/local/lib/bashio-standalone.sh /.bashio-standalone.sh; do
if [ -f "$f" ]; then
BASHIO_LIB="$f"
break
fi
done
fi
##############################
@@ -268,56 +274,56 @@ fi
# skips the wait. When the Supervisor is already up -- the normal case -- this costs one request.
wait_for_supervisor() {
local max="${HA_SUPERVISOR_WAIT:-30}"
local started deadline remaining attempt announced=0
local max="${HA_SUPERVISOR_WAIT:-30}"
local started deadline remaining attempt announced=0
# Nothing to wait for without a token. The standalone shim is excluded too: it answers these
# calls from environment variables and never contacts the Supervisor, so it can never satisfy
# the probe and would burn the whole ceiling on every boot.
[ -n "${SUPERVISOR_TOKEN:-}" ] || return 0
[ "${BASHIO_LIB_FULL:-false}" = "true" ] || return 0
# bashio's own curl carries no --max-time, so each attempt is bounded from the outside.
command -v timeout >/dev/null 2>&1 || return 0
# Digits only, then forced to base 10: `test -gt` accepts a zero-padded override like 08, but
# arithmetic expansion reads it as octal and fails, which would leave the deadline empty and
# spin the loop below forever.
case "$max" in '' | *[!0-9]*) return 0 ;; esac
max=$((10#$max))
[ "$max" -gt 0 ] || return 0
# Nothing to wait for without a token. The standalone shim is excluded too: it answers these
# calls from environment variables and never contacts the Supervisor, so it can never satisfy
# the probe and would burn the whole ceiling on every boot.
[ -n "${SUPERVISOR_TOKEN:-}" ] || return 0
[ "${BASHIO_LIB_FULL:-false}" = "true" ] || return 0
# bashio's own curl carries no --max-time, so each attempt is bounded from the outside.
command -v timeout > /dev/null 2>&1 || return 0
# Digits only, then forced to base 10: `test -gt` accepts a zero-padded override like 08, but
# arithmetic expansion reads it as octal and fails, which would leave the deadline empty and
# spin the loop below forever.
case "$max" in '' | *[!0-9]*) return 0 ;; esac
max=$((10#$max))
[ "$max" -gt 0 ] || return 0
started=$SECONDS
deadline=$((started + max))
started=$SECONDS
deadline=$((started + max))
while :; do
remaining=$((deadline - SECONDS))
if [ "$remaining" -le 0 ]; then
echo -e "\e[38;5;214m$(date) WARNING: Supervisor API did not report this add-on's network details within ${max}s, continuing anyway\e[0m"
return 0
fi
while :; do
remaining=$((deadline - SECONDS))
if [ "$remaining" -le 0 ]; then
echo -e "\e[38;5;214m$(date) WARNING: Supervisor API did not report this add-on's network details within ${max}s, continuing anyway\e[0m"
return 0
fi
# No single attempt may outlive the ceiling it is bounded by.
attempt=5
[ "$remaining" -lt "$attempt" ] && attempt="$remaining"
# No single attempt may outlive the ceiling it is bounded by.
attempt=5
[ "$remaining" -lt "$attempt" ] && attempt="$remaining"
# One call is enough to settle all of them: bashio fetches the whole /addons/self/info object
# and caches it, so a populated ip_address means ingress_port and the rest are cached too.
# Run in a child shell so bashio's globals and traps stay out of the entrypoint; its own error
# logging is dropped because a failed attempt here is expected, not news.
# shellcheck disable=SC2016
if timeout "$attempt" bash -c '. "$1" && [ -n "$(bashio::addon.ip_address)" ]' \
_ "$BASHIO_LIB" >/dev/null 2>&1; then
[ "$announced" -eq 0 ] || echo "Supervisor API ready after $((SECONDS - started))s"
return 0
fi
# One call is enough to settle all of them: bashio fetches the whole /addons/self/info object
# and caches it, so a populated ip_address means ingress_port and the rest are cached too.
# Run in a child shell so bashio's globals and traps stay out of the entrypoint; its own error
# logging is dropped because a failed attempt here is expected, not news.
# shellcheck disable=SC2016
if timeout "$attempt" bash -c '. "$1" && [ -n "$(bashio::addon.ip_address)" ]' \
_ "$BASHIO_LIB" > /dev/null 2>&1; then
[ "$announced" -eq 0 ] || echo "Supervisor API ready after $((SECONDS - started))s"
return 0
fi
if [ "$announced" -eq 0 ]; then
echo "Waiting for the Supervisor API to report this add-on's network details..."
announced=1
fi
if [ "$announced" -eq 0 ]; then
echo "Waiting for the Supervisor API to report this add-on's network details..."
announced=1
fi
# Skipped when the attempt already consumed what was left, so the sleep cannot overshoot.
[ "$((deadline - SECONDS))" -gt 0 ] && sleep 1
done
# Skipped when the attempt already consumed what was left, so the sleep cannot overshoot.
[ "$((deadline - SECONDS))" -gt 0 ] && sleep 1
done
}
wait_for_supervisor
@@ -327,97 +333,97 @@ wait_for_supervisor
####################
run_one_script() {
local script="$1"
local script="$1"
echo "$script: executing"
echo "$script: executing"
if [ "$(id -u)" -eq 0 ]; then
chown "$(id -u)":"$(id -g)" "$script" || true
chmod a+x "$script" || true
else
echo -e "\e[38;5;214m$(date) WARNING: Script executed with user $(id -u):$(id -g), things can break and chown won't work\e[0m"
sed -i "s/^[[:space:]]*chown /true # chown /g" "$script"
sed -i "s/^[[:space:]]*chmod /true # chmod /g" "$script"
fi
sed -i "1s|^.*|#!$shebang|" "$script"
chmod +x "$script"
if [ "${ha_entry_source:-null}" = "true" ]; then
sed -i -E 's/^[[:space:]]*exit ([0-9]+)/return \1 \|\| exit \1/g' "$script"
sed -i 's/bashio::exit\.nok/return 1/g' "$script"
sed -i 's/bashio::exit\.ok/return 0/g' "$script"
# shellcheck disable=SC1090
source "$script" || echo -e "\033[0;31mError\033[0m : $script exiting $?"
else
_run_rc=0
"$script" || _run_rc=$?
if [ "$_run_rc" -eq 126 ] && [ -n "${BASHIO_LIB:-}" ]; then
echo "Direct exec failed (rc=126, likely E2BIG), retrying via source in subshell..."
_run_rc=0
(
# shellcheck disable=SC1090
. "$BASHIO_LIB" 2>/dev/null || true
# shellcheck disable=SC1090
. "$script"
) || _run_rc=$?
if [ "$_run_rc" -ne 0 ]; then
echo -e "\033[0;31mError\033[0m : $script exiting $_run_rc"
fi
elif [ "$_run_rc" -ne 0 ]; then
echo -e "\033[0;31mError\033[0m : $script exiting $_run_rc"
if [ "$(id -u)" -eq 0 ]; then
chown "$(id -u)":"$(id -g)" "$script" || true
chmod a+x "$script" || true
else
echo -e "\e[38;5;214m$(date) WARNING: Script executed with user $(id -u):$(id -g), things can break and chown won't work\e[0m"
sed -i "s/^[[:space:]]*chown /true # chown /g" "$script"
sed -i "s/^[[:space:]]*chmod /true # chmod /g" "$script"
fi
fi
sed -i '1a exit 0' "$script"
sed -i "1s|^.*|#!$shebang|" "$script"
chmod +x "$script"
if [ "${ha_entry_source:-null}" = "true" ]; then
sed -i -E 's/^[[:space:]]*exit ([0-9]+)/return \1 \|\| exit \1/g' "$script"
sed -i 's/bashio::exit\.nok/return 1/g' "$script"
sed -i 's/bashio::exit\.ok/return 0/g' "$script"
# shellcheck disable=SC1090
source "$script" || echo -e "\033[0;31mError\033[0m : $script exiting $?"
else
_run_rc=0
"$script" || _run_rc=$?
if [ "$_run_rc" -eq 126 ] && [ -n "${BASHIO_LIB:-}" ]; then
echo "Direct exec failed (rc=126, likely E2BIG), retrying via source in subshell..."
_run_rc=0
(
# shellcheck disable=SC1090
. "$BASHIO_LIB" 2> /dev/null || true
# shellcheck disable=SC1090
. "$script"
) || _run_rc=$?
if [ "$_run_rc" -ne 0 ]; then
echo -e "\033[0;31mError\033[0m : $script exiting $_run_rc"
fi
elif [ "$_run_rc" -ne 0 ]; then
echo -e "\033[0;31mError\033[0m : $script exiting $_run_rc"
fi
fi
sed -i '1a exit 0' "$script"
}
if [ -d /etc/cont-init.d ]; then
for SCRIPTS in /etc/cont-init.d/*; do
[ -e "$SCRIPTS" ] || continue
run_one_script "$SCRIPTS"
done
for SCRIPTS in /etc/cont-init.d/*; do
[ -e "$SCRIPTS" ] || continue
run_one_script "$SCRIPTS"
done
fi
if $PID1; then
shopt -s nullglob
for runfile in /etc/services.d/*/run /etc/s6-overlay/s6-rc.d/*/run; do
[ -f "$runfile" ] || continue
echo "Starting: $runfile"
sed -i "1s|^.*|#!$shebang|" "$runfile"
chmod +x "$runfile"
(
restart_count=0
max_restarts=5
while true; do
_svc_rc=0
"$runfile" || _svc_rc=$?
if [ "$_svc_rc" -eq 126 ] && [ -n "${BASHIO_LIB:-}" ]; then
echo "Direct exec of $runfile failed (rc=126, likely E2BIG), retrying via source..."
_svc_rc=0
(
# shellcheck disable=SC1090
. "$BASHIO_LIB" 2>/dev/null || true
# shellcheck disable=SC1090
. "$runfile"
) || _svc_rc=$?
fi
rc=$_svc_rc
if [ "$rc" -eq 0 ]; then
echo "$runfile exited cleanly (exit 0), not restarting."
break
fi
restart_count=$((restart_count + 1))
if [ "$restart_count" -ge "$max_restarts" ]; then
echo -e "\033[0;31mERROR: $runfile has crashed $restart_count times (last exit code: $rc), giving up.\033[0m"
break
fi
echo -e "\e[38;5;214m$(date) WARNING: $runfile exited (code $rc), restarting (#${restart_count}/${max_restarts}) in 5s...\e[0m"
sleep 5
done
) &
done
shopt -u nullglob
shopt -s nullglob
for runfile in /etc/services.d/*/run /etc/s6-overlay/s6-rc.d/*/run; do
[ -f "$runfile" ] || continue
echo "Starting: $runfile"
sed -i "1s|^.*|#!$shebang|" "$runfile"
chmod +x "$runfile"
(
restart_count=0
max_restarts=5
while true; do
_svc_rc=0
"$runfile" || _svc_rc=$?
if [ "$_svc_rc" -eq 126 ] && [ -n "${BASHIO_LIB:-}" ]; then
echo "Direct exec of $runfile failed (rc=126, likely E2BIG), retrying via source..."
_svc_rc=0
(
# shellcheck disable=SC1090
. "$BASHIO_LIB" 2> /dev/null || true
# shellcheck disable=SC1090
. "$runfile"
) || _svc_rc=$?
fi
rc=$_svc_rc
if [ "$rc" -eq 0 ]; then
echo "$runfile exited cleanly (exit 0), not restarting."
break
fi
restart_count=$((restart_count + 1))
if [ "$restart_count" -ge "$max_restarts" ]; then
echo -e "\033[0;31mERROR: $runfile has crashed $restart_count times (last exit code: $rc), giving up.\033[0m"
break
fi
echo -e "\e[38;5;214m$(date) WARNING: $runfile exited (code $rc), restarting (#${restart_count}/${max_restarts}) in 5s...\e[0m"
sleep 5
done
) &
done
shopt -u nullglob
fi
######################
@@ -425,43 +431,43 @@ fi
######################
if $PID1; then
echo " "
echo -e "\033[0;32mEverything started!\033[0m"
echo " "
echo -e "\033[0;32mEverything started!\033[0m"
terminate() {
local local_pid
echo "Termination signal received, forwarding to subprocesses..."
if command -v pgrep >/dev/null 2>&1; then
while read -r pid; do
[ -n "$pid" ] || continue
echo "Terminating child PID $pid"
kill -TERM "$pid" 2>/dev/null || echo "Failed to terminate PID $pid"
done < <(pgrep -P "$$" || true)
else
for p in /proc/[0-9]*/; do
local_pid="${p#/proc/}"
local_pid="${local_pid%/}"
if [ "$local_pid" -ne 1 ] && grep -q "^PPid:[[:space:]]*$$" "/proc/$local_pid/status" 2>/dev/null; then
echo "Terminating child PID $local_pid"
kill -TERM "$local_pid" 2>/dev/null || echo "Failed to terminate PID $local_pid"
terminate() {
local local_pid
echo "Termination signal received, forwarding to subprocesses..."
if command -v pgrep > /dev/null 2>&1; then
while read -r pid; do
[ -n "$pid" ] || continue
echo "Terminating child PID $pid"
kill -TERM "$pid" 2> /dev/null || echo "Failed to terminate PID $pid"
done < <(pgrep -P "$$" || true)
else
for p in /proc/[0-9]*/; do
local_pid="${p#/proc/}"
local_pid="${local_pid%/}"
if [ "$local_pid" -ne 1 ] && grep -q "^PPid:[[:space:]]*$$" "/proc/$local_pid/status" 2> /dev/null; then
echo "Terminating child PID $local_pid"
kill -TERM "$local_pid" 2> /dev/null || echo "Failed to terminate PID $local_pid"
fi
done
fi
done
fi
wait || true
echo "All subprocesses terminated. Exiting."
exit 0
}
wait || true
echo "All subprocesses terminated. Exiting."
exit 0
}
trap terminate SIGTERM SIGINT
while :; do
sleep infinity &
wait $!
done
trap terminate SIGTERM SIGINT
while :; do
sleep infinity &
wait $!
done
else
echo " "
echo -e "\033[0;32mStarting the upstream container\033[0m"
echo " "
if [ -f /docker-mods ]; then
exec /docker-mods
fi
echo " "
echo -e "\033[0;32mStarting the upstream container\033[0m"
echo " "
if [ -f /docker-mods ]; then
exec /docker-mods
fi
fi

View File

@@ -147,7 +147,7 @@ manual actions with precedence. Prompts live in `.github/prompts/`, shared shell
in `.github/scripts/`.
| Workflow | Model | Trigger | Role |
|---|---|---|---|
| --- | --- | --- | --- |
| `on_issues_ai_triage.yaml` | Sonnet-low | issue opened (+ author reply, daily catch-up) | Tier 1: classify, dedupe, answer, ask for info; label `ai-triage` for real add-on bugs |
| `daily_ai_fix.yaml` | Opus 5-xhigh | daily 03:00 | Tier 2: diagnose the `ai-triage` batch; small+confident → ready PR (`ai:fixed`); else write a plan (`ai:plan-pending`) |
| `on_issue_approved.yaml` | Opus 5-high | maintainer adds `ai:approved` | Tier 3: execute the approved plan → ready PR |
@@ -176,7 +176,7 @@ prompt-only, not machine-enforced.
## Linting Rules
| Tool | Config | Key ignores |
|------|--------|------------|
| ------ | -------- | ------------ |
| Hadolint | `.hadolint.yaml` | DL3002, DL3006-9, DL3018 (no pinning required) |
| ShellCheck | `.shellcheckrc` | SC2002 |
| Markdownlint | `.markdownlint.yaml` | MD013 (line length), MD025, MD033, MD041 |

View File

@@ -75,7 +75,7 @@ You can add the following tags in the file :
### Addon version numbering
The `version` written in the addon `config.yaml` is the one Home Assistant compares to decide whether an update is available. Home Assistant hides the update when it can order both versions and the new one is not strictly newer (`1.2.3` -> `1.2.3-2` is a semver pre-release, so it is *older*), and it cannot order tags such as `version-bf9e0b4f` or `ubuntu-2026-06-01` at all.
The `version` written in the addon `config.yaml` is the one Home Assistant compares to decide whether an update is available. Home Assistant hides the update when it can order both versions and the new one is not strictly newer (`1.2.3` -> `1.2.3-2` is a semver pre-release, so it is _older_), and it cannot order tags such as `version-bf9e0b4f` or `ubuntu-2026-06-01` at all.
The addon version is therefore derived from the upstream tag:

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -1,31 +1,31 @@
## 2.8.0 (2026-09-05)
- Update to latest version from lklynet/aurral (changelog : https://github.com/lklynet/aurral/releases)
## 2.7.0 (2026-08-29)
- Update to latest version from lklynet/aurral (changelog : https://github.com/lklynet/aurral/releases)
## 2.5.1 (2026-08-22)
- Update to latest version from lklynet/aurral (changelog : https://github.com/lklynet/aurral/releases)
## 2.4.0 (2026-08-13)
- Update to latest version from lklynet/aurral (changelog : https://github.com/lklynet/aurral/releases)
## 2.1.0 (2026-08-08)
- Update to latest version from lklynet/aurral (changelog : https://github.com/lklynet/aurral/releases)
## 2.0.3 (2026-08-01)
- Update to latest version from lklynet/aurral (changelog : https://github.com/lklynet/aurral/releases)
## 2.0.2 (2026-08-01)
- Update to latest version from lklynet/aurral (changelog : https://github.com/lklynet/aurral/releases)
## 2.0.0 (2026-07-22)
- Update to latest version from lklynet/aurral (changelog : https://github.com/lklynet/aurral/releases)
## 1.76.52 (2026-07-11)
- Update to latest version from lklynet/aurral (changelog : https://github.com/lklynet/aurral/releases)
## 1.76.51 (2026-06-17)
- Update to latest version from lklynet/aurral (changelog : https://github.com/lklynet/aurral/releases)
## 1.76.49 (2026-06-05)

View File

@@ -6,7 +6,7 @@ This addon is based on the docker image <https://github.com/lklynet/aurral>
## Configuration
| Option | Default | Description |
|---|---|---|
| --- | --- | --- |
| `download_folder` | `/share/aurral/downloads` | Path where Aurral writes flow downloads. Must be under `/share`. |
| `weekly_flow_folder` | `weekly-flow` | Subfolder name appended to `download_folder` for weekly flow files. The full path will be `download_folder/weekly_flow_folder`. |

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -1,16 +1,16 @@
## 1.85.0 (2026-08-29)
- Update to latest version from autobrr/autobrr (changelog : https://github.com/autobrr/autobrr/releases)
## 1.84.0 (2026-08-15)
- Update to latest version from autobrr/autobrr (changelog : https://github.com/autobrr/autobrr/releases)
## 1.83.0 (2026-08-01)
- Update to latest version from autobrr/autobrr (changelog : https://github.com/autobrr/autobrr/releases)
## 1.82.1 (2026-07-16)
- Update to latest version from autobrr/autobrr (changelog : https://github.com/autobrr/autobrr/releases)
## 1.81.0 (2026-07-04)
- Update to latest version from autobrr/autobrr (changelog : https://github.com/autobrr/autobrr/releases)
## 1.80.0 (2026-06-05)

View File

@@ -67,7 +67,7 @@ Default credentials: `admin` / `password` (change after first login).
### Options
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| -------- | ------ | --------- | ------------- |
| `PGID` | int | `0` | Group ID for file permissions |
| `PUID` | int | `0` | User ID for file permissions |
| `TZ` | str | | Timezone (e.g., `Europe/London`) |

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -44,7 +44,7 @@ Configurations can be done through the app webUI, except for the following optio
### Options
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| -------- | ------ | --------- | ------------- |
| `PGID` | int | `0` | Group ID for file permissions |
| `PUID` | int | `0` | User ID for file permissions |
| `TZ` | str | | Timezone (e.g., `Europe/London`) |

0
bazarr/rootfs/etc/services.d/nginx/run Normal file → Executable file
View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -17,7 +17,7 @@ A privacy-first PDF toolkit running entirely in your browser — no uploads, no
### Organize & Edit
| Tool | Tool | Tool |
|------|------|------|
| ------ | ------ | ------ |
| Merge PDF | Split PDF | Organize PDF |
| Delete Pages | Extract Pages | Reverse Pages |
| Rotate PDF | Rotate Custom | Crop PDF |
@@ -28,7 +28,7 @@ A privacy-first PDF toolkit running entirely in your browser — no uploads, no
### Convert TO PDF
| Tool | Tool | Tool |
|------|------|------|
| ------ | ------ | ------ |
| Word to PDF | Excel to PDF | PowerPoint to PDF |
| Image to PDF | JPG to PDF | PNG to PDF |
| BMP to PDF | TIFF to PDF | WEBP to PDF |
@@ -44,7 +44,7 @@ A privacy-first PDF toolkit running entirely in your browser — no uploads, no
### Convert FROM PDF
| Tool | Tool | Tool |
|------|------|------|
| ------ | ------ | ------ |
| PDF to DOCX | PDF to Excel | PDF to JPG |
| PDF to PNG | PDF to BMP | PDF to TIFF |
| PDF to WEBP | PDF to SVG | PDF to Text |
@@ -54,7 +54,7 @@ A privacy-first PDF toolkit running entirely in your browser — no uploads, no
### Security & Metadata
| Tool | Tool | Tool |
|------|------|------|
| ------ | ------ | ------ |
| Encrypt PDF | Decrypt PDF | Change Permissions |
| Remove Restrictions | Sign PDF | Digital Sign PDF |
| Validate Signature | Edit Metadata | View Metadata |
@@ -64,7 +64,7 @@ A privacy-first PDF toolkit running entirely in your browser — no uploads, no
### Enhance & Process
| Tool | Tool | Tool |
|------|------|------|
| ------ | ------ | ------ |
| Compress PDF | OCR PDF | Deskew PDF |
| Rasterize PDF | Linearize PDF | PDF to PDF/A |
| Adjust Colors | Invert Colors | Text Color |
@@ -76,7 +76,7 @@ A privacy-first PDF toolkit running entirely in your browser — no uploads, no
### Forms & More
| Tool | Tool | Tool |
|------|------|------|
| ------ | ------ | ------ |
| Form Creator | Form Filler | Table of Contents |
| Bookmark | PDF Editor | Extract Images |
| Extract Tables | Extract Attachments | Edit Attachments |

0
bentopdf/rootfs/etc/s6-overlay/s6-rc.d/bentopdf/run Normal file → Executable file
View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@@ -43,8 +43,8 @@ if [ ! -f "$CONFIG_LOCATION" ]; then
# config. (We can't remove the file and continue — subsequent yq calls
# under set -e would abort the init script.)
if ! curl -fL -s -S \
https://raw.githubusercontent.com/tphakala/birdnet-go/refs/heads/main/internal/conf/config.yaml \
-o "$CONFIG_LOCATION"; then
https://raw.githubusercontent.com/tphakala/birdnet-go/refs/heads/main/internal/conf/config.yaml \
-o "$CONFIG_LOCATION"; then
bashio::log.warning "Could not download default config.yaml; seeding an empty document so addon defaults can populate it"
echo '{}' > "$CONFIG_LOCATION"
fi
@@ -190,5 +190,5 @@ LOG_DIR="/config/logs"
if [ -d "$LOG_DIR" ]; then
bashio::log.info "Trimming log files older than ${LOG_MAX_AGE_DAYS} days in ${LOG_DIR}"
ln -sf "$LOG_DIR" /logs
find "$LOG_DIR" -type f -name "*.log*" -mtime +"$LOG_MAX_AGE_DAYS" -delete 2>/dev/null || true
find "$LOG_DIR" -type f -name "*.log*" -mtime +"$LOG_MAX_AGE_DAYS" -delete 2> /dev/null || true
fi

View File

@@ -44,8 +44,8 @@ if ! bashio::config.true 'mariadb_auto_config'; then
# Only revert if config.yaml points at the HA MariaDB host we would have
# written — a mysql block pointing at a different host was set manually.
# shellcheck disable=SC2016
CURRENT_MYSQL_HOST="$(yq -r '.output.mysql.host // empty' "$CONFIG_LOCATION" 2>/dev/null || true)"
if yq -e '.output.mysql.enabled == true' "$CONFIG_LOCATION" >/dev/null 2>&1 \
CURRENT_MYSQL_HOST="$(yq -r '.output.mysql.host // empty' "$CONFIG_LOCATION" 2> /dev/null || true)"
if yq -e '.output.mysql.enabled == true' "$CONFIG_LOCATION" > /dev/null 2>&1 \
&& [ "${CURRENT_MYSQL_HOST}" = "${MYSQL_HOST}" ]; then
yq -i -y \
'.output.mysql.enabled = false
@@ -71,7 +71,7 @@ bashio::log.green "---"
# Resolve MariaDB hostname to IPv4: on HAOS >=17.3 the Supervisor network
# gained IPv6, but the MariaDB addon only grants its user from the IPv4
# subnet. Fall back to the raw hostname if resolution fails.
MYSQL_HOST_RESOLVED="$(getent ahostsv4 "${MYSQL_HOST}" 2>/dev/null | awk '{print $1; exit}')"
MYSQL_HOST_RESOLVED="$(getent ahostsv4 "${MYSQL_HOST}" 2> /dev/null | awk '{print $1; exit}')"
MYSQL_HOST_RESOLVED="${MYSQL_HOST_RESOLVED:-${MYSQL_HOST}}"
if [ "${MYSQL_HOST_RESOLVED}" != "${MYSQL_HOST}" ]; then
bashio::log.blue "Resolved ${MYSQL_HOST} -> ${MYSQL_HOST_RESOLVED} (forcing IPv4)"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -1,4 +1,4 @@
## 20260827 (2026-08-29)
- Update to latest version from tphakala/birdnet-go (changelog : https://github.com/tphakala/birdnet-go/releases)
- Upstream tag : 20260823
@@ -6,13 +6,13 @@
- Minor bugs fixed
## 20260716.1 (26-08-2026)
- Minor bugs fixed
## 20260716.1 (2026-08-26)
- Rebuild after syncing the alexbelgium/birdnet-go fork with upstream tphakala/birdnet-go and resolving conflicts on all open pull requests
## 20260716 (2026-07-16)
- Update to latest version from tphakala/birdnet-go (changelog : https://github.com/tphakala/birdnet-go/releases)
## 20260712 (2026-07-13)
- Update to latest version from tphakala/birdnet-go (changelog : https://github.com/tphakala/birdnet-go/releases)
## nightly-20260615-4 (09-07-2026)
@@ -22,8 +22,8 @@
## nightly-20260615-2 (05-07-2026)
- Minor bugs fixed
- Fix detections/database not persisting across restarts on a fresh install: upstream's default `config.yaml` ships `output.sqlite.path: birdnet.db` (relative) explicitly, so the missing-only (`//=`) seeding introduced previously never rewrote it to an absolute path. A relative path resolves against the app's ephemeral working directory, so the database was silently recreated empty on every restart. Any relative `output.sqlite.path` is now rewritten to live under the persistent `/config` on startup; values already set to an absolute path are left untouched. (https://github.com/tphakala/birdnet-go/discussions/3774)
- MQTT auto-config now also enables BirdNET-Go's native Home Assistant MQTT auto-discovery: detection sensors appear in Home Assistant automatically with no manual YAML (existing UI/config.yaml edits are preserved)
- Fix detections/database not persisting across restarts on a fresh install: upstream's default `config.yaml` ships `output.sqlite.path: birdnet.db` (relative) explicitly, so the missing-only (`//=`) seeding introduced previously never rewrote it to an absolute path. A relative path resolves against the app's ephemeral working directory, so the database was silently recreated empty on every restart. Any relative `output.sqlite.path` is now rewritten to live under the persistent `/config` on startup; values already set to an absolute path are left untouched. (https://github.com/tphakala/birdnet-go/discussions/3774)
- MQTT auto-config now also enables BirdNET-Go's native Home Assistant MQTT auto-discovery: detection sensors appear in Home Assistant automatically with no manual YAML (existing UI/config.yaml edits are preserved)
- MQTT auto-config seeds `realtime.mqtt.retain: true` (only when unset) so sensor states survive Home Assistant restarts
- Added supervisor watchdog (tcp://[HOST]:[PORT:8080]) so the add-on is automatically restarted if BirdNET-Go stops responding
- Added backup_exclude for rotated logs, making Home Assistant backups smaller (SQLite journals are kept so hot backups stay consistent)

View File

@@ -43,8 +43,8 @@ if [ ! -f "$CONFIG_LOCATION" ]; then
# config. (We can't remove the file and continue — subsequent yq calls
# under set -e would abort the init script.)
if ! curl -fL -s -S \
https://raw.githubusercontent.com/tphakala/birdnet-go/refs/heads/main/internal/conf/config.yaml \
-o "$CONFIG_LOCATION"; then
https://raw.githubusercontent.com/tphakala/birdnet-go/refs/heads/main/internal/conf/config.yaml \
-o "$CONFIG_LOCATION"; then
bashio::log.warning "Could not download default config.yaml; seeding an empty document so addon defaults can populate it"
echo '{}' > "$CONFIG_LOCATION"
fi
@@ -186,5 +186,5 @@ LOG_DIR="/config/logs"
if [ -d "$LOG_DIR" ]; then
bashio::log.info "Trimming log files older than ${LOG_MAX_AGE_DAYS} days in ${LOG_DIR}"
ln -sf "$LOG_DIR" /logs
find "$LOG_DIR" -type f -name "*.log*" -mtime +"$LOG_MAX_AGE_DAYS" -delete 2>/dev/null || true
find "$LOG_DIR" -type f -name "*.log*" -mtime +"$LOG_MAX_AGE_DAYS" -delete 2> /dev/null || true
fi

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -9,9 +9,9 @@ Here are some example tests I did (the whole threads are really interesting also
**My recommendation:**
- **Best entry system (< 50€):** Boya By-lm40 (30€) + deadcat (10€)
- **Best middle end system (< 150€):** Clippy EM272 TRS/TRRS (55€) + Rode AI micro trs/trrs to usb (70€) + Rycote deadcat (27€)
- **Best high end system (<400€):** Clippy EM272 XLR (85€) or LOM Ucho Pro (75€) + Focusrite Scarlet 2i2 4th Gen (200€) + Bubblebee Pro Extreme deadcat (45€)
- **Best entry system (< 50 €):** Boya By-lm40 (30 €) + deadcat (10 €)
- **Best middle end system (< 150 €):** Clippy EM272 TRS/TRRS (55 €) + Rode AI micro trs/trrs to usb (70 €) + Rycote deadcat (27 €)
- **Best high end system (<400 €):** Clippy EM272 XLR (85 €) or LOM Ucho Pro (75 €) + Focusrite Scarlet 2i2 4th Gen (200 €) + Bubblebee Pro Extreme deadcat (45 €)
**Sources for high end microphones in Europe:**

View File

@@ -112,6 +112,7 @@ def automatic_mqtt_publish(file, detection, path):
mqttc.publish(mqtt_topic, json_bird, 1)
log.info("Posted to MQTT: ok")
# Create MQTT client using legacy callback API when available for
# compatibility with paho-mqtt >= 2.0
callback_api = getattr(mqtt, "CallbackAPIVersion", None)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -9,9 +9,9 @@ Here are some example tests I did (the whole threads are really interesting also
**My recommendation:**
- **Best entry system (< 50€):** Boya By-lm40 (30€) + deadcat (10€)
- **Best middle end system (< 150€):** Clippy EM272 TRS/TRRS (55€) + Rode AI micro trs/trrs to usb (70€) + Rycote deadcat (27€)
- **Best high end system (<400€):** Clippy EM272 XLR (85€) or LOM Ucho Pro (75€) + Focusrite Scarlet 2i2 4th Gen (200€) + Bubblebee Pro Extreme deadcat (45€)
- **Best entry system (< 50 €):** Boya By-lm40 (30 €) + deadcat (10 €)
- **Best middle end system (< 150 €):** Clippy EM272 TRS/TRRS (55 €) + Rode AI micro trs/trrs to usb (70 €) + Rycote deadcat (27 €)
- **Best high end system (<400 €):** Clippy EM272 XLR (85 €) or LOM Ucho Pro (75 €) + Focusrite Scarlet 2i2 4th Gen (200 €) + Bubblebee Pro Extreme deadcat (45 €)
**Sources for high end microphones in Europe:**

View File

@@ -112,6 +112,7 @@ def automatic_mqtt_publish(file, detection, path):
mqttc.publish(mqtt_topic, json_bird, 1)
log.info("Posted to MQTT: ok")
# Create MQTT client using legacy callback API when available for
# compatibility with paho-mqtt >= 2.0
callback_api = getattr(mqtt, "CallbackAPIVersion", None)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.7 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@@ -1,19 +1,19 @@
## 0.8.8 (2026-08-22)
- Update to latest version from Suncuss/BirdNET-PiPy (changelog : https://github.com/Suncuss/BirdNET-PiPy/releases)
## 0.8.6 (2026-08-13)
- Update to latest version from Suncuss/BirdNET-PiPy (changelog : https://github.com/Suncuss/BirdNET-PiPy/releases)
## 0.8.5 (2026-08-01)
- Update to latest version from Suncuss/BirdNET-PiPy (changelog : https://github.com/Suncuss/BirdNET-PiPy/releases)
## 0.8.4 (2026-07-13)
- Update to latest version from Suncuss/BirdNET-PiPy (changelog : https://github.com/Suncuss/BirdNET-PiPy/releases)
## 0.8.3 (2026-07-11)
- Update to latest version from Suncuss/BirdNET-PiPy (changelog : https://github.com/Suncuss/BirdNET-PiPy/releases)
## 0.8.2.1 (2026-07-05)
- Re-tag of 0.8.2-1 with no content change. Home Assistant compares add-on versions with semver semantics, where a `-N` suffix counts as a *pre-release* and sorts **below** the base version — so users already on 0.8.2 saw the 0.8.2-1 nginx fix as "Up-to-date" with the Update button disabled. Four-segment `0.8.2.1` sorts above both `0.8.2` and `0.8.2-1` (and below the next upstream `0.8.3`), so the update becomes installable everywhere.
@@ -23,7 +23,7 @@
## 0.8.2 (2026-07-04)
- Update to latest version from Suncuss/BirdNET-PiPy (changelog : https://github.com/Suncuss/BirdNET-PiPy/releases)
## 0.7.5 (2026-06-23)
- Update to latest version from Suncuss/BirdNET-PiPy (changelog : https://github.com/Suncuss/BirdNET-PiPy/releases)
## 0.7.4 (2026-06-01)

View File

@@ -73,7 +73,7 @@ map:
name: BirdNET-PiPy
options:
env_vars: []
ICECAST_PASSWORD: ''
ICECAST_PASSWORD: ""
data_location: /config/data
panel_icon: mdi:bird
ports:

View File

@@ -7,11 +7,11 @@ DATA_LOCATION="$(bashio::config 'data_location' || true)"
DATA_LOCATION="${DATA_LOCATION:-$DEFAULT_LOCATION}"
case "${DATA_LOCATION}" in
/config/*|/share/*|/data/*) ;;
*)
bashio::log.warning "Invalid data_location '${DATA_LOCATION}', falling back to ${DEFAULT_LOCATION}"
DATA_LOCATION="${DEFAULT_LOCATION}"
;;
/config/* | /share/* | /data/*) ;;
*)
bashio::log.warning "Invalid data_location '${DATA_LOCATION}', falling back to ${DEFAULT_LOCATION}"
DATA_LOCATION="${DEFAULT_LOCATION}"
;;
esac
LEGACY1="/config/birdnet-pipy/data"
@@ -19,14 +19,14 @@ LEGACY2="/data"
mkdir -p "${DATA_LOCATION}"
if [ -z "$(ls -A "${DATA_LOCATION}" 2>/dev/null || true)" ]; then
if [ -d "${LEGACY1}" ] && [ -n "$(ls -A "${LEGACY1}" 2>/dev/null || true)" ]; then
bashio::log.notice "Migrating legacy data from ${LEGACY1} to ${DATA_LOCATION}"
cp -a "${LEGACY1}/." "${DATA_LOCATION}/" || true
elif [ -d "${LEGACY2}" ] && [ "${LEGACY2}" != "${DATA_LOCATION}" ] && [ -n "$(ls -A "${LEGACY2}" 2>/dev/null || true)" ]; then
bashio::log.notice "Migrating legacy data from ${LEGACY2} to ${DATA_LOCATION}"
cp -a "${LEGACY2}/." "${DATA_LOCATION}/" || true
fi
if [ -z "$(ls -A "${DATA_LOCATION}" 2> /dev/null || true)" ]; then
if [ -d "${LEGACY1}" ] && [ -n "$(ls -A "${LEGACY1}" 2> /dev/null || true)" ]; then
bashio::log.notice "Migrating legacy data from ${LEGACY1} to ${DATA_LOCATION}"
cp -a "${LEGACY1}/." "${DATA_LOCATION}/" || true
elif [ -d "${LEGACY2}" ] && [ "${LEGACY2}" != "${DATA_LOCATION}" ] && [ -n "$(ls -A "${LEGACY2}" 2> /dev/null || true)" ]; then
bashio::log.notice "Migrating legacy data from ${LEGACY2} to ${DATA_LOCATION}"
cp -a "${LEGACY2}/." "${DATA_LOCATION}/" || true
fi
fi
mkdir -p "${DATA_LOCATION}/config" "${DATA_LOCATION}/clips" "${DATA_LOCATION}/logs" "${DATA_LOCATION}/cache" || true

View File

@@ -7,10 +7,10 @@ CFG_DIR="${DATA_DIR}/config"
SETTINGS="${CFG_DIR}/user_settings.json"
if [ -f "$SETTINGS" ]; then
if ! grep -q 'detection' "$SETTINGS"; then
bak="${SETTINGS}.bak"
[ -e "$bak" ] && bak="${SETTINGS}.bak.$(date -u +%Y%m%dT%H%M%SZ)"
mv -f -- "$SETTINGS" "$bak"
echo "WARNING: Erroneous file detected: '$SETTINGS' did not contain 'detection' and was renamed to '$bak'." >&2
fi
if ! grep -q 'detection' "$SETTINGS"; then
bak="${SETTINGS}.bak"
[ -e "$bak" ] && bak="${SETTINGS}.bak.$(date -u +%Y%m%dT%H%M%SZ)"
mv -f -- "$SETTINGS" "$bak"
echo "WARNING: Erroneous file detected: '$SETTINGS' did not contain 'detection' and was renamed to '$bak'." >&2
fi
fi

View File

@@ -3,21 +3,21 @@
set -euo pipefail
SND_GID=""
if [ -e /dev/snd ] && command -v stat >/dev/null 2>&1; then
SND_GID="$(stat -c '%g' /dev/snd 2>/dev/null || true)"
if [ -e /dev/snd ] && command -v stat > /dev/null 2>&1; then
SND_GID="$(stat -c '%g' /dev/snd 2> /dev/null || true)"
fi
if [ -n "${SND_GID}" ] && getent group audio >/dev/null 2>&1; then
current_gid="$(getent group audio | cut -d: -f3 || true)"
if [ -n "${current_gid}" ] && [ "${current_gid}" != "${SND_GID}" ]; then
groupmod -g "${SND_GID}" audio 2>/dev/null || true
fi
if [ -n "${SND_GID}" ] && getent group audio > /dev/null 2>&1; then
current_gid="$(getent group audio | cut -d: -f3 || true)"
if [ -n "${current_gid}" ] && [ "${current_gid}" != "${SND_GID}" ]; then
groupmod -g "${SND_GID}" audio 2> /dev/null || true
fi
fi
for u in root nginx www-data icecast2; do
if id "${u}" >/dev/null 2>&1; then
usermod -aG audio "${u}" 2>/dev/null || true
fi
if id "${u}" > /dev/null 2>&1; then
usermod -aG audio "${u}" 2> /dev/null || true
fi
done
# Create /run/pulse/native symlink if the PulseAudio socket is elsewhere
@@ -25,21 +25,21 @@ done
PULSE_SOCK="${PULSE_SERVER:-}"
PULSE_SOCK="${PULSE_SOCK#unix:}"
if [ -n "${PULSE_SOCK}" ] && [ -S "${PULSE_SOCK}" ] && [ ! -S /run/pulse/native ]; then
mkdir -p /run/pulse
ln -sf "${PULSE_SOCK}" /run/pulse/native
mkdir -p /run/pulse
ln -sf "${PULSE_SOCK}" /run/pulse/native
fi
# Copy PulseAudio cookie for the icecast2 user so it can authenticate
if id icecast2 >/dev/null 2>&1; then
ICECAST_HOME="$(getent passwd icecast2 | cut -d: -f6)"
if [ -n "${ICECAST_HOME}" ]; then
for cookie in /config/.config/pulse/cookie /root/.config/pulse/cookie; do
if [ -f "${cookie}" ]; then
mkdir -p "${ICECAST_HOME}/.config/pulse"
cp "${cookie}" "${ICECAST_HOME}/.config/pulse/cookie"
chown icecast2 "${ICECAST_HOME}/.config/pulse" "${ICECAST_HOME}/.config/pulse/cookie"
break
fi
done
fi
if id icecast2 > /dev/null 2>&1; then
ICECAST_HOME="$(getent passwd icecast2 | cut -d: -f6)"
if [ -n "${ICECAST_HOME}" ]; then
for cookie in /config/.config/pulse/cookie /root/.config/pulse/cookie; do
if [ -f "${cookie}" ]; then
mkdir -p "${ICECAST_HOME}/.config/pulse"
cp "${cookie}" "${ICECAST_HOME}/.config/pulse/cookie"
chown icecast2 "${ICECAST_HOME}/.config/pulse" "${ICECAST_HOME}/.config/pulse/cookie"
break
fi
done
fi
fi

14
birdnet-pipy/rootfs/etc/services.d/api/run Normal file → Executable file
View File

@@ -10,10 +10,10 @@ cd /app
# and core.main reach the API locally, satisfying the wait_for in nginx/run +
# main/run). Heavy admin jobs cooperatively yield to keep this worker live.
exec gunicorn wsgi:application \
--worker-class geventwebsocket.gunicorn.workers.GeventWebSocketWorker \
--workers 1 \
--bind 127.0.0.1:5002 \
--timeout 120 \
--graceful-timeout 30 \
--keep-alive 65 \
--error-logfile -
--worker-class geventwebsocket.gunicorn.workers.GeventWebSocketWorker \
--workers 1 \
--bind 127.0.0.1:5002 \
--timeout 120 \
--graceful-timeout 30 \
--keep-alive 65 \
--error-logfile -

0
birdnet-pipy/rootfs/etc/services.d/icecast/run Normal file → Executable file
View File

0
birdnet-pipy/rootfs/etc/services.d/main/run Normal file → Executable file
View File

0
birdnet-pipy/rootfs/etc/services.d/model/run Normal file → Executable file
View File

0
birdnet-pipy/rootfs/etc/services.d/nginx/run Normal file → Executable file
View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -1,10 +1,10 @@
## 1.37.2 (2026-08-29)
- Update to latest version from dani-garcia/bitwarden_rs (changelog : https://github.com/dani-garcia/bitwarden_rs/releases)
## 1.37.1 (2026-08-01)
- Update to latest version from dani-garcia/bitwarden_rs (changelog : https://github.com/dani-garcia/bitwarden_rs/releases)
## 1.37.0 (2026-07-25)
- Update to latest version from dani-garcia/bitwarden_rs (changelog : https://github.com/dani-garcia/bitwarden_rs/releases)

0
bitwarden/rootfs/etc/s6-overlay/s6-rc.d/init-nginx/run Normal file → Executable file
View File

0
bitwarden/rootfs/etc/s6-overlay/s6-rc.d/nginx/run Normal file → Executable file
View File

View File

@@ -21,19 +21,19 @@ export ROCKET_SECRET_KEY="${secret_key}"
# Find the matching log level
if bashio::config.has_value 'log_level'; then
case "$(bashio::string.lower "$(bashio::config 'log_level')")" in
all|trace)
all | trace)
log_level="trace"
;;
debug)
log_level="debug"
;;
info|notice)
info | notice)
log_level="info"
;;
warning)
log_level="warn"
;;
error|fatal)
error | fatal)
log_level="error"
;;
off)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -64,7 +64,7 @@ Default credentials are shown in the startup logs.
### Options
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| -------- | ------ | --------- | ------------- |
| `PGID` | int | `0` | Group ID for file permissions |
| `PUID` | int | `0` | User ID for file permissions |
| `TZ` | str | | Timezone (e.g., `Europe/London`) |

View File

@@ -1,46 +1,46 @@
## 1.94.121-ls127 (2026-09-05)
- Update to latest version from linuxserver/docker-brave (changelog : https://github.com/linuxserver/docker-brave/releases)
## 1.94.117-ls125 (2026-08-29)
- Update to latest version from linuxserver/docker-brave (changelog : https://github.com/linuxserver/docker-brave/releases)
## 1.93.138-ls124 (2026-08-22)
- Update to latest version from linuxserver/docker-brave (changelog : https://github.com/linuxserver/docker-brave/releases)
## 1.93.136-ls122 (2026-08-13)
- Update to latest version from linuxserver/docker-brave (changelog : https://github.com/linuxserver/docker-brave/releases)
## 1.93.134-ls121 (2026-08-08)
- Update to latest version from linuxserver/docker-brave (changelog : https://github.com/linuxserver/docker-brave/releases)
## 1.93.129-ls119 (2026-08-01)
- Update to latest version from linuxserver/docker-brave (changelog : https://github.com/linuxserver/docker-brave/releases)
## 1.92.144-ls117 (2026-07-25)
- Update to latest version from linuxserver/docker-brave (changelog : https://github.com/linuxserver/docker-brave/releases)
## 1.92.141-ls115 (2026-07-18)
- Update to latest version from linuxserver/docker-brave (changelog : https://github.com/linuxserver/docker-brave/releases)
## 1.92.140-ls113 (2026-07-16)
- Update to latest version from linuxserver/docker-brave (changelog : https://github.com/linuxserver/docker-brave/releases)
## 1.92.139-ls112 (2026-07-11)
- Update to latest version from linuxserver/docker-brave (changelog : https://github.com/linuxserver/docker-brave/releases)
## 1.92.134-ls109 (2026-07-04)
- Update to latest version from linuxserver/docker-brave (changelog : https://github.com/linuxserver/docker-brave/releases)
## 1.91.180-ls108 (2026-06-27)
- Update to latest version from linuxserver/docker-brave (changelog : https://github.com/linuxserver/docker-brave/releases)
## 1.91.175-ls106 (2026-06-20)
- Update to latest version from linuxserver/docker-brave (changelog : https://github.com/linuxserver/docker-brave/releases)
## 1.91.172-ls105 (2026-06-13)
- Update to latest version from linuxserver/docker-brave (changelog : https://github.com/linuxserver/docker-brave/releases)
## 1.91.171-ls104 (2026-06-11)
- Update to latest version from linuxserver/docker-brave (changelog : https://github.com/linuxserver/docker-brave/releases)
## 1.91.168-ls103 (2026-06-05)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -1,12 +1,12 @@
## 2026.09.05 (2026-09-05)
- Update to latest version from linuxserver/docker-chromium (changelog : https://github.com/linuxserver/docker-chromium/releases)
- Upstream tag : version-d39975b5
## 2026.08.13 (2026-08-13)
- Update to latest version from linuxserver/docker-chromium (changelog : https://github.com/linuxserver/docker-chromium/releases)
- Upstream tag : version-e8713bf7
## 2026.08.01.1 (2026-08-01)
- Update to latest version from linuxserver/docker-chromium (changelog : https://github.com/linuxserver/docker-chromium/releases)
- Upstream tag : version-e2e1ec9c
@@ -14,25 +14,25 @@
- Version renamed from `version-bf9e0b4f`, which Home Assistant could not order and therefore could not reliably offer as an update: the previous version held no number that could order it, so the date of this change is used. The addon itself and the upstream version it tracks are unchanged
## version-bf9e0b4f (2026-07-25)
- Update to latest version from linuxserver/docker-chromium (changelog : https://github.com/linuxserver/docker-chromium/releases)
## version-ae9a7fca (2026-07-16)
- Update to latest version from linuxserver/docker-chromium (changelog : https://github.com/linuxserver/docker-chromium/releases)
## version-30a7c401 (2026-07-11)
- Update to latest version from linuxserver/docker-chromium (changelog : https://github.com/linuxserver/docker-chromium/releases)
## version-7148c2a3 (2026-07-04)
- Update to latest version from linuxserver/docker-chromium (changelog : https://github.com/linuxserver/docker-chromium/releases)
## version-ec868334 (2026-06-27)
- Update to latest version from linuxserver/docker-chromium (changelog : https://github.com/linuxserver/docker-chromium/releases)
## version-b9b82d3e (2026-06-20)
- Update to latest version from linuxserver/docker-chromium (changelog : https://github.com/linuxserver/docker-chromium/releases)
## version-cca5154d (2026-06-11)
- Update to latest version from linuxserver/docker-chromium (changelog : https://github.com/linuxserver/docker-chromium/releases)
## version-89f88890 (2026-06-05)

1
browser_chromium/rootfs/etc/cont-init.d/20-folders.sh Executable file → Normal file
View File

@@ -7,7 +7,6 @@ set -e
PUID=$(bashio::config "PUID")
PGID=$(bashio::config "PGID")
# Create cache
#mkdir -p /.cache
#chmod 755 /.cache

View File

@@ -5,12 +5,12 @@ set -Eeuo pipefail
# Resolve chromium binary (LSIO uses this path)
if [[ -x /usr/lib/chromium/chromium ]]; then
BIN=/usr/lib/chromium/chromium
BIN=/usr/lib/chromium/chromium
elif command -v chromium >/dev/null 2>&1; then
BIN="$(command -v chromium)"
BIN="$(command -v chromium)"
else
echo "ERROR: chromium binary not found" >&2
exit 127
echo "ERROR: chromium binary not found" >&2
exit 127
fi
# Fixed profile dir as requested
@@ -19,7 +19,7 @@ mkdir -p "${USER_DATA_DIR}"
# Cleanup stale locks if chromium is not already running
if ! pgrep -f 'chromium' >/dev/null 2>&1; then
rm -f "${USER_DATA_DIR}/Singleton"* || true
rm -f "${USER_DATA_DIR}/Singleton"* || true
fi
# Detect seccomp mode (0 = disabled)
@@ -27,22 +27,22 @@ seccomp_mode="$(awk -F':' '/^Seccomp:/{gsub(/[[:space:]]/,"",$2); print $2}' /pr
# Base arguments (exactly what you requested + required safety flags)
args=(
--remote-debugging-address=0.0.0.0
--remote-debugging-port=9221
--user-data-dir="${USER_DATA_DIR}"
--disable-dev-shm-usage
--no-first-run
--no-default-browser-check
--disable-background-networking
--disable-sync
--password-store=basic
"--simulate-outdated-no-au=Tue, 31 Dec 2099 23:59:59 GMT"
--start-maximized
--remote-debugging-address=0.0.0.0
--remote-debugging-port=9221
--user-data-dir="${USER_DATA_DIR}"
--disable-dev-shm-usage
--no-first-run
--no-default-browser-check
--disable-background-networking
--disable-sync
--password-store=basic
"--simulate-outdated-no-au=Tue, 31 Dec 2099 23:59:59 GMT"
--start-maximized
)
# Container reality: sandbox usually breaks unless seccomp is fully disabled
if [[ "${seccomp_mode}" != "0" ]]; then
args+=( --no-sandbox --test-type )
args+=(--no-sandbox --test-type)
fi
# IMPORTANT:

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -1,37 +1,37 @@
## 2.56.3 (2026-09-05)
- Update to latest version from browserless/chrome (changelog : https://github.com/browserless/chrome/releases)
## 2.56.0 (2026-08-22)
- Update to latest version from browserless/chrome (changelog : https://github.com/browserless/chrome/releases)
## 2.55.4 (2026-08-13)
- Update to latest version from browserless/chrome (changelog : https://github.com/browserless/chrome/releases)
## 2.55.3 (2026-08-08)
- Update to latest version from browserless/chrome (changelog : https://github.com/browserless/chrome/releases)
## 2.55.2 (2026-08-01)
- Update to latest version from browserless/chrome (changelog : https://github.com/browserless/chrome/releases)
## 2.55.0 (2026-07-16)
- Update to latest version from browserless/chrome (changelog : https://github.com/browserless/chrome/releases)
## 2.54.2 (2026-07-04)
- Update to latest version from browserless/chrome (changelog : https://github.com/browserless/chrome/releases)
## 2.54.1 (2026-06-23)
- Update to latest version from browserless/chrome (changelog : https://github.com/browserless/chrome/releases)
## 2.54.0 (2026-06-20)
- Update to latest version from browserless/chrome (changelog : https://github.com/browserless/chrome/releases)
## 2.52.2 (2026-06-17)
- Update to latest version from browserless/chrome (changelog : https://github.com/browserless/chrome/releases)
## 2.52.1 (2026-06-13)
- Update to latest version from browserless/chrome (changelog : https://github.com/browserless/chrome/releases)
## 2.52.0 (2026-06-11)
- Update to latest version from browserless/chrome (changelog : https://github.com/browserless/chrome/releases)
## 2.51.0 (2026-06-05)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@@ -1,16 +1,16 @@
## 9.14.0 (2026-08-29)
- Update to latest version from linuxserver/docker-calibre (changelog : https://github.com/linuxserver/docker-calibre/releases)
## 9.13.0 (2026-08-08)
- Update to latest version from linuxserver/docker-calibre (changelog : https://github.com/linuxserver/docker-calibre/releases)
## 9.12.0 (2026-08-01)
- Update to latest version from linuxserver/docker-calibre (changelog : https://github.com/linuxserver/docker-calibre/releases)
## 9.11.0 (2026-07-04)
- Update to latest version from linuxserver/docker-calibre (changelog : https://github.com/linuxserver/docker-calibre/releases)
## 9.10.0 (2026-06-27)
- Update to latest version from linuxserver/docker-calibre (changelog : https://github.com/linuxserver/docker-calibre/releases)

View File

@@ -62,7 +62,7 @@ Please read the upstream container documentation for further info: https://githu
### Options
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| -------- | ------ | --------- | ------------- |
| `PGID` | int | `0` | Group ID for file permissions |
| `PUID` | int | `0` | User ID for file permissions |
| `TZ` | str | | Timezone (e.g., `Europe/London`) |

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -1,4 +1,4 @@
## 0.6.27.4 (2026-09-04)
- Fix: Kobo sync could not be enabled, failing with "Kepubify binary not found" even when the path was set by hand. The LinuxServer base image installs the converter as `/usr/bin/kepubify` with `curl -o`, which leaves it mode 0644 and gives it a name calibre-web does not accept : `binary_helper.py` only takes `kepubify-linux-64bit` or `kepubify-linux-32bit`, and only when `os.access(X_OK)` passes. The addon now makes the binary executable and publishes it as `/opt/kepubify/kepubify-linux-64bit`, the directory calibre-web's own autodetection already probes, so the path is filled in without any manual step (https://github.com/alexbelgium/hassio-addons/issues/3040)
- Fix: on installs created before that change, calibre-web had already run its autodetection once, found nothing usable and stored an empty path, and it never retries. An empty path is now reset so calibre-web detects the converter itself at the next start. A path set by hand is left alone

View File

@@ -62,7 +62,7 @@ Default password: admin123
### Options
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| -------- | ------ | --------- | ------------- |
| `PGID` | int | `0` | Group ID for file permissions |
| `PUID` | int | `0` | User ID for file permissions |
| `TZ` | str | | Timezone (e.g., `Europe/London`) |
@@ -99,7 +99,7 @@ This addon supports mounting both local drives and remote SMB shares:
### Optional Calibre-Web features
Calibre-Web documents optional extras that a manual installation adds with `pip install calibreweb[metadata]` and similar. **You do not need to install anything here**: the LinuxServer base image this add-on builds on installs Calibre-Web's `requirements.txt` *and* its full `optional-requirements.txt` into the application's virtualenv, so the gdrive, gmail, goodreads, ldap, oauth, metadata, comics and kobo dependencies are all present already. Running `pip install calibreweb[...]` inside the container is not a supported way to enable them: it installs the PyPI distribution of Calibre-Web over an installation that already has those dependencies, and it can disturb the versions the base image pinned. It is also thrown away, because the Supervisor recreates the add-on container on restart.
Calibre-Web documents optional extras that a manual installation adds with `pip install calibreweb[metadata]` and similar. **You do not need to install anything here**: the LinuxServer base image this add-on builds on installs Calibre-Web's `requirements.txt` _and_ its full `optional-requirements.txt` into the application's virtualenv, so the gdrive, gmail, goodreads, ldap, oauth, metadata, comics and kobo dependencies are all present already. Running `pip install calibreweb[...]` inside the container is not a supported way to enable them: it installs the PyPI distribution of Calibre-Web over an installation that already has those dependencies, and it can disturb the versions the base image pinned. It is also thrown away, because the Supervisor recreates the add-on container on restart.
Optional features are switched on in the Calibre-Web web interface, not in the add-on options, under `Admin` -> `Basic Configuration` -> `Feature Configuration` (for example `Enable Uploads`, `Enable Kobo sync`, `Use Goodreads`).

View File

@@ -30,16 +30,16 @@ else
# duplicates it can leave behind are entries calibre-web skips or already trusts.
# The column only exists once calibre-web 0.6.27+ has migrated app.db and cont-init runs
# before calibre-web, so a failure here is not fatal : the next start applies it.
trusted_ips_error=$(sqlite3 /config/app.db "update settings set config_reverse_proxy_trusted_ips='127.0.0.1,::1,::ffff:127.0.0.1,172.30.32.0/23,::ffff:172.30.32.0/119,'||coalesce(config_reverse_proxy_trusted_ips,'') where coalesce(config_reverse_proxy_trusted_ips,'') not like '%::ffff:172.30.32.0/119%'" 2>&1) ||
bashio::log.warning "Could not set the ingress trusted ip list, it will be applied at next start (${trusted_ips_error})"
trusted_ips_error=$(sqlite3 /config/app.db "update settings set config_reverse_proxy_trusted_ips='127.0.0.1,::1,::ffff:127.0.0.1,172.30.32.0/23,::ffff:172.30.32.0/119,'||coalesce(config_reverse_proxy_trusted_ips,'') where coalesce(config_reverse_proxy_trusted_ips,'') not like '%::ffff:172.30.32.0/119%'" 2>&1) \
|| bashio::log.warning "Could not set the ingress trusted ip list, it will be applied at next start (${trusted_ips_error})"
# Calibre-web autodetects kepubify only while this setting is still NULL. On every install
# that predates the Dockerfile fix above, that detection already ran, found nothing usable and
# stored an empty string, so it is never retried. Put an empty value back to NULL and
# calibre-web detects /opt/kepubify itself when it starts, a few seconds after this runs.
# A path the user set by hand is not empty and is left alone.
kepubify_error=$(sqlite3 /config/app.db "update settings set config_kepubifypath = NULL where config_kepubifypath = ''" 2>&1) ||
bashio::log.warning "Could not reset the kepubify path, it will be applied at next start (${kepubify_error})"
kepubify_error=$(sqlite3 /config/app.db "update settings set config_kepubifypath = NULL where config_kepubifypath = ''" 2>&1) \
|| bashio::log.warning "Could not reset the kepubify path, it will be applied at next start (${kepubify_error})"
fi
bashio::log.info "Default username:password is admin:admin123"

View File

@@ -1,7 +1,7 @@
## 0.60.3 (2026-09-05)
- Update to latest version from linuxserver/docker-changedetection.io (changelog : https://github.com/linuxserver/docker-changedetection.io/releases)
## 0.55.8 (2026-07-16)
- Update to latest version from linuxserver/docker-changedetection.io (changelog : https://github.com/linuxserver/docker-changedetection.io/releases)

View File

@@ -52,7 +52,7 @@ You can add a shortcut pointing to your Changedetection.io instance with the fol
### Options
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| -------- | ------ | --------- | ------------- |
| `PGID` | int | `0` | Group ID for file permissions |
| `PUID` | int | `0` | User ID for file permissions |
| `TZ` | str | | Timezone (e.g., `Europe/London`) |

0
changedetection.io/rootfs/etc/services.d/nginx/run Normal file → Executable file
View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -1,12 +1,12 @@
## 07308545.6 (2026-09-05)
- Update to latest version from aaddrick/claude-desktop-debian (changelog : https://github.com/aaddrick/claude-desktop-debian/releases)
- Upstream tag : v3.2.3+claude1.40609.1
## 07308545.5 (2026-08-29)
- Update to latest version from aaddrick/claude-desktop-debian (changelog : https://github.com/aaddrick/claude-desktop-debian/releases)
- Upstream tag : v3.2.2+claude1.37937.3
## 07308545.4 (2026-08-22)
- Update to latest version from aaddrick/claude-desktop-debian (changelog : https://github.com/aaddrick/claude-desktop-debian/releases)
- Upstream tag : v3.2.2+claude1.32885.1
@@ -71,7 +71,7 @@
## 07308545.1 (17-08-2026)
- Minor bugs fixed
## 07308543.1 (17-08-2026)
- Fix the "For your security, sign in again" prompt recurring on every restart again. The v1.37
`safeStorage` patch (`86-claude_safestorage.sh` / `claude-safestorage-patch.js`) only knew how
@@ -94,11 +94,11 @@
## 07308545 (2026-08-15)
- Update to latest version from aaddrick/claude-desktop-debian (changelog : https://github.com/aaddrick/claude-desktop-debian/releases)
- Upstream tag : v3.2.2+claude1.30096.1
## 07308544 (2026-08-13)
- Update to latest version from aaddrick/claude-desktop-debian (changelog : https://github.com/aaddrick/claude-desktop-debian/releases)
- Upstream tag : v3.2.2+claude1.28929.0
## 07308543 (2026-08-08)
- Update to latest version from linuxserver/docker-baseimage-selkies (changelog : https://github.com/linuxserver/docker-baseimage-selkies/releases)
- Upstream tag : debiantrixie-version-07308543
@@ -185,10 +185,10 @@
## 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)
## kali-version-e963b19b (2026-08-01)
- Update to latest version from linuxserver/docker-baseimage-selkies (changelog : https://github.com/linuxserver/docker-baseimage-selkies/releases)
## 1.37 (28-07-2026)
@@ -199,7 +199,7 @@
- **`gnome-keyring` stays out of the image.** Re-adding it would reintroduce the first-boot keyring password prompt that blocks Claude Desktop from launching at all. This route needs no keyring, no D-Bus Secret Service, no daemon and no password, so no prompt can appear. The trade-off is unchanged from v1.35 and is inherent to the `basic` backend: its key is fixed rather than gated by a keyring, so any process that can read the persistent `$HOME/.config/Claude` profile can recover the stored credentials.
- **One-time step after upgrading.** The previously stored session is already stale, so a single sign-in is still needed once after this update; it then persists across restarts.
## 1.36.4 (28-07-2026)
- Fix Selkies dying with a Rust `RuntimeDirNotSet` unwrap panic just after `Data WebSocket Server listening on port 8081`, and the data websocket then being proxied to the wrong port. Upstream relies on s6-rc ordering: `init-selkies-config` publishes `XDG_RUNTIME_DIR` and `CUSTOM_WS_PORT` into the s6 envdir and `svc-selkies` starts afterwards. The add-on entrypoint replaces s6-overlay and starts every `s6-rc.d` run script in parallel with no dependency graph, so Selkies can snapshot the envdir before that oneshot has written to it -- which is why it bound port 8081 (its own default) instead of the 8082 nginx proxies to, and why its Wayland compositor found no runtime directory to bind a socket in. `20-folders.sh` now exports both variables inside each run script, where no start ordering can lose them, and corrects the base image's `$HOME/.XDG` override where that write happens instead of appending a correction after the `exit 0` that the oneshot-tolerance block adds -- which meant the correction never ran on any boot after the first.
@@ -210,7 +210,7 @@
## 1.36.2 (28-07-2026)
- Minor bugs fixed
## 1.36.1 (27-07-2026)
- Fix the Codex CLI install failing on every boot with `Verified Codex <version> installation failed; Codex is unavailable this boot`, leaving `install_codex_cli` permanently non-functional. The download, its SHA-256 verification, and the extraction all succeeded; the chain broke at the final step, which validates the candidate binary by running `--version` as the `abc` runtime user. `mktemp -d` creates its directory `0700 root:root`, and `abc` cannot traverse a root-only directory, so executing the staged binary failed with `unable to exec: Permission denied` (exit 126) before it could be moved into place. Reproduced and fixed by making the staging directory traversable (`chmod 0755`) immediately after `mktemp`; verified on a live add-on container, where the same probe goes from exit 126 to success once the mode is widened. Nothing secret is staged there — the public release archive and the extracted binary, both world-readable upstream artifacts — and the existing `cleanup()` trap still removes the directory on exit. The validation deliberately keeps running as `abc` rather than root, so the binary is exercised as the identity that will actually run it.
@@ -240,10 +240,10 @@
## 1.33 (22-07-2026)
- Add cowork virtualization support: `qemu-system-x86` and `ovmf` (Bookworm main, installed via apt) plus `virtiofsd` for sharing the workspace into the sandbox microVM. `virtiofsd` is only packaged for Debian trixie/sid, not Bookworm or bookworm-backports, and its trixie `.deb` links a newer GLIBC than this add-on's Bookworm runtime — so it now gets built from the pinned crates.io release (`1.14.0`) in a dedicated `virtiofsd-builder` stage, the same GLIBC-safe pattern already used for `rtk` and `tokensave`. Its build deps (`libseccomp-dev`, `libcap-ng-dev`, `pkg-config`, `clang`, `libclang-dev`) live only in that builder stage; only the runtime shared libs (`libseccomp2`, `libcap-ng0`) ship in the final image. The built binary is validated with `--version` at build time alongside `rtk`/`tokensave`, so a GLIBC/ABI mismatch fails the image build instead of surfacing at container start. Docker itself is deliberately not installed: this base image already ships Docker-in-Docker (`docker-ce`/`containerd.io` from Docker's own apt repo, started via the pre-existing `START_DOCKER` env var) — an initial attempt to also `apt-get install docker.io` broke the build, since Debian's package pulls in `containerd`/`runc`, which apt refuses to install alongside the base image's already-installed `containerd.io` (`Conflicts`).
## ubunturesolute-version-8208e985 (2026-07-21)
- Update to latest version from linuxserver/docker-baseimage-selkies (changelog : https://github.com/linuxserver/docker-baseimage-selkies/releases)
## kali-version-9ad48e7a (2026-07-18)
- Update to latest version from linuxserver/docker-baseimage-selkies (changelog : https://github.com/linuxserver/docker-baseimage-selkies/releases)
## 1.32 (17-07-2026)

View File

@@ -55,9 +55,9 @@
# 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}')"
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=""
SHM_FLAGS=""
fi
# SHM_FLAGS must stay unquoted so it expands to a separate argument, or to nothing at all.

View File

@@ -141,7 +141,7 @@ release_info=""
if curl -fsSL --retry 3 --retry-delay 2 --connect-timeout 10 --max-time 30 \
-o "$release_metadata" "$CODEX_RELEASE_API"; then
release_info="$(
CODEX_ASSET="$CODEX_ASSET" python3 - "$release_metadata" <<'PY' 2> /dev/null || true
CODEX_ASSET="$CODEX_ASSET" python3 - "$release_metadata" << 'PY' 2> /dev/null || true
import json
import os
import re
@@ -249,7 +249,7 @@ fi
{
printf '#!/usr/bin/env bash\n'
printf 'CODEX_REAL=%q\n' "$CODEX_REAL"
cat <<'SH'
cat << 'SH'
RUNTIME_HOME="$(getent passwd abc | cut -d: -f6)"
if [ -z "$RUNTIME_HOME" ]; then
echo "codex: unable to resolve the abc runtime home" >&2
@@ -338,8 +338,7 @@ ln -sfn "$CODEX_BIN" "$CODEX_LINK"
CODEX_SANDBOX_MODE="$(bashio::config 'codex_sandbox_mode' 'danger-full-access')"
run_as_runtime_user mkdir -p "$RUNTIME_HOME/.codex"
CODEX_SANDBOX_MODE="$CODEX_SANDBOX_MODE" RUNTIME_HOME="$RUNTIME_HOME" \
run_as_runtime_user python3 - <<'PY' \
|| bashio::log.warning "Unable to update the managed Codex configuration block"
run_as_runtime_user python3 - << 'PY' || bashio::log.warning "Unable to update the managed Codex configuration block"
import os
import re
import tomllib

View File

@@ -30,7 +30,7 @@ manage_claude_md_block() {
fi
elif [ -f "$CLAUDE_MD" ] && grep -qF "$begin" "$CLAUDE_MD"; then
bashio::log.info "Removing ${name} guidance from CLAUDE.md"
CLAUDE_MD="$CLAUDE_MD" BLOCK_NAME="$name" python3 - <<'PY' || bashio::log.warning "Unable to remove the ${name} guidance automatically"
CLAUDE_MD="$CLAUDE_MD" BLOCK_NAME="$name" python3 - << 'PY' || bashio::log.warning "Unable to remove the ${name} guidance automatically"
import os
import re
from pathlib import Path
@@ -58,7 +58,7 @@ PY
manage_settings_hook() {
# manage_settings_hook <event> <matcher> <command> <add|remove>
HOOK_EVENT="$1" HOOK_MATCHER="$2" HOOK_COMMAND="$3" HOOK_ACTION="$4" \
python3 - <<'PY' || bashio::log.warning "Unable to update the $1 hook for '$3'"
python3 - << 'PY' || bashio::log.warning "Unable to update the $1 hook for '$3'"
import json
import os
from pathlib import Path
@@ -389,14 +389,14 @@ 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"
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
import os
from pathlib import Path
@@ -607,7 +607,7 @@ PY
# Guide Claude to actually use the Headroom compression tools so the MCP integration produces
# real savings when transparent proxying is unavailable.
if $HEADROOM_ENABLED; then
manage_claude_md_block headroom add <<'MD'
manage_claude_md_block headroom add << 'MD'
## Headroom context compression
A local Headroom proxy (127.0.0.1:8787) backs the `headroom` MCP tools. To save context tokens:
@@ -636,7 +636,7 @@ if $HEADROOM_ENABLED && bashio::config.true 'headroom_wrap_claude_code'; then
else
HEADROOM_ROUTE_ACTION="remove"
fi
HEADROOM_ROUTE_ACTION="$HEADROOM_ROUTE_ACTION" python3 - <<'PY' || bashio::log.warning "Unable to manage the Claude Code proxy routing env"
HEADROOM_ROUTE_ACTION="$HEADROOM_ROUTE_ACTION" python3 - << 'PY' || bashio::log.warning "Unable to manage the Claude Code proxy routing env"
import json
import os
from pathlib import Path
@@ -703,7 +703,7 @@ manage_settings_hook PostToolUse "Bash|Grep|Glob|WebFetch" "$HEADROOM_HOOK_CMD"
# Tell Claude Code that it can configure Home Assistant over the Core API via the shipped
# `ha-cli` helper (no /config filesystem mount needed).
if bashio::config.true 'enable_ha_api_helper'; then
manage_claude_md_block ha-api-helper add <<'MD'
manage_claude_md_block ha-api-helper add << 'MD'
## Configuring Home Assistant
You can configure this Home Assistant instance through its Core API using the `ha-cli`
@@ -731,7 +731,7 @@ fi
# Registering the MCP server is not enough on its own: without guidance the model rarely reaches
# for a second agent, the same gap the Headroom block above exists to close.
if $CODEX_ENABLED; then
manage_claude_md_block codex add <<'MD'
manage_claude_md_block codex add << 'MD'
## Delegating to ChatGPT Codex
The `codex` MCP server runs OpenAI's Codex agent locally, signed in with the user's ChatGPT

View File

@@ -19,7 +19,7 @@ esac
# only while it still holds one of those managed values — a defaultMode the user set by hand
# is never deleted. Ownership of the written file is reconciled by 84-claude_runtime_ownership.sh.
mkdir -p "$(dirname "$SETTINGS_PATH")"
PERMISSION_MODE="$PERMISSION_MODE" SETTINGS_PATH="$SETTINGS_PATH" python3 - <<'PY'
PERMISSION_MODE="$PERMISSION_MODE" SETTINGS_PATH="$SETTINGS_PATH" python3 - << 'PY'
import json
import os
from pathlib import Path

View File

@@ -36,7 +36,7 @@ fi
# archive stays unpatched, so every later boot would retry under a new pid and strand another
# copy until the container runs out of space.
cleanup_tmp() {
find "$(dirname "$ASAR")" -maxdepth 1 -name ".$(basename "$ASAR").addon-tmp.*" -delete 2>/dev/null || true
find "$(dirname "$ASAR")" -maxdepth 1 -name ".$(basename "$ASAR").addon-tmp.*" -delete 2> /dev/null || true
}
if output=$(timeout 120 node "$PATCHER" "$ASAR" 2>&1); then

View File

@@ -45,7 +45,7 @@ if [ "$(bashio::config 'permission_mode')" = "bypass" ]; then
fi
section "Claude Code permission state"
RUNTIME_HOME="$RUNTIME_HOME" python3 - <<'PY'
RUNTIME_HOME="$RUNTIME_HOME" python3 - << 'PY'
import json
import os
from pathlib import Path
@@ -66,7 +66,7 @@ else:
PY
section "MCP registrations (environment values redacted)"
RUNTIME_HOME="$RUNTIME_HOME" python3 - <<'PY'
RUNTIME_HOME="$RUNTIME_HOME" python3 - << 'PY'
import json
import os
from pathlib import Path
@@ -104,7 +104,7 @@ for path in paths:
PY
section "Claude Code hooks"
RUNTIME_HOME="$RUNTIME_HOME" python3 - <<'PY'
RUNTIME_HOME="$RUNTIME_HOME" python3 - << 'PY'
import json
import os
from pathlib import Path

0
claude_desktop/rootfs/usr/local/bin/codex-login Executable file → Normal file
View File

14
claude_desktop/rootfs/usr/local/bin/ha-cli Executable file → Normal file
View File

@@ -96,7 +96,7 @@ def resolve_endpoint():
def _url(base, path):
path = path.lstrip("/")
if path.startswith("api/"):
path = path[len("api/"):]
path = path[len("api/") :]
return base.rstrip("/") + "/" + path
@@ -133,7 +133,9 @@ def rest(method, base, token, path, body=None):
if body is not None:
data = json.dumps(body).encode("utf-8")
headers["Content-Type"] = "application/json"
req = urllib.request.Request(_url(base, path), data=data, method=method, headers=headers)
req = urllib.request.Request(
_url(base, path), data=data, method=method, headers=headers
)
try:
with urllib.request.urlopen(req, timeout=30) as resp:
text = resp.read().decode("utf-8")
@@ -141,7 +143,9 @@ def rest(method, base, token, path, body=None):
detail = exc.read().decode("utf-8", "replace").strip()
sys.exit(f"ha-cli: HTTP {exc.code} {exc.reason} on {method} {path}\n{detail}")
except urllib.error.URLError as exc:
sys.exit(f"ha-cli: cannot reach Home Assistant ({exc.reason}) on {method} {path}")
sys.exit(
f"ha-cli: cannot reach Home Assistant ({exc.reason}) on {method} {path}"
)
try:
return json.loads(text) if text.strip() else None
except ValueError:
@@ -214,7 +218,9 @@ def main(argv):
sys.exit("usage: ha-cli call <domain.service> [BODY]")
domain, service = args[0].split(".", 1)
body = _read_body(args[1]) if len(args) > 1 else None
_print(rest("POST", rest_base, token, f"services/{domain}/{service}", body or {}))
_print(
rest("POST", rest_base, token, f"services/{domain}/{service}", body or {})
)
elif cmd == "states":
path = f"states/{args[0]}" if args else "states"
_print(rest("GET", rest_base, token, path))

View File

@@ -76,7 +76,11 @@ def main() -> int:
# Find big string/array fields before paying the headroom import cost.
def is_string_array(value):
return isinstance(value, list) and len(value) > ARRAY_KEEP and all(isinstance(v, str) for v in value)
return (
isinstance(value, list)
and len(value) > ARRAY_KEEP
and all(isinstance(v, str) for v in value)
)
if isinstance(response, str):
string_candidates = ["__whole__"] if len(response) >= MIN_CHARS else []
@@ -85,10 +89,14 @@ def main() -> int:
string_candidates = [
key
for key, value in response.items()
if key not in SKIP_KEYS and isinstance(value, str) and len(value) >= MIN_CHARS
if key not in SKIP_KEYS
and isinstance(value, str)
and len(value) >= MIN_CHARS
]
array_candidates = [
key for key, value in response.items() if key not in SKIP_KEYS and is_string_array(value)
key
for key, value in response.items()
if key not in SKIP_KEYS and is_string_array(value)
]
else:
string_candidates = []

Some files were not shown because too many files have changed in this diff Show More