Compare commits

..

22 Commits

Author SHA1 Message Date
github-actions
dcc21988e3 GitHub bot: changelog [nobuild] 2026-09-17 10:23:46 +00:00
Alexandre
47363725ea Update config.yaml 2026-09-17 12:17:36 +02:00
github-actions
b42a6ac3aa GitHub bot: changelog [nobuild] 2026-09-17 05:55:48 +00:00
Alexandre
f61b3b95d3 Update config.yaml 2026-09-17 07:49:33 +02:00
github-actions
15a2abe451 GitHub bot: changelog [nobuild] 2026-09-16 15:54:51 +00:00
Alexandre
4c1038916d Update config.yaml 2026-09-16 17:49:36 +02:00
github-actions
71bbdb0929 GitHub bot: changelog [nobuild] 2026-09-16 14:04:48 +00:00
Alexandre
41f0863702 Update config.yaml 2026-09-16 15:58:39 +02:00
github-actions
8fec8556e8 GitHub bot: changelog [nobuild] 2026-09-16 11:35:30 +00:00
Alexandre
3ec897fa38 Update config.yaml 2026-09-16 13:29:08 +02:00
GitHub Actions
5a4d7b2f8c Revert "Update config.yaml"
This reverts commit b519002cea.
2026-09-16 09:01:55 +00:00
Alexandre
b519002cea Update config.yaml 2026-09-16 10:58:36 +02:00
github-actions
bad72b9451 GitHub bot: changelog [nobuild] 2026-09-15 10:03:57 +00:00
Alexandre
4028564991 Update config.yaml 2026-09-15 11:57:02 +02:00
GitHub Actions
096db771fc Revert "birdnet-go-dev: bump to 20260915 (upstream sync, PR updates)"
This reverts commit 58e98fb63c.
2026-09-15 07:11:59 +00:00
BirdNET-Go Addon Builder
58e98fb63c birdnet-go-dev: bump to 20260915 (upstream sync, PR updates) 2026-09-15 09:05:40 +02:00
dependabot[bot]
40d0401ccd build(deps): bump home-assistant/builder from 2026.06.0 to 2026.09.0 (#3065)
Bumps [home-assistant/builder](https://github.com/home-assistant/builder) from 2026.06.0 to 2026.09.0.
- [Release notes](https://github.com/home-assistant/builder/releases)
- [Commits](https://github.com/home-assistant/builder/compare/2026.06.0...2026.09.0)

---
updated-dependencies:
- dependency-name: home-assistant/builder
  dependency-version: 2026.09.0
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-09-15 07:23:11 +02:00
dependabot[bot]
883b38cacc build(deps): bump anthropics/claude-code-action from 1.0.216 to 1.0.222 (#3066)
Bumps [anthropics/claude-code-action](https://github.com/anthropics/claude-code-action) from 1.0.216 to 1.0.222.
- [Release notes](https://github.com/anthropics/claude-code-action/releases)
- [Commits](d75b94d5ad...56cf60fde4)

---
updated-dependencies:
- dependency-name: anthropics/claude-code-action
  dependency-version: 1.0.222
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-09-15 07:22:17 +02:00
github-actions
80821a7d9a Github bot : image compressed 2026-09-13 23:13:02 +00:00
Alexandre
e7f8c37695 Remove scheduled linting from lint.yml
Removed scheduled linting from the workflow.
2026-09-13 07:13:33 +02:00
Alexandre
88faf503fb fix(transmission): let incomplete-dir stay off across restarts (#3063)
* fix(transmission): let incomplete-dir stay off across restarts

01-config.sh rewrites Transmission's settings.json on every restart,
deriving "incomplete-dir-enabled" purely from whether incomplete_dir is
non-empty. Since incomplete_dir defaults to a non-empty path, any toggle
made in Transmission's own Web UI was silently overwritten back to enabled
on the next restart (#3059).

Add incomplete_dir_enabled (bool, default true): setting it to false writes
the disabled state on every restart instead of forcing it back on. The
default of true preserves normal existing configurations unchanged; the two
edge cases it also touches were already latent bugs, see below.

While touching this line: bashio::config's own default-value argument
cannot be an empty string (bash's ${2:-null} treats "" as unset), so an
install whose incomplete_dir key was entirely absent, or literally the four
characters "null", returned bashio's own "null" fallback and would have
created a directory named "null". Normalized that explicitly; both now
disable instead.

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

* fix(transmission): keep incomplete-dir enabled on upgrade in standalone mode

The previous commit relied on bashio::config's default argument to treat
an absent incomplete_dir_enabled as true. The standalone shim
(.templates/bashio-standalone.sh), injected by 00-banner.sh when the image
runs without Supervisor, ignores that argument and returns "" for a
missing key, so an upgraded standalone install would have silently
disabled its incomplete directory.

Treat anything but an explicit "false" as enabled instead. That covers
real bashio ("null") and the shim ("") alike, and drops the default
argument altogether.

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

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-13 07:10:57 +02:00
github-actions[bot]
b88c5c5826 Update stargazer map & cache 2026-09-13 00:55:02 +00:00
449 changed files with 2780 additions and 2900 deletions

View File

@@ -30,14 +30,8 @@ VAR="${1:?usage: env_trace.sh <VAR> [process-name-or-pid]}"
TARGET="${2:-}" TARGET="${2:-}"
# VAR is interpolated into grep/sed patterns below — restrict it to a valid env var name # VAR is interpolated into grep/sed patterns below — restrict it to a valid env var name
case "$VAR" in case "$VAR" in
[A-Za-z_]*) [ -z "${VAR//[A-Za-z0-9_]/}" ] || { [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 *) echo "invalid env var name: $VAR" >&2; exit 1 ;;
exit 1
} ;;
*)
echo "invalid env var name: $VAR" >&2
exit 1
;;
esac esac
echo "== tracing ${VAR} ==" echo "== tracing ${VAR} =="
@@ -45,7 +39,7 @@ echo
echo "1. /data/options.json (the user's saved options)" echo "1. /data/options.json (the user's saved options)"
if [ -f /data/options.json ]; then if [ -f /data/options.json ]; then
python3 - "$VAR" << 'PY' python3 - "$VAR" <<'PY'
import json, sys import json, sys
var = sys.argv[1] var = sys.argv[1]
try: try:
@@ -86,8 +80,7 @@ echo "3. s6 container_environment (only read by services using #!/usr/bin/with-c
seen3=0 seen3=0
for d in /var/run/s6/container_environment /run/s6/container_environment; do for d in /var/run/s6/container_environment /run/s6/container_environment; do
if [ -f "$d/$VAR" ]; then if [ -f "$d/$VAR" ]; then
echo " $d/$VAR = [$(cat "$d/$VAR")]" echo " $d/$VAR = [$(cat "$d/$VAR")]"; seen3=1
seen3=1
fi fi
done done
[ "$seen3" -eq 0 ] && echo " not present in either envdir" [ "$seen3" -eq 0 ] && echo " not present in either envdir"
@@ -129,8 +122,8 @@ else
done done
fi fi
# What it was actually launched with beats any theory about its environment. # What it was actually launched with beats any theory about its environment.
tr '\0' '\n' < "/proc/$pid/cmdline" 2> /dev/null | tail -n +2 \ tr '\0' '\n' < "/proc/$pid/cmdline" 2> /dev/null | tail -n +2 |
| grep -iE "res|screen|${VAR}" | head -3 | sed 's/^/ argv: /' grep -iE "res|screen|${VAR}" | head -3 | sed 's/^/ argv: /'
done done
fi fi
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
echo "== per-process memory (top 20 by PSS) ==" echo "== per-process memory (top 20 by PSS) =="
printf ' %-28s %8s %8s %8s\n' COMMAND RSS PSS PRIVATE printf ' %-28s %8s %8s %8s\n' COMMAND RSS PSS PRIVATE
python3 - "$OUT" << 'PY' python3 - "$OUT" <<'PY'
import os, sys import os, sys
rows = [] rows = []
for pid in filter(str.isdigit, os.listdir('/proc')): for pid in filter(str.isdigit, os.listdir('/proc')):
@@ -62,17 +62,17 @@ PY
echo echo
echo "== reserved-but-not-resident (lazy allocations, NOT leaks) ==" 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
echo "== CPU over ${SAMPLE}s (idle unless you are driving the UI) ==" 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 — # 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 # a plain $14+$15 is wrong for anything like 'npm exec @foo' and silently reports a fabricated
# number rather than failing. # 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 # 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, # 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. # and the busiest process is not necessarily a big one.
@@ -86,14 +86,14 @@ for pid in "${!t0[@]}"; do
[ -r "/proc/$pid/stat" ] || continue [ -r "/proc/$pid/stat" ] || continue
t1=$(jiffies "$pid") || continue t1=$(jiffies "$pid") || continue
[ -n "$t1" ] && [ -n "${t0[$pid]}" ] || continue [ -n "$t1" ] && [ -n "${t0[$pid]}" ] || continue
delta=$((t1 - ${t0[$pid]})) delta=$(( t1 - ${t0[$pid]} ))
[ "$delta" -gt 0 ] || continue [ "$delta" -gt 0 ] || continue
pct=$(awk -v d="$delta" -v s="$SAMPLE" -v hz="$HZ" 'BEGIN{printf "%.2f", d*100/(hz*s)}') 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" echo "$pct $pid $comm"
done | sort -rn | head -12 | awk '{printf " %6s%% %-8s %s\n", $1, $2, $3}' done | sort -rn | head -12 | awk '{printf " %6s%% %-8s %s\n", $1, $2, $3}'
echo 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 " (those are claude_desktop/webtop viewer ports; 0 here means CPU above is idle burn)"
echo " raw ps: $OUT/ps.txt" echo " raw ps: $OUT/ps.txt"

View File

@@ -22,20 +22,13 @@ set -uo pipefail
REPO="${HASSIO_REPO:-}" REPO="${HASSIO_REPO:-}"
[ -z "$REPO" ] && REPO=$(gh repo view --json nameWithOwner --jq .nameWithOwner 2> /dev/null) [ -z "$REPO" ] && REPO=$(gh repo view --json nameWithOwner --jq .nameWithOwner 2> /dev/null)
[ -z "$REPO" ] && { [ -z "$REPO" ] && { echo "cannot determine repo; set HASSIO_REPO=owner/name" >&2; exit 1; }
echo "cannot determine repo; set HASSIO_REPO=owner/name" >&2
exit 1
}
echo "repo: $REPO" >&2 echo "repo: $REPO" >&2
CMD="${1:-}" CMD="${1:-}"; PR="${2:-}"
PR="${2:-}" [ -z "$CMD" ] || [ -z "$PR" ] && { sed -n '2,20p' "$0" | sed 's/^# \?//'; exit 1; }
[ -z "$CMD" ] || [ -z "$PR" ] && {
sed -n '2,20p' "$0" | sed 's/^# \?//'
exit 1
}
case "$CMD" in case "$CMD" in
list) list)
echo "== inline comments on #$PR ==" echo "== inline comments on #$PR =="
gh api "repos/$REPO/pulls/$PR/comments" --paginate \ gh api "repos/$REPO/pulls/$PR/comments" --paginate \
--jq 'sort_by(.created_at)[] | "=== [\(.id)] \(.user.login) | \(.path):\(.line // .original_line) ===\n\(.body)\n"' --jq 'sort_by(.created_at)[] | "=== [\(.id)] \(.user.login) | \(.path):\(.line // .original_line) ===\n\(.body)\n"'
@@ -43,15 +36,14 @@ case "$CMD" in
gh api "repos/$REPO/pulls/$PR/reviews" \ gh api "repos/$REPO/pulls/$PR/reviews" \
--jq '.[] | select(.body != "") | "--- \(.user.login) (\(.state)) ---\n\(.body[0:4000])\n"' --jq '.[] | select(.body != "") | "--- \(.user.login) (\(.state)) ---\n\(.body[0:4000])\n"'
;; ;;
status) status)
gh pr checks "$PR" 2>&1 | head -15 gh pr checks "$PR" 2>&1 | head -15
echo echo
gh api graphql -f query="{repository(owner:\"${REPO%%/*}\",name:\"${REPO##*/}\"){pullRequest(number:$PR){reviewThreads(first:50){nodes{id isResolved path comments(first:1){nodes{author{login}}}}}}}}" \ gh api graphql -f query="{repository(owner:\"${REPO%%/*}\",name:\"${REPO##*/}\"){pullRequest(number:$PR){reviewThreads(first:50){nodes{id isResolved path comments(first:1){nodes{author{login}}}}}}}}" \
--jq '.data.repository.pullRequest.reviewThreads.nodes[] | "\(if .isResolved then "resolved" else "OPEN " end) \(.id) \(.comments.nodes[0].author.login) \(.path)"' --jq '.data.repository.pullRequest.reviewThreads.nodes[] | "\(if .isResolved then "resolved" else "OPEN " end) \(.id) \(.comments.nodes[0].author.login) \(.path)"'
;; ;;
reply) reply)
ID="${3:?comment id}" ID="${3:?comment id}"; BODY="${4:?text or @file}"
BODY="${4:?text or @file}"
if [ "${BODY#@}" != "$BODY" ]; then if [ "${BODY#@}" != "$BODY" ]; then
out=$(gh api "repos/$REPO/pulls/$PR/comments/$ID/replies" -F body=@"${BODY#@}" --jq '.id' 2>&1) out=$(gh api "repos/$REPO/pulls/$PR/comments/$ID/replies" -F body=@"${BODY#@}" --jq '.id' 2>&1)
rc=$? rc=$?
@@ -69,7 +61,7 @@ case "$CMD" in
exit 1 exit 1
fi fi
;; ;;
resolve) resolve)
shift 2 shift 2
ids="$*" ids="$*"
if [ "${ids:-}" = "--all" ]; then if [ "${ids:-}" = "--all" ]; then
@@ -77,12 +69,8 @@ case "$CMD" in
echo "answered each one — a resolved-but-wrong thread is worse than an open one." echo "answered each one — a resolved-but-wrong thread is worse than an open one."
gh api graphql -f query="{repository(owner:\"${REPO%%/*}\",name:\"${REPO##*/}\"){pullRequest(number:$PR){reviewThreads(first:100){nodes{isResolved path comments(first:1){nodes{author{login} body}}}}}}}" \ gh api graphql -f query="{repository(owner:\"${REPO%%/*}\",name:\"${REPO##*/}\"){pullRequest(number:$PR){reviewThreads(first:100){nodes{isResolved path comments(first:1){nodes{author{login} body}}}}}}}" \
--jq '.data.repository.pullRequest.reviewThreads.nodes[] | select(.isResolved==false) | " - \(.comments.nodes[0].author.login) \(.path): \(.comments.nodes[0].body[0:90])"' --jq '.data.repository.pullRequest.reviewThreads.nodes[] | select(.isResolved==false) | " - \(.comments.nodes[0].author.login) \(.path): \(.comments.nodes[0].body[0:90])"'
printf 'Type "yes" to resolve all: ' printf 'Type "yes" to resolve all: '; read -r ok
read -r ok [ "$ok" = "yes" ] || { echo "aborted"; exit 1; }
[ "$ok" = "yes" ] || {
echo "aborted"
exit 1
}
ids="" ids=""
elif [ -z "$ids" ]; then elif [ -z "$ids" ]; then
echo "usage: pr_review.sh resolve <PR> <THREAD_ID...> (or --all, with confirmation)" >&2 echo "usage: pr_review.sh resolve <PR> <THREAD_ID...> (or --all, with confirmation)" >&2
@@ -93,10 +81,7 @@ case "$CMD" in
ids=$(gh api graphql -f query="{repository(owner:\"${REPO%%/*}\",name:\"${REPO##*/}\"){pullRequest(number:$PR){reviewThreads(first:50){nodes{id isResolved}}}}}" \ ids=$(gh api graphql -f query="{repository(owner:\"${REPO%%/*}\",name:\"${REPO##*/}\"){pullRequest(number:$PR){reviewThreads(first:50){nodes{id isResolved}}}}}" \
--jq '.data.repository.pullRequest.reviewThreads.nodes[] | select(.isResolved==false) | .id') --jq '.data.repository.pullRequest.reviewThreads.nodes[] | select(.isResolved==false) | .id')
fi fi
[ -z "$ids" ] && { [ -z "$ids" ] && { echo "nothing unresolved"; exit 0; }
echo "nothing unresolved"
exit 0
}
rfail=0 rfail=0
for id in $ids; do for id in $ids; do
r=$(gh api graphql -f query="mutation{resolveReviewThread(input:{threadId:\"$id\"}){thread{isResolved}}}" \ r=$(gh api graphql -f query="mutation{resolveReviewThread(input:{threadId:\"$id\"}){thread{isResolved}}}" \
@@ -107,7 +92,7 @@ case "$CMD" in
# exiting 0 on a failed mutation would let a session believe threads were resolved # exiting 0 on a failed mutation would let a session believe threads were resolved
exit "$rfail" exit "$rfail"
;; ;;
watch) watch)
MINS="${3:-180}" # the addon build alone has taken ~3h; 20 was far too short 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 # 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 # branch protection, and #3019, #3044 and #3050 all merged with Codacy failing. They are kept
@@ -116,9 +101,7 @@ case "$CMD" in
# CI later counts as blocking until someone puts it here on purpose. # 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 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 wfail=2 # not 0: running out of minutes with checks still pending is not a pass
c="" c=""; bstates=""; adv=""
bstates=""
adv=""
for i in $(seq 1 "$MINS"); do for i in $(seq 1 "$MINS"); do
# gh pr checks emits TAB-separated columns with no header when its output is not a TTY, # 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 # which inside this $(... | awk) it never is. (Attached to a terminal it prints a wholly
@@ -138,8 +121,7 @@ case "$CMD" in
# Normal in the first minutes after `gh pr create`, and also whenever gh errors. # 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. # Calling that "settled" would report success for checks that never ran.
echo "[$i] no checks reported yet (gh returned nothing) — still waiting" echo "[$i] no checks reported yet (gh returned nothing) — still waiting"
sleep 60 sleep 60; continue
continue
fi fi
c=$(printf '%s\n' "$rows" | cut -f2 | tr '\n' ' ') c=$(printf '%s\n' "$rows" | cut -f2 | tr '\n' ' ')
echo "[$i] $c" echo "[$i] $c"
@@ -149,14 +131,12 @@ case "$CMD" in
adv=$(printf '%s\n' "$rows" | awk -F'\t' '$1 == "A" { print $2 }' | tr '\n' ' ') adv=$(printf '%s\n' "$rows" | awk -F'\t' '$1 == "A" { print $2 }' | tr '\n' ' ')
if [ -z "$bstates" ]; then if [ -z "$bstates" ]; then
echo " only advisory checks have reported — no blocking check has run yet" echo " only advisory checks have reported — no blocking check has run yet"
sleep 60 sleep 60; continue
continue
fi fi
# Allowlist the good states rather than denylisting the bad ones: an unrecognised state # 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 # must land in the failure branch, because falling through to "passing" is this command's
# worst outcome. # worst outcome.
nbad=0 nbad=0; npend=0
npend=0
for s in $bstates; do for s in $bstates; do
case "$s" in case "$s" in
pass | skipping) ;; pass | skipping) ;;
@@ -166,17 +146,13 @@ case "$CMD" in
done done
if [ "$nbad" -gt 0 ]; then if [ "$nbad" -gt 0 ]; then
echo "settled — blocking checks FAILED:" echo "settled — blocking checks FAILED:"
printf '%s\n' "$rows" \ printf '%s\n' "$rows" |
| awk -F'\t' '$1 == "B" && $3 != "pass" && $3 != "skipping" && $3 != "pending" { print " " $2 }' awk -F'\t' '$1 == "B" && $3 != "pass" && $3 != "skipping" && $3 != "pending" { print " " $2 }'
wfail=1 wfail=1; break
break
elif [ "$npend" -gt 0 ]; then elif [ "$npend" -gt 0 ]; then
sleep 60 sleep 60; continue
continue
else else
echo "settled — blocking checks passing" echo "settled — blocking checks passing"; wfail=0; break
wfail=0
break
fi fi
done done
[ "$wfail" -eq 2 ] && echo "gave up after ${MINS}m, checks still unsettled — NOT a pass" [ "$wfail" -eq 2 ] && echo "gave up after ${MINS}m, checks still unsettled — NOT a pass"
@@ -187,8 +163,5 @@ case "$CMD" in
echo "note: long queues here are usually account runner contention, not your diff." echo "note: long queues here are usually account runner contention, not your diff."
exit "$wfail" exit "$wfail"
;; ;;
*) *) echo "unknown: $CMD"; exit 1 ;;
echo "unknown: $CMD"
exit 1
;;
esac esac

View File

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

View File

@@ -12,10 +12,7 @@
set -uo pipefail set -uo pipefail
TRAPS="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)/references/traps.md" TRAPS="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)/references/traps.md"
[ -f "$TRAPS" ] || { [ -f "$TRAPS" ] || { echo "not found: $TRAPS" >&2; exit 1; }
echo "not found: $TRAPS" >&2
exit 1
}
# The Contents list duplicates the headings; grep the headings themselves so the list cannot drift. # The Contents list duplicates the headings; grep the headings themselves so the list cannot drift.
list() { list() {
@@ -23,16 +20,13 @@ list() {
grep '^## ' "$TRAPS" | grep -v '^## Contents' | sed 's/^## / /' grep '^## ' "$TRAPS" | grep -v '^## Contents' | sed 's/^## / /'
} }
[ $# -eq 0 ] && { [ $# -eq 0 ] && { list; exit 0; }
list
exit 0
}
# awk over exact heading text: section names contain '/' and other characters that would need # awk over exact heading text: section names contain '/' and other characters that would need
# escaping in a sed address, and a keyword matching several headings should be an error, not a # escaping in a sed address, and a keyword matching several headings should be an error, not a
# silent pick of the first. # silent pick of the first.
mapfile -t matches < <(grep '^## ' "$TRAPS" | grep -v '^## Contents' \ mapfile -t matches < <(grep '^## ' "$TRAPS" | grep -v '^## Contents' |
| grep -iF -- "$1" | sed 's/^## //') grep -iF -- "$1" | sed 's/^## //')
case "${#matches[@]}" in case "${#matches[@]}" in
0) 0)

View File

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

View File

@@ -44,7 +44,7 @@ for pr in $PRS; do
echo "$BAD" echo "$BAD"
# Ensure the label exists before adding it — with set -e a missing # 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. # 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 ready "$pr" --repo "$REPO" --undo || true
gh pr edit "$pr" --repo "$REPO" --add-label "ai:blocked" gh pr edit "$pr" --repo "$REPO" --add-label "ai:blocked"
gh pr comment "$pr" --repo "$REPO" --body \ gh pr comment "$pr" --repo "$REPO" --body \

View File

@@ -21,17 +21,14 @@ TITLE=$(jq -r '.title' "$OUT/issue.json")
# ---------------------------------------------------------------- addon slug # ---------------------------------------------------------------- addon slug
# Titles follow "🐛 [Immich Frame] ENV_VARS arent being picked up". # 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="" ADDON=""
if [ -n "$RAW" ]; then if [ -n "$RAW" ]; then
CAND=$(tr '[:upper:] ' '[:lower:]_' <<< "$RAW") CAND=$(tr '[:upper:] ' '[:lower:]_' <<<"$RAW")
# Directory list without checking out any of them. # Directory list without checking out any of them.
git ls-tree -d --name-only HEAD > "$OUT/dirs.txt" git ls-tree -d --name-only HEAD > "$OUT/dirs.txt"
for guess in "$CAND" "${CAND//_/-}" "${CAND//_/.}"; do for guess in "$CAND" "${CAND//_/-}" "${CAND//_/.}"; do
if grep -qxF "$guess" "$OUT/dirs.txt"; then if grep -qxF "$guess" "$OUT/dirs.txt"; then ADDON="$guess"; break; fi
ADDON="$guess"
break
fi
done done
# Separator-insensitive exact match: a title like "[Calibre-web]" (hyphen) # Separator-insensitive exact match: a title like "[Calibre-web]" (hyphen)
# against a directory named calibre_web (underscore) matches neither exact # against a directory named calibre_web (underscore) matches neither exact
@@ -39,12 +36,9 @@ if [ -n "$RAW" ]; then
# below, which picks the shorter "calibre" instead — the wrong add-on. # below, which picks the shorter "calibre" instead — the wrong add-on.
# Stripping -, _, . from both sides before comparing catches this case. # Stripping -, _, . from both sides before comparing catches this case.
if [ -z "$ADDON" ]; then if [ -z "$ADDON" ]; then
CAND_STRIPPED=$(tr -d '_.-' <<< "$CAND") CAND_STRIPPED=$(tr -d '_.-' <<<"$CAND")
while IFS= read -r dir; do while IFS= read -r dir; do
if [ "$(tr -d '_.-' <<< "$dir")" = "$CAND_STRIPPED" ]; then if [ "$(tr -d '_.-' <<<"$dir")" = "$CAND_STRIPPED" ]; then ADDON="$dir"; break; fi
ADDON="$dir"
break
fi
done < "$OUT/dirs.txt" done < "$OUT/dirs.txt"
fi fi
# Last resort: longest directory name contained in the candidate. # Last resort: longest directory name contained in the candidate.
@@ -105,7 +99,7 @@ if [ -n "$ADDON" ]; then
echo echo
echo "## Recent commits touching ${ADDON}/" echo "## Recent commits touching ${ADDON}/"
git log -n 15 --date=short --pretty='- %ad %h %s' -- "$ADDON" 2> /dev/null || true git log -n 15 --date=short --pretty='- %ad %h %s' -- "$ADDON" 2>/dev/null || true
fi fi
} >> "$CTX" } >> "$CTX"
fi fi
@@ -114,14 +108,14 @@ fi
{ {
echo echo
echo "## Similar existing issues (candidate duplicates)" echo "## Similar existing issues (candidate duplicates)"
KEYWORDS=$(tr -cs '[:alnum:]' ' ' <<< "$TITLE" \ KEYWORDS=$(tr -cs '[:alnum:]' ' ' <<<"$TITLE" \
| tr '[:upper:]' '[:lower:]' \ | tr '[:upper:]' '[:lower:]' \
| tr ' ' '\n' | awk 'length($0)>3' | head -n6 | paste -sd' ') | tr ' ' '\n' | awk 'length($0)>3' | head -n6 | paste -sd' ')
# Excludes the issue being triaged: if it's already indexed by GitHub search # 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 # by the time this runs, keyword overlap with its own title would otherwise
# list it as a "candidate duplicate" of itself. # list it as a "candidate duplicate" of itself.
gh search issues --repo "$REPO" --limit 15 \ gh search issues --repo "$REPO" --limit 15 \
--json number,title,state,url -- "$KEYWORDS" 2> /dev/null \ --json number,title,state,url -- "$KEYWORDS" 2>/dev/null \
| jq -r --argjson self "$ISSUE_NUMBER" \ | jq -r --argjson self "$ISSUE_NUMBER" \
'.[] | select(.number != $self) | "- #\(.number) [\(.state)] \(.title)"' \ '.[] | select(.number != $self) | "- #\(.number) [\(.state)] \(.title)"' \
|| echo "(search unavailable)" || echo "(search unavailable)"

View File

@@ -250,6 +250,7 @@ F4bsi,Germany,
Fabaderheld,Austria, Fabaderheld,Austria,
Fabeuss,France, Fabeuss,France,
Fabian-Eigenherd,Germany, Fabian-Eigenherd,Germany,
Fabiot9,,2026-09-13
Faithfinder,Canada, Faithfinder,Canada,
FallsSteven,,2026-08-10 FallsSteven,,2026-08-10
Felitendo,Germany, Felitendo,Germany,
@@ -471,6 +472,7 @@ MNDani,,2026-08-10
MTHarden,United States, MTHarden,United States,
MYLE-01,New Zealand, MYLE-01,New Zealand,
MaBeniu,,2026-08-10 MaBeniu,,2026-08-10
Maboroshi0327,,2026-09-13
MachinaDev,,2026-08-10 MachinaDev,,2026-08-10
Madelena,United States, Madelena,United States,
MagicMau,,2026-08-10 MagicMau,,2026-08-10
@@ -515,6 +517,7 @@ Mofx-01,,2026-08-10
Mokdeze,,2026-08-10 Mokdeze,,2026-08-10
MondoGao,United States, MondoGao,United States,
Moosh-be,Belgium, Moosh-be,Belgium,
Moozzuzz,,2026-09-13
MrAct1on,United States, MrAct1on,United States,
MrCraigen,,2026-08-10 MrCraigen,,2026-08-10
MrHacky,,2026-08-10 MrHacky,,2026-08-10
@@ -952,6 +955,7 @@ austinmccalley,,2026-08-10
autodeck,United Kingdom, autodeck,United Kingdom,
aviadlevy,,2026-08-10 aviadlevy,,2026-08-10
aviv926,,2026-08-10 aviv926,,2026-08-10
axelburks,United States,2026-09-13
axute,Germany, axute,Germany,
azerioxal,United States, azerioxal,United States,
azure1922,,2026-08-10 azure1922,,2026-08-10
@@ -1043,6 +1047,7 @@ brettmiller,United States,
brewston,,2026-08-10 brewston,,2026-08-10
brianalexander,,2026-08-10 brianalexander,,2026-08-10
brianm71,,2026-08-10 brianm71,,2026-08-10
brietman,,2026-09-13
brittek,Australia, brittek,Australia,
brokec,,2026-08-10 brokec,,2026-08-10
brunerm99,,2026-08-10 brunerm99,,2026-08-10
@@ -1451,6 +1456,7 @@ gh-residante,,2026-08-10
ghisch,,2026-08-10 ghisch,,2026-08-10
gieljnssns,Belgium, gieljnssns,Belgium,
gigatexel,,2026-08-10 gigatexel,,2026-08-10
gilberth,Peru,2026-09-13
gileswells,United States, gileswells,United States,
giotec,,2026-08-10 giotec,,2026-08-10
git-mdumont,, git-mdumont,,
@@ -1771,6 +1777,7 @@ ldec,,2026-08-10
ldkdev,Viet Nam, ldkdev,Viet Nam,
le-pet,,2026-08-10 le-pet,,2026-08-10
lead0r,,2026-08-10 lead0r,,2026-08-10
leaskovski,,2026-09-13
legovaer,Belgium, legovaer,Belgium,
legusha,,2026-08-10 legusha,,2026-08-10
lehnerpat,,2026-08-10 lehnerpat,,2026-08-10
@@ -2001,6 +2008,8 @@ najdias,Portugal,
nalditopr,,2026-08-10 nalditopr,,2026-08-10
nana4rider,Japan, nana4rider,Japan,
naor1,Israel, naor1,Israel,
narmod,,2026-09-13
nascentt,,2026-09-13
natbrood,,2026-08-10 natbrood,,2026-08-10
nate-marshall,,2026-08-10 nate-marshall,,2026-08-10
nduest,Germany, nduest,Germany,
@@ -2583,6 +2592,7 @@ w10devheh,,2026-08-10
waako,,2026-08-10 waako,,2026-08-10
walduino,,2026-08-10 walduino,,2026-08-10
wangjiezhe,China, wangjiezhe,China,
wannes-wagemans,Belgium,2026-09-13
warbringer,,2026-08-10 warbringer,,2026-08-10
wardbeyens,Belgium, wardbeyens,Belgium,
warkurre86,,2026-08-10 warkurre86,,2026-08-10
1 username country last_checked
250 Fabaderheld Austria
251 Fabeuss France
252 Fabian-Eigenherd Germany
253 Fabiot9 2026-09-13
254 Faithfinder Canada
255 FallsSteven 2026-08-10
256 Felitendo Germany
472 MTHarden United States
473 MYLE-01 New Zealand
474 MaBeniu 2026-08-10
475 Maboroshi0327 2026-09-13
476 MachinaDev 2026-08-10
477 Madelena United States
478 MagicMau 2026-08-10
517 Mokdeze 2026-08-10
518 MondoGao United States
519 Moosh-be Belgium
520 Moozzuzz 2026-09-13
521 MrAct1on United States
522 MrCraigen 2026-08-10
523 MrHacky 2026-08-10
955 autodeck United Kingdom
956 aviadlevy 2026-08-10
957 aviv926 2026-08-10
958 axelburks United States 2026-09-13
959 axute Germany
960 azerioxal United States
961 azure1922 2026-08-10
1047 brewston 2026-08-10
1048 brianalexander 2026-08-10
1049 brianm71 2026-08-10
1050 brietman 2026-09-13
1051 brittek Australia
1052 brokec 2026-08-10
1053 brunerm99 2026-08-10
1456 ghisch 2026-08-10
1457 gieljnssns Belgium
1458 gigatexel 2026-08-10
1459 gilberth Peru 2026-09-13
1460 gileswells United States
1461 giotec 2026-08-10
1462 git-mdumont
1777 ldkdev Viet Nam
1778 le-pet 2026-08-10
1779 lead0r 2026-08-10
1780 leaskovski 2026-09-13
1781 legovaer Belgium
1782 legusha 2026-08-10
1783 lehnerpat 2026-08-10
2008 nalditopr 2026-08-10
2009 nana4rider Japan
2010 naor1 Israel
2011 narmod 2026-09-13
2012 nascentt 2026-09-13
2013 natbrood 2026-08-10
2014 nate-marshall 2026-08-10
2015 nduest Germany
2592 waako 2026-08-10
2593 walduino 2026-08-10
2594 wangjiezhe China
2595 wannes-wagemans Belgium 2026-09-13
2596 warbringer 2026-08-10
2597 wardbeyens Belgium
2598 warkurre86 2026-08-10

Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

After

Width:  |  Height:  |  Size: 67 KiB

BIN
.github/stats.png vendored

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 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

@@ -125,7 +125,7 @@ jobs:
- name: Analyse and fix - name: Analyse and fix
if: steps.batch.outputs.count != '0' if: steps.batch.outputs.count != '0'
uses: anthropics/claude-code-action@d75b94d5ad426cb8546e6628b6f5f19b84e5cce1 # v1 uses: anthropics/claude-code-action@56cf60fde42f7b19c3abfd5c9c48b69a1288461f # v1
with: with:
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
# Skip the OIDC -> Claude App token exchange. The scheduled path # Skip the OIDC -> Claude App token exchange. The scheduled path

View File

@@ -7,8 +7,6 @@ on:
- "**/config.*" - "**/config.*"
pull_request: pull_request:
branches: [ master ] branches: [ master ]
schedule:
- cron: "0 0 * * 0"
workflow_dispatch: workflow_dispatch:
permissions: permissions:

View File

@@ -64,7 +64,7 @@ jobs:
fetch-depth: 1 fetch-depth: 1
- name: Run Claude Code - name: Run Claude Code
uses: anthropics/claude-code-action@d75b94d5ad426cb8546e6628b6f5f19b84e5cce1 # v1 uses: anthropics/claude-code-action@56cf60fde42f7b19c3abfd5c9c48b69a1288461f # v1
with: with:
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
# AI_PR_TOKEN, not GITHUB_TOKEN, so a PR Claude opens triggers CI. # AI_PR_TOKEN, not GITHUB_TOKEN, so a PR Claude opens triggers CI.

View File

@@ -135,7 +135,7 @@ jobs:
- name: Execute the plan - name: Execute the plan
if: steps.bundle.outputs.has_plan == 'true' if: steps.bundle.outputs.has_plan == 'true'
uses: anthropics/claude-code-action@d75b94d5ad426cb8546e6628b6f5f19b84e5cce1 # v1 uses: anthropics/claude-code-action@56cf60fde42f7b19c3abfd5c9c48b69a1288461f # v1
with: with:
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
# Skip the OIDC -> Claude App token exchange, which 401s whenever # Skip the OIDC -> Claude App token exchange, which 401s whenever

View File

@@ -166,7 +166,7 @@ jobs:
id: classify id: classify
if: github.event_name != 'issue_comment' || steps.claim.outputs.go == 'true' if: github.event_name != 'issue_comment' || steps.claim.outputs.go == 'true'
continue-on-error: true continue-on-error: true
uses: anthropics/claude-code-action@d75b94d5ad426cb8546e6628b6f5f19b84e5cce1 # v1 uses: anthropics/claude-code-action@56cf60fde42f7b19c3abfd5c9c48b69a1288461f # v1
with: with:
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
# Without this the action falls back to the OIDC -> Claude App token # Without this the action falls back to the OIDC -> Claude App token

View File

@@ -79,7 +79,7 @@ jobs:
- name: Address CodeRabbit comments - name: Address CodeRabbit comments
if: steps.claim.outputs.go == 'true' if: steps.claim.outputs.go == 'true'
uses: anthropics/claude-code-action@d75b94d5ad426cb8546e6628b6f5f19b84e5cce1 # v1 uses: anthropics/claude-code-action@56cf60fde42f7b19c3abfd5c9c48b69a1288461f # v1
with: with:
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
# Skip the OIDC -> Claude App token exchange, which 401s whenever # Skip the OIDC -> Claude App token exchange, which 401s whenever

View File

@@ -306,7 +306,7 @@ jobs:
id: build id: build
if: steps.info.outputs.build_arch == 'true' && steps.info.outputs.has_dockerfile == 'true' if: steps.info.outputs.build_arch == 'true' && steps.info.outputs.has_dockerfile == 'true'
continue-on-error: true continue-on-error: true
uses: home-assistant/builder/actions/build-image@2026.06.0 uses: home-assistant/builder/actions/build-image@2026.09.0
with: with:
arch: ${{ matrix.arch }} arch: ${{ matrix.arch }}
cache-gha: "false" cache-gha: "false"
@@ -331,7 +331,7 @@ jobs:
# else for the retry to be skipped from. # else for the retry to be skipped from.
- name: Build ${{ matrix.addon }} add-on (retry after failed attempt) - name: Build ${{ matrix.addon }} add-on (retry after failed attempt)
if: steps.build.outcome == 'failure' if: steps.build.outcome == 'failure'
uses: home-assistant/builder/actions/build-image@2026.06.0 uses: home-assistant/builder/actions/build-image@2026.09.0
with: with:
arch: ${{ matrix.arch }} arch: ${{ matrix.arch }}
cache-gha: "false" cache-gha: "false"

View File

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

View File

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

View File

@@ -4,7 +4,7 @@
set -e set -e
if ! bashio::supervisor.ping 2> /dev/null; then if ! bashio::supervisor.ping 2>/dev/null; then
bashio::log.blue "Disabled : please use another method" bashio::log.blue "Disabled : please use another method"
exit 0 exit 0
fi fi
@@ -33,16 +33,16 @@ test_mount() {
_test_write() { _test_write() {
local testfile="$mountpoint/.writetest_$$" local testfile="$mountpoint/.writetest_$$"
if : > "$testfile" 2> /dev/null; then if : >"$testfile" 2>/dev/null; then
rm -f "$testfile" 2> /dev/null || true rm -f "$testfile" 2>/dev/null || true
return 0 return 0
else else
rm -f "$testfile" 2> /dev/null || true rm -f "$testfile" 2>/dev/null || true
return 1 return 1
fi fi
} }
if _test_write 2> /dev/null; then if _test_write 2>/dev/null; then
MOUNTED=true MOUNTED=true
return 0 return 0
fi fi
@@ -53,9 +53,9 @@ test_mount() {
echo "... retrying mount with noserverino" echo "... retrying mount with noserverino"
MOUNTOPTIONS="${orig_mountoptions},noserverino" MOUNTOPTIONS="${orig_mountoptions},noserverino"
umount "$mountpoint" 2> /dev/null || true umount "$mountpoint" 2>/dev/null || true
if mount_drive "$MOUNTOPTIONS"; then if mount_drive "$MOUNTOPTIONS"; then
if _test_write 2> /dev/null; then if _test_write 2>/dev/null; then
MOUNTED=true MOUNTED=true
return 0 return 0
fi fi
@@ -66,9 +66,9 @@ test_mount() {
echo "... retrying mount with noperm" echo "... retrying mount with noperm"
MOUNTOPTIONS="${orig_mountoptions},noperm" MOUNTOPTIONS="${orig_mountoptions},noperm"
umount "$mountpoint" 2> /dev/null || true umount "$mountpoint" 2>/dev/null || true
if mount_drive "$MOUNTOPTIONS"; then if mount_drive "$MOUNTOPTIONS"; then
if _test_write 2> /dev/null; then if _test_write 2>/dev/null; then
MOUNTED=true MOUNTED=true
return 0 return 0
fi fi
@@ -88,9 +88,9 @@ mount_drive() {
MOUNTOPTIONS="$1" MOUNTOPTIONS="$1"
if [[ "$FSTYPE" == "cifs" ]]; then if [[ "$FSTYPE" == "cifs" ]]; then
mount -t cifs -o "$MOUNTOPTIONS" "$disk" "/mnt/$diskname" 2> "$ERRORCODE_FILE" || MOUNTED=false mount -t cifs -o "$MOUNTOPTIONS" "$disk" "/mnt/$diskname" 2>"$ERRORCODE_FILE" || MOUNTED=false
elif [[ "$FSTYPE" == "nfs" ]]; then elif [[ "$FSTYPE" == "nfs" ]]; then
mount -t nfs -o "$MOUNTOPTIONS" "$disk" "/mnt/$diskname" 2> "$ERRORCODE_FILE" || MOUNTED=false mount -t nfs -o "$MOUNTOPTIONS" "$disk" "/mnt/$diskname" 2>"$ERRORCODE_FILE" || MOUNTED=false
fi fi
if [[ "$MOUNTED" == "true" ]]; then if [[ "$MOUNTED" == "true" ]]; then
@@ -106,7 +106,7 @@ retry_cifs_with_vers_ladder_on_dialect_failure() {
local err mountpoint local err mountpoint
mountpoint="/mnt/$diskname" mountpoint="/mnt/$diskname"
err="$(cat "$ERRORCODE_FILE" 2> /dev/null || true)" 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 if echo "$err" | grep -Eq 'mount error\(13\)|Permission denied|NT_STATUS_(LOGON_FAILURE|ACCESS_DENIED)|STATUS_(LOGON_FAILURE|ACCESS_DENIED)'; then
return 0 return 0
@@ -132,7 +132,7 @@ retry_cifs_with_vers_ladder_on_dialect_failure() {
for vers in "${vers_variants[@]}"; do for vers in "${vers_variants[@]}"; do
for sectry in "${sec_variants[@]}"; do for sectry in "${sec_variants[@]}"; do
[[ "$MOUNTED" == "false" ]] || break [[ "$MOUNTED" == "false" ]] || break
umount "$mountpoint" 2> /dev/null || true umount "$mountpoint" 2>/dev/null || true
try_opts="${base_opts}${vopt},vers=${vers}${sectry}" try_opts="${base_opts}${vopt},vers=${vers}${sectry}"
mount_drive "$try_opts" mount_drive "$try_opts"
done done
@@ -154,7 +154,7 @@ retry_cifs_with_vers_ladder_on_dialect_failure() {
for vers in "${vers_variants2[@]}"; do for vers in "${vers_variants2[@]}"; do
for sectry in "${sec_variants[@]}"; do for sectry in "${sec_variants[@]}"; do
[[ "$MOUNTED" == "false" ]] || break [[ "$MOUNTED" == "false" ]] || break
umount "$mountpoint" 2> /dev/null || true umount "$mountpoint" 2>/dev/null || true
try_opts="${base_opts}${vopt},vers=${vers}${sectry}" try_opts="${base_opts}${vopt},vers=${vers}${sectry}"
mount_drive "$try_opts" mount_drive "$try_opts"
done done
@@ -167,7 +167,7 @@ retry_cifs_with_vers_ladder_on_dialect_failure() {
for vopt in "${opt_variants[@]}"; do for vopt in "${opt_variants[@]}"; do
for vers in "${vers_variants2[@]}"; do for vers in "${vers_variants2[@]}"; do
[[ "$MOUNTED" == "false" ]] || break [[ "$MOUNTED" == "false" ]] || break
umount "$mountpoint" 2> /dev/null || true umount "$mountpoint" 2>/dev/null || true
try_opts="${base_opts}${vopt},vers=${vers},sec=ntlmssp" try_opts="${base_opts}${vopt},vers=${vers},sec=ntlmssp"
mount_drive "$try_opts" mount_drive "$try_opts"
done done
@@ -246,13 +246,13 @@ if bashio::config.has_value 'networkdisks'; then
diskname="${diskname//\\//}" diskname="${diskname//\\//}"
diskname="${diskname##*/}" diskname="${diskname##*/}"
if mountpoint -q "/mnt/$diskname" 2> /dev/null; then 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." bashio::log.warning "...... mount point /mnt/$diskname already in use (name collision for $disk). Skipping this share."
continue continue
fi fi
ERRORCODE_FILE="/tmp/mount_error_${diskname//[^a-zA-Z0-9._-]/_}.log" ERRORCODE_FILE="/tmp/mount_error_${diskname//[^a-zA-Z0-9._-]/_}.log"
: > "$ERRORCODE_FILE" || true : >"$ERRORCODE_FILE" || true
MOUNTED=false MOUNTED=false
CIFS_LADDER_ATTEMPTED=false CIFS_LADDER_ATTEMPTED=false
@@ -266,13 +266,13 @@ if bashio::config.has_value 'networkdisks'; then
if [[ "$FSTYPE" == "cifs" ]]; then if [[ "$FSTYPE" == "cifs" ]]; then
if [[ ! "$disk" =~ ^//[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+/.+ ]]; 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" 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 echo "Invalid CIFS path structure: $disk" >"$ERRORCODE_FILE" || true
continue continue
fi fi
else else
if [[ ! "$disk" =~ ^[^/:]+:/.+ ]]; then if [[ ! "$disk" =~ ^[^/:]+:/.+ ]]; then
bashio::log.fatal "...... invalid NFS path \"$disk\". Use server:/export/path or 123.12.12.12:/export/path" 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 echo "Invalid NFS path structure: $disk" >"$ERRORCODE_FILE" || true
continue continue
fi fi
fi fi
@@ -289,7 +289,7 @@ if bashio::config.has_value 'networkdisks'; then
if [[ -n "${CIFSDOMAIN:-}" ]]; then if [[ -n "${CIFSDOMAIN:-}" ]]; then
printf 'domain=%s\n' "$CIFSDOMAIN" printf 'domain=%s\n' "$CIFSDOMAIN"
fi fi
} > "$CRED_FILE" } >"$CRED_FILE"
fi fi
if [[ "$FSTYPE" == "cifs" ]]; then if [[ "$FSTYPE" == "cifs" ]]; then
@@ -303,8 +303,8 @@ if bashio::config.has_value 'networkdisks'; then
if [[ "$MOUNTED" == "false" ]]; then if [[ "$MOUNTED" == "false" ]]; then
if [[ "$FSTYPE" == "cifs" ]]; then if [[ "$FSTYPE" == "cifs" ]]; then
if command -v nmap > /dev/null 2>&1; then if command -v nmap >/dev/null 2>&1; then
output="$(nmap -F "$server" -T5 -oG - 2> /dev/null || true)" output="$(nmap -F "$server" -T5 -oG - 2>/dev/null || true)"
if ! echo "$output" | grep -q "445/open"; then if ! echo "$output" | grep -q "445/open"; then
if echo "$output" | grep -q "/open"; then if echo "$output" | grep -q "/open"; then
bashio::log.fatal "...... $server is reachable but SMB port not opened, stopping script" bashio::log.fatal "...... $server is reachable but SMB port not opened, stopping script"
@@ -312,7 +312,7 @@ if bashio::config.has_value 'networkdisks'; then
bashio::log.fatal "...... fatal : $server not reachable, is it correct" bashio::log.fatal "...... fatal : $server not reachable, is it correct"
fi fi
cleanup_cred cleanup_cred
rm -f "$ERRORCODE_FILE" 2> /dev/null || true rm -f "$ERRORCODE_FILE" 2>/dev/null || true
continue continue
fi fi
echo "...... $server is confirmed reachable" echo "...... $server is confirmed reachable"
@@ -320,13 +320,13 @@ if bashio::config.has_value 'networkdisks'; then
bashio::log.warning "...... nmap not available; skipping SMB port reachability test" bashio::log.warning "...... nmap not available; skipping SMB port reachability test"
fi fi
if command -v smbclient > /dev/null 2>&1; then if command -v smbclient >/dev/null 2>&1; then
OUTPUT="$(smbclient -t 2 -L "$server" -U "$USERNAME%$PASSWORD" -c "exit" $DOMAINCLIENT 2>&1 || true)" OUTPUT="$(smbclient -t 2 -L "$server" -U "$USERNAME%$PASSWORD" -c "exit" $DOMAINCLIENT 2>&1 || true)"
if echo "$OUTPUT" | grep -q "LOGON_FAILURE"; then if echo "$OUTPUT" | grep -q "LOGON_FAILURE"; then
bashio::log.fatal "...... incorrect Username, Password, or Domain! Script will stop." bashio::log.fatal "...... incorrect Username, Password, or Domain! Script will stop."
cleanup_cred cleanup_cred
rm -f "$ERRORCODE_FILE" 2> /dev/null || true rm -f "$ERRORCODE_FILE" 2>/dev/null || true
bashio::addon.stop bashio::addon.stop
elif echo "$OUTPUT" | grep -q "tree connect failed" || echo "$OUTPUT" | grep -q "NT_STATUS_CONNECTION_DISCONNECTED"; then elif echo "$OUTPUT" | grep -q "tree connect failed" || echo "$OUTPUT" | grep -q "NT_STATUS_CONNECTION_DISCONNECTED"; then
echo "... using SMBv1" echo "... using SMBv1"
@@ -344,8 +344,8 @@ if bashio::config.has_value 'networkdisks'; then
SMBRAW="" SMBRAW=""
SMB1_DETECTED=false SMB1_DETECTED=false
if command -v nmap > /dev/null 2>&1; then if command -v nmap >/dev/null 2>&1; then
NMAP_OUTPUT="$(nmap --script smb-protocols -p 445 "$server" 2> /dev/null || true)" NMAP_OUTPUT="$(nmap --script smb-protocols -p 445 "$server" 2>/dev/null || true)"
SMBRAW="$( SMBRAW="$(
echo "$NMAP_OUTPUT" \ echo "$NMAP_OUTPUT" \
| awk '/SMB2_DIALECT_/ {print $NF}' \ | awk '/SMB2_DIALECT_/ {print $NF}' \
@@ -364,7 +364,7 @@ if bashio::config.has_value 'networkdisks'; then
302) SMBVERS=",vers=3.02" ;; 302) SMBVERS=",vers=3.02" ;;
300) SMBVERS=",vers=3.0" ;; 300) SMBVERS=",vers=3.0" ;;
210) SMBVERS=",vers=2.1" ;; 210) SMBVERS=",vers=2.1" ;;
202 | 200) SMBVERS=",vers=2.0" ;; 202|200) SMBVERS=",vers=2.0" ;;
*) SMBVERS="" ;; *) SMBVERS="" ;;
esac esac
@@ -376,7 +376,7 @@ if bashio::config.has_value 'networkdisks'; then
if [[ -n "$SMBVERS" ]]; then if [[ -n "$SMBVERS" ]]; then
echo "...... SMB version detected : ${SMBVERS#,vers=}" 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 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" echo "...... SMB version : only SMBv1 is supported, this can lead to issues"
SECVERS=",sec=ntlm" SECVERS=",sec=ntlm"
SMBVERS=",vers=1.0" SMBVERS=",vers=1.0"
@@ -405,8 +405,8 @@ if bashio::config.has_value 'networkdisks'; then
fi fi
else else
if command -v nmap > /dev/null 2>&1; then if command -v nmap >/dev/null 2>&1; then
output="$(nmap -F "$server" -T5 -oG - 2> /dev/null || true)" output="$(nmap -F "$server" -T5 -oG - 2>/dev/null || true)"
if ! echo "$output" | grep -Eq '(2049|111)/open'; then if ! echo "$output" | grep -Eq '(2049|111)/open'; then
bashio::log.fatal "...... $server is reachable but NFS ports not open" bashio::log.fatal "...... $server is reachable but NFS ports not open"
fi fi
@@ -424,7 +424,7 @@ if bashio::config.has_value 'networkdisks'; then
if [[ "$MOUNTED" == "true" ]]; then if [[ "$MOUNTED" == "true" ]]; then
bashio::log.info "...... $disk successfully mounted to /mnt/$diskname with options ${MOUNTOPTIONS/$PASSWORD/XXXXXXXXXX}" bashio::log.info "...... $disk successfully mounted to /mnt/$diskname with options ${MOUNTOPTIONS/$PASSWORD/XXXXXXXXXX}"
rm -f "$ERRORCODE_FILE" 2> /dev/null || true rm -f "$ERRORCODE_FILE" 2>/dev/null || true
if [[ "$FSTYPE" == "cifs" && "$MOUNTOPTIONS" == *"vers=1.0"* ]]; then if [[ "$FSTYPE" == "cifs" && "$MOUNTOPTIONS" == *"vers=1.0"* ]]; then
bashio::log.warning "" bashio::log.warning ""
@@ -437,14 +437,14 @@ if bashio::config.has_value 'networkdisks'; then
elif [[ "$MOUNTED" == "readonly" ]]; then 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 "...... $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." 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 rm -f "$ERRORCODE_FILE" 2>/dev/null || true
cleanup_cred cleanup_cred
else else
if [[ "$FSTYPE" == "cifs" ]]; then 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 "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 :" bashio::log.fatal "Here is some debugging info :"
if command -v smbclient > /dev/null 2>&1; then if command -v smbclient >/dev/null 2>&1; then
smbclient -t 2 -L "$server" -U "$USERNAME%$PASSWORD" -c "exit" $DOMAINCLIENT || true smbclient -t 2 -L "$server" -U "$USERNAME%$PASSWORD" -c "exit" $DOMAINCLIENT || true
else else
bashio::log.warning "smbclient not available; cannot print SMB debugging info" bashio::log.warning "smbclient not available; cannot print SMB debugging info"
@@ -459,13 +459,13 @@ if bashio::config.has_value 'networkdisks'; then
mount_drive "rw" mount_drive "rw"
fi fi
ERR_READ="$(cat "$ERRORCODE_FILE" 2> /dev/null || true)" ERR_READ="$(cat "$ERRORCODE_FILE" 2>/dev/null || true)"
bashio::log.fatal "Error read : ${ERR_READ:-unknown error}, addon will stop in 1 min" bashio::log.fatal "Error read : ${ERR_READ:-unknown error}, addon will stop in 1 min"
umount "/mnt/$diskname" 2> /dev/null || true umount "/mnt/$diskname" 2>/dev/null || true
rmdir "/mnt/$diskname" 2> /dev/null || true rmdir "/mnt/$diskname" 2>/dev/null || true
cleanup_cred cleanup_cred
rm -f "$ERRORCODE_FILE" 2> /dev/null || true rm -f "$ERRORCODE_FILE" 2>/dev/null || true
bashio::addon.stop bashio::addon.stop
fi fi

View File

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

View File

@@ -11,20 +11,14 @@ pick_exec_dir() {
for d in /dev/shm /run /var/run /mnt /root /; do for d in /dev/shm /run /var/run /mnt /root /; do
if [ -d "$d" ] && [ -w "$d" ]; then if [ -d "$d" ] && [ -w "$d" ]; then
local t="${d%/}/.exec_test_$$" local t="${d%/}/.exec_test_$$"
printf '#!/bin/sh\necho ok\n' > "$t" 2> /dev/null || { printf '#!/bin/sh\necho ok\n' >"$t" 2>/dev/null || { rm -f "$t" 2>/dev/null || true; continue; }
rm -f "$t" 2> /dev/null || true chmod 700 "$t" 2>/dev/null || { rm -f "$t" 2>/dev/null || true; continue; }
continue if "$t" >/dev/null 2>&1; then
} rm -f "$t" 2>/dev/null || true
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" echo "$d"
return 0 return 0
fi fi
rm -f "$t" 2> /dev/null || true rm -f "$t" 2>/dev/null || true
fi fi
done done
return 1 return 1
@@ -96,7 +90,7 @@ validate_shebang() {
local errfile msg local errfile msg
# shellcheck disable=SC2206 # shellcheck disable=SC2206
local cmd=($candidate) local cmd=( $candidate )
local exe="${cmd[0]}" local exe="${cmd[0]}"
if [ ! -x "$exe" ]; then if [ ! -x "$exe" ]; then
@@ -109,18 +103,18 @@ validate_shebang() {
{ {
printf '#!%s\n' "$candidate" printf '#!%s\n' "$candidate"
printf '%s\n' "$probe_script_content" printf '%s\n' "$probe_script_content"
} > "$tmp" } >"$tmp"
chmod 700 "$tmp" 2> /dev/null || true chmod 700 "$tmp" 2>/dev/null || true
set +e set +e
out="$("$tmp" 2> "$errfile")" out="$("$tmp" 2>"$errfile")"
rc=$? rc=$?
set -e set -e
rm -f "$tmp" 2> /dev/null || true rm -f "$tmp" 2>/dev/null || true
if [ "$rc" -eq 0 ] && [ -n "${out:-}" ] && [ "$out" != "null" ]; then if [ "$rc" -eq 0 ] && [ -n "${out:-}" ] && [ "$out" != "null" ]; then
rm -f "$errfile" 2> /dev/null || true rm -f "$errfile" 2>/dev/null || true
return 0 return 0
fi fi
@@ -132,7 +126,7 @@ validate_shebang() {
msg+=$' stderr: <empty>\n' msg+=$' stderr: <empty>\n'
fi fi
SHEBANG_ERRORS+=("$msg") SHEBANG_ERRORS+=("$msg")
rm -f "$errfile" 2> /dev/null || true rm -f "$errfile" 2>/dev/null || true
return 1 return 1
} }
@@ -153,7 +147,7 @@ if [ -z "$shebang" ]; then
exit 1 exit 1
fi fi
if ! command -v bashio::addon.version > /dev/null 2>&1; then 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 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 if [ -f "$f" ]; then
# shellcheck disable=SC1090 # shellcheck disable=SC1090
@@ -168,7 +162,7 @@ fi
################## ##################
# Exit if /config is not mounted or HA not used # Exit if /config is not mounted or HA not used
if [ ! -d /config ] || ! bashio::supervisor.ping 2> /dev/null; then if [ ! -d /config ] || ! bashio::supervisor.ping 2>/dev/null; then
echo "..." echo "..."
exit 0 exit 0
fi fi
@@ -201,7 +195,7 @@ if [ ! -f "$CONFIGSOURCE" ]; then
fi fi
# Convert scripts to linux # Convert scripts to linux
dos2unix "$CONFIGSOURCE" > /dev/null 2>&1 || true dos2unix "$CONFIGSOURCE" >/dev/null 2>&1 || true
chmod +x "$CONFIGSOURCE" chmod +x "$CONFIGSOURCE"
if grep -q 'bashio' "$CONFIGSOURCE"; then if grep -q 'bashio' "$CONFIGSOURCE"; then

View File

@@ -46,8 +46,7 @@ _bashio_color() {
} }
_bashio_log() { _bashio_log() {
local c="${1:-}" local c="${1:-}"; shift || true
shift || true
printf '%s%s%s\n' "$(_bashio_color "$c")" "$*" "$(_bashio_color reset)" printf '%s%s%s\n' "$(_bashio_color "$c")" "$*" "$(_bashio_color reset)"
} }
@@ -56,7 +55,7 @@ _bashio_log() {
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
_bashio_is_true() { _bashio_is_true() {
case "${1:-}" in case "${1:-}" in
1 | true | TRUE | True | yes | YES | Yes | on | ON | On) return 0 ;; 1|true|TRUE|True|yes|YES|Yes|on|ON|On) return 0 ;;
*) return 1 ;; *) return 1 ;;
esac esac
} }
@@ -100,7 +99,7 @@ _bashio_env_get() {
_bashio_env_has() { _bashio_env_has() {
local key="${1:-}" local key="${1:-}"
[ -n "$key" ] || return 1 [ -n "$key" ] || return 1
_bashio_env_get "$key" > /dev/null 2>&1 _bashio_env_get "$key" >/dev/null 2>&1
} }
# JSON options source (jq required). Prints value or empty; returns 0 always. # JSON options source (jq required). Prints value or empty; returns 0 always.
@@ -111,10 +110,10 @@ _bashio_json_get() {
[ -n "$key" ] || return 0 [ -n "$key" ] || return 0
[ -n "$file" ] || return 0 [ -n "$file" ] || return 0
[ -f "$file" ] || return 0 [ -f "$file" ] || return 0
command -v jq > /dev/null 2>&1 || return 0 command -v jq >/dev/null 2>&1 || return 0
# getpath(split(".")) supports nested access; missing => empty # getpath(split(".")) supports nested access; missing => empty
jq -er --arg k "$key" 'getpath(($k|split("."))) // empty' "$file" 2> /dev/null || true jq -er --arg k "$key" 'getpath(($k|split("."))) // empty' "$file" 2>/dev/null || true
} }
# Net wait using /dev/tcp with a timeout # Net wait using /dev/tcp with a timeout
@@ -125,7 +124,7 @@ _bashio_tcp_wait() {
local start now local start now
start="$(date +%s)" start="$(date +%s)"
while :; do while :; do
if exec 3<> "/dev/tcp/${host}/${port}" 2> /dev/null; then if exec 3<>"/dev/tcp/${host}/${port}" 2>/dev/null; then
exec 3>&- 3<&- exec 3>&- 3<&-
return 0 return 0
fi fi
@@ -139,10 +138,10 @@ _bashio_tcp_wait() {
# Prefer nc if present, fallback to /dev/tcp # Prefer nc if present, fallback to /dev/tcp
_bashio_tcp_wait_nc() { _bashio_tcp_wait_nc() {
command -v nc > /dev/null 2>&1 || return 1 command -v nc >/dev/null 2>&1 || return 1
local host="${1:-}" port="${2:-}" to="${3:-30}" local host="${1:-}" port="${2:-}" to="${3:-30}"
# BusyBox and OpenBSD nc differ; cover both styles # BusyBox and OpenBSD nc differ; cover both styles
nc -z -w "$to" "$host" "$port" 2> /dev/null || nc -z "$host" "$port" 2> /dev/null nc -z -w "$to" "$host" "$port" 2>/dev/null || nc -z "$host" "$port" 2>/dev/null
} }
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
@@ -174,7 +173,7 @@ bashio::supervisor.ping() {
bashio::addon.name() { printf '%s' "${ADDON_NAME:-Standalone container}"; } bashio::addon.name() { printf '%s' "${ADDON_NAME:-Standalone container}"; }
bashio::addon.description() { printf '%s' "${ADDON_DESCRIPTION:-Running without Home Assistant Supervisor}"; } bashio::addon.description() { printf '%s' "${ADDON_DESCRIPTION:-Running without Home Assistant Supervisor}"; }
bashio::addon.version() { printf '%s' "${BUILD_VERSION:-1.0}"; } bashio::addon.version() { printf '%s' "${BUILD_VERSION:-1.0}"; }
bashio::addon.version_latest() { printf '%s' "${ADDON_VERSION_LATEST:-${BUILD_VERSION:-1.0}}"; } bashio::addon.version_latest(){ printf '%s' "${ADDON_VERSION_LATEST:-${BUILD_VERSION:-1.0}}"; }
bashio::addon.update_available() { bashio::addon.update_available() {
if [ -n "${ADDON_VERSION_LATEST:-}" ] && [ "${ADDON_VERSION_LATEST:-}" != "${BUILD_VERSION:-}" ]; then if [ -n "${ADDON_VERSION_LATEST:-}" ] && [ "${ADDON_VERSION_LATEST:-}" != "${BUILD_VERSION:-}" ]; then
@@ -195,11 +194,11 @@ bashio::addon.port() {
local arg="${1:-}" local arg="${1:-}"
if [[ "$arg" =~ ^[0-9]+$ ]]; then if [[ "$arg" =~ ^[0-9]+$ ]]; then
local v="" local v=""
v="$(_bashio_env_get "PORT_${arg}" 2> /dev/null || true)" v="$(_bashio_env_get "PORT_${arg}" 2>/dev/null || true)"
[ -z "$v" ] && v="$(_bashio_env_get "ADDON_PORT_${arg}" 2> /dev/null || true)" [ -z "$v" ] && v="$(_bashio_env_get "ADDON_PORT_${arg}" 2>/dev/null || true)"
printf '%s' "${v:-$arg}" printf '%s' "${v:-$arg}"
else else
printf '%s' "$(_bashio_env_get "$arg" 2> /dev/null || true)" printf '%s' "$(_bashio_env_get "$arg" 2>/dev/null || true)"
fi fi
} }
@@ -208,13 +207,13 @@ bashio::addon.option() {
local key="${1:-}" value="${2-__BASHIO_UNSET__}" file="${STANDALONE_OPTIONS_JSON:-}" local key="${1:-}" value="${2-__BASHIO_UNSET__}" file="${STANDALONE_OPTIONS_JSON:-}"
[ -n "$key" ] || return 0 [ -n "$key" ] || return 0
if [ -n "$file" ] && [ -f "$file" ] && command -v jq > /dev/null 2>&1; then if [ -n "$file" ] && [ -f "$file" ] && command -v jq >/dev/null 2>&1; then
local tmp local tmp
tmp="$(mktemp)" tmp="$(mktemp)"
if [ "$value" = "__BASHIO_UNSET__" ]; then if [ "$value" = "__BASHIO_UNSET__" ]; then
jq --arg k "$key" 'delpath(($k|split(".")))' "$file" > "$tmp" && mv "$tmp" "$file" jq --arg k "$key" 'delpath(($k|split(".")))' "$file" >"$tmp" && mv "$tmp" "$file"
else else
jq --arg k "$key" --arg v "$value" 'setpath(($k|split(".")); $v)' "$file" > "$tmp" && mv "$tmp" "$file" jq --arg k "$key" --arg v "$value" 'setpath(($k|split(".")); $v)' "$file" >"$tmp" && mv "$tmp" "$file"
fi fi
return 0 return 0
fi fi
@@ -239,7 +238,7 @@ bashio::info.operating_system() {
} }
bashio::info.arch() { uname -m; } bashio::info.arch() { uname -m; }
bashio::info.machine() { uname -m; } bashio::info.machine() { uname -m; }
bashio::info.homeassistant() { printf '%s' "standalone"; } bashio::info.homeassistant(){ printf '%s' "standalone"; }
bashio::info.supervisor() { printf '%s' "standalone"; } bashio::info.supervisor() { printf '%s' "standalone"; }
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
@@ -247,14 +246,11 @@ bashio::info.supervisor() { printf '%s' "standalone"; }
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
bashio::config() { bashio::config() {
local key="${1:-}" local key="${1:-}"
[ -n "$key" ] || { [ -n "$key" ] || { printf '%s' ""; return 0; }
printf '%s' ""
return 0
}
local v="" local v=""
if _bashio_env_get "$key" > /dev/null 2>&1; then if _bashio_env_get "$key" >/dev/null 2>&1; then
v="$(_bashio_env_get "$key" 2> /dev/null || true)" v="$(_bashio_env_get "$key" 2>/dev/null || true)"
fi fi
[ -z "$v" ] && v="$(_bashio_json_get "$key")" [ -z "$v" ] && v="$(_bashio_json_get "$key")"
printf '%s' "${v:-}" printf '%s' "${v:-}"
@@ -275,8 +271,8 @@ bashio::config.exists() {
return 0 return 0
fi fi
if [ -n "$file" ] && [ -f "$file" ] && command -v jq > /dev/null 2>&1; then 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 jq -e --arg k "$key" 'haspath(($k|split(".")))' "$file" >/dev/null 2>&1
return $? return $?
fi fi
@@ -297,8 +293,8 @@ bashio::config.array() {
raw="$(bashio::config "$key")" raw="$(bashio::config "$key")"
[ -n "$raw" ] || return 0 [ -n "$raw" ] || return 0
if command -v jq > /dev/null 2>&1 && printf '%s' "$raw" | jq -e . > /dev/null 2>&1; then 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 printf '%s' "$raw" | jq -r '.[]' 2>/dev/null && return 0
fi fi
if printf '%s' "$raw" | grep -q ','; then if printf '%s' "$raw" | grep -q ','; then
@@ -323,7 +319,7 @@ bashio::fs.file_exists() { [ -f "${1:-}" ]; }
bashio::fs.directory_exists() { [ -d "${1:-}" ]; } bashio::fs.directory_exists() { [ -d "${1:-}" ]; }
bashio::fs.file_contains() { bashio::fs.file_contains() {
local f="${1:-}" p="${2:-}" local f="${1:-}" p="${2:-}"
[ -f "$f" ] && grep -q -- "$p" "$f" 2> /dev/null [ -f "$f" ] && grep -q -- "$p" "$f" 2>/dev/null
} }
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
@@ -340,16 +336,16 @@ bashio::net.wait_for() {
bashio::dns.host() { bashio::dns.host() {
local h="${1:-}" local h="${1:-}"
[ -n "$h" ] || return 1 [ -n "$h" ] || return 1
if command -v getent > /dev/null 2>&1; then if command -v getent >/dev/null 2>&1; then
getent ahostsv4 "$h" | awk '{print $1; exit}' getent ahostsv4 "$h" | awk '{print $1; exit}'
else else
nslookup "$h" 2> /dev/null | awk '/^Address: /{print $2; exit}' nslookup "$h" 2>/dev/null | awk '/^Address: /{print $2; exit}'
fi fi
} }
# Hostname # Hostname
bashio::host.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}"
} }
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
@@ -360,10 +356,7 @@ bashio::host.hostname() {
# bashio::services.available "mqtt" # bashio::services.available "mqtt"
bashio::services() { bashio::services() {
local svc="${1:-}" key="${2:-}" local svc="${1:-}" key="${2:-}"
[ -n "$svc" ] && [ -n "$key" ] || { [ -n "$svc" ] && [ -n "$key" ] || { printf '%s' ""; return 0; }
printf '%s' ""
return 0
}
local upper svc_upper var v="" local upper svc_upper var v=""
upper="$(printf '%s' "$key" | tr '[:lower:]' '[:upper:]')" upper="$(printf '%s' "$key" | tr '[:lower:]' '[:upper:]')"
@@ -376,15 +369,15 @@ bashio::services() {
MQTT:USERNAME) var="MQTT_USER" ;; MQTT:USERNAME) var="MQTT_USER" ;;
MQTT:PASSWORD) var="MQTT_PASSWORD" ;; MQTT:PASSWORD) var="MQTT_PASSWORD" ;;
MQTT:TLS) var="MQTT_TLS" ;; MQTT:TLS) var="MQTT_TLS" ;;
MYSQL:HOST | MARIADB:HOST) var="DB_HOST" ;; MYSQL:HOST|MARIADB:HOST) var="DB_HOST" ;;
MYSQL:PORT | MARIADB:PORT) var="DB_PORT" ;; MYSQL:PORT|MARIADB:PORT) var="DB_PORT" ;;
MYSQL:USERNAME | MARIADB:USERNAME) var="DB_USER" ;; MYSQL:USERNAME|MARIADB:USERNAME) var="DB_USER" ;;
MYSQL:PASSWORD | MARIADB:PASSWORD) var="DB_PASSWORD" ;; MYSQL:PASSWORD|MARIADB:PASSWORD) var="DB_PASSWORD" ;;
MYSQL:DATABASE | MARIADB:DATABASE) var="DB_NAME" ;; MYSQL:DATABASE|MARIADB:DATABASE) var="DB_NAME" ;;
*) var="${svc_upper}_${upper}" ;; *) var="${svc_upper}_${upper}" ;;
esac esac
v="$(_bashio_env_get "$var" 2> /dev/null || true)" v="$(_bashio_env_get "$var" 2>/dev/null || true)"
if [ -z "$v" ]; then if [ -z "$v" ]; then
v="$(_bashio_json_get "services.${svc}.${key}")" v="$(_bashio_json_get "services.${svc}.${key}")"
[ -z "$v" ] && v="$(_bashio_json_get "${svc}.${key}")" [ -z "$v" ] && v="$(_bashio_json_get "${svc}.${key}")"
@@ -409,15 +402,15 @@ bashio::cache.set() { printf '%s' "${2:-}" > "$BASHIO_CACHE_DIR/${1}.cache"; }
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
# jq wrapper (some add-ons call bashio::jq) # 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 # Home Assistant token
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
bashio::homeassistant.token() { bashio::homeassistant.token() {
local t="${HOMEASSISTANT_TOKEN:-${HASS_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 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)" t="$(jq -er '.homeassistant.token // empty' "$STANDALONE_OPTIONS_JSON" 2>/dev/null || true)"
fi fi
printf '%s' "${t:-}" printf '%s' "${t:-}"
} }
@@ -426,11 +419,7 @@ bashio::homeassistant.token() {
# Exit helpers # Exit helpers
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
bashio::exit.ok() { exit 0; } bashio::exit.ok() { exit 0; }
bashio::exit.nok() { bashio::exit.nok() { local m="${1:-}"; [ -n "$m" ] && bashio::log.red "$m"; exit 1; }
local m="${1:-}"
[ -n "$m" ] && bashio::log.red "$m"
exit 1
}
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
# Core config check shim # Core config check shim

View File

@@ -41,14 +41,14 @@ restore_apt_sources_if_missing() {
mirror="http://ports.ubuntu.com/ubuntu-ports" mirror="http://ports.ubuntu.com/ubuntu-ports"
security_mirror="$mirror" security_mirror="$mirror"
fi fi
cat > /etc/apt/sources.list << EOF2 cat > /etc/apt/sources.list <<EOF2
deb ${mirror} ${codename} main restricted universe multiverse deb ${mirror} ${codename} main restricted universe multiverse
deb ${mirror} ${codename}-updates main restricted universe multiverse deb ${mirror} ${codename}-updates main restricted universe multiverse
deb ${security_mirror} ${codename}-security main restricted universe multiverse deb ${security_mirror} ${codename}-security main restricted universe multiverse
EOF2 EOF2
;; ;;
debian) 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} 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 ${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 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" = "apt" ]; then apt-get update > /dev/null; fi
if [ "$PACKMANAGER" = "pacman" ]; then pacman -Sy > /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 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 if [ -n "$ALPINE_VER" ]; then
echo "https://dl-cdn.alpinelinux.org/alpine/v${ALPINE_VER}/community" >> /etc/apk/repositories echo "https://dl-cdn.alpinelinux.org/alpine/v${ALPINE_VER}/community" >> /etc/apk/repositories
apk update > /dev/null 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 if ! ls $files 1> /dev/null 2>&1; then continue; fi
# Bashio # 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" [ "$VERBOSE" = true ] && echo "install bashio"
BASHIO_VERSION="v0.17.5" BASHIO_VERSION="v0.17.5"
mkdir -p /tmp/bashio mkdir -p /tmp/bashio

View File

@@ -10,7 +10,7 @@ if [ "$$" -eq 1 ]; then
PID1=true PID1=true
echo "Starting as entrypoint" echo "Starting as entrypoint"
if [ -d /command ]; then if [ -d /command ]; then
ln -sf /command/* /usr/bin/ 2> /dev/null || true ln -sf /command/* /usr/bin/ 2>/dev/null || true
fi fi
else else
echo "Starting custom scripts" echo "Starting custom scripts"
@@ -37,19 +37,19 @@ terminate() {
# fails whenever the stop arrives before the database is up. # fails whenever the stop arrives before the database is up.
set +e set +e
echo "Termination signal received, forwarding to subprocesses..." echo "Termination signal received, forwarding to subprocesses..."
if command -v pgrep > /dev/null 2>&1; then if command -v pgrep >/dev/null 2>&1; then
while read -r pid; do while read -r pid; do
[ -n "$pid" ] || continue [ -n "$pid" ] || continue
echo "Terminating child PID $pid" echo "Terminating child PID $pid"
kill -TERM "$pid" 2> /dev/null || echo "Failed to terminate PID $pid" kill -TERM "$pid" 2>/dev/null || echo "Failed to terminate PID $pid"
done < <(pgrep -P "$$" || true) done < <(pgrep -P "$$" || true)
else else
for p in /proc/[0-9]*/; do for p in /proc/[0-9]*/; do
local_pid="${p#/proc/}" local_pid="${p#/proc/}"
local_pid="${local_pid%/}" local_pid="${local_pid%/}"
if [ "$local_pid" -ne 1 ] && grep -q "^PPid:[[:space:]]*$$" "/proc/$local_pid/status" 2> /dev/null; then if [ "$local_pid" -ne 1 ] && grep -q "^PPid:[[:space:]]*$$" "/proc/$local_pid/status" 2>/dev/null; then
echo "Terminating child PID $local_pid" echo "Terminating child PID $local_pid"
kill -TERM "$local_pid" 2> /dev/null || echo "Failed to terminate PID $local_pid" kill -TERM "$local_pid" 2>/dev/null || echo "Failed to terminate PID $local_pid"
fi fi
done done
fi fi
@@ -76,20 +76,14 @@ pick_exec_dir() {
if [ -d "$d" ] && [ -w "$d" ]; then if [ -d "$d" ] && [ -w "$d" ]; then
# Create a tiny test executable to confirm "exec" works # Create a tiny test executable to confirm "exec" works
local t="${d%/}/.exec_test_$$" local t="${d%/}/.exec_test_$$"
printf '#!/bin/sh\necho ok\n' > "$t" 2> /dev/null || { printf '#!/bin/sh\necho ok\n' >"$t" 2>/dev/null || { rm -f "$t" 2>/dev/null || true; continue; }
rm -f "$t" 2> /dev/null || true chmod 700 "$t" 2>/dev/null || { rm -f "$t" 2>/dev/null || true; continue; }
continue if "$t" >/dev/null 2>&1; then
} rm -f "$t" 2>/dev/null || true
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" echo "$d"
return 0 return 0
fi fi
rm -f "$t" 2> /dev/null || true rm -f "$t" 2>/dev/null || true
fi fi
done done
return 1 return 1
@@ -163,7 +157,7 @@ validate_shebang() {
local errfile msg local errfile msg
# shellcheck disable=SC2206 # shellcheck disable=SC2206
local cmd=($candidate) local cmd=( $candidate )
local exe="${cmd[0]}" local exe="${cmd[0]}"
if [ ! -x "$exe" ]; then if [ ! -x "$exe" ]; then
@@ -176,18 +170,18 @@ validate_shebang() {
{ {
printf '#!%s\n' "$candidate" printf '#!%s\n' "$candidate"
printf '%s\n' "$probe_script_content" printf '%s\n' "$probe_script_content"
} > "$tmp" } >"$tmp"
chmod 700 "$tmp" 2> /dev/null || true chmod 700 "$tmp" 2>/dev/null || true
set +e set +e
out="$("$tmp" 2> "$errfile")" out="$("$tmp" 2>"$errfile")"
rc=$? rc=$?
set -e set -e
rm -f "$tmp" 2> /dev/null || true rm -f "$tmp" 2>/dev/null || true
if [ "$rc" -eq 0 ] && [ -n "${out:-}" ] && [ "$out" != "null" ]; then if [ "$rc" -eq 0 ] && [ -n "${out:-}" ] && [ "$out" != "null" ]; then
rm -f "$errfile" 2> /dev/null || true rm -f "$errfile" 2>/dev/null || true
return 0 return 0
fi fi
@@ -199,7 +193,7 @@ validate_shebang() {
msg+=$' stderr: <empty>\n' msg+=$' stderr: <empty>\n'
fi fi
SHEBANG_ERRORS+=("$msg") SHEBANG_ERRORS+=("$msg")
rm -f "$errfile" 2> /dev/null || true rm -f "$errfile" 2>/dev/null || true
return 1 return 1
} }
@@ -263,14 +257,14 @@ if $PID1 && { [ -x /command/with-contenv ] || [ -x /usr/bin/with-contenv ]; }; t
# accepts a surviving symlink-to-directory and would let the dump follow it. rm does not traverse # 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 # 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. # 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" \ 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 s6-dumpenv -- "$S6_CONTAINER_ENV.tmp" && mv "$S6_CONTAINER_ENV.tmp" "$S6_CONTAINER_ENV"; then
echo "Populated $S6_CONTAINER_ENV for with-contenv" echo "Populated $S6_CONTAINER_ENV for with-contenv"
else else
# Leaves the directory absent, which is exactly how this fails today -- so the failure mode is # 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, # 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. # 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 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" 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
fi fi
@@ -331,7 +325,7 @@ wait_for_supervisor() {
[ -n "${SUPERVISOR_TOKEN:-}" ] || return 0 [ -n "${SUPERVISOR_TOKEN:-}" ] || return 0
[ "${BASHIO_LIB_FULL:-false}" = "true" ] || 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. # 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 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 # 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 # arithmetic expansion reads it as octal and fails, which would leave the deadline empty and
# spin the loop below forever. # spin the loop below forever.
@@ -359,7 +353,7 @@ wait_for_supervisor() {
# logging is dropped because a failed attempt here is expected, not news. # logging is dropped because a failed attempt here is expected, not news.
# shellcheck disable=SC2016 # shellcheck disable=SC2016
if timeout "$attempt" bash -c '. "$1" && [ -n "$(bashio::addon.ip_address)" ]' \ if timeout "$attempt" bash -c '. "$1" && [ -n "$(bashio::addon.ip_address)" ]' \
_ "$BASHIO_LIB" > /dev/null 2>&1; then _ "$BASHIO_LIB" >/dev/null 2>&1; then
[ "$announced" -eq 0 ] || echo "Supervisor API ready after $((SECONDS - started))s" [ "$announced" -eq 0 ] || echo "Supervisor API ready after $((SECONDS - started))s"
return 0 return 0
fi fi
@@ -411,7 +405,7 @@ run_one_script() {
_run_rc=0 _run_rc=0
( (
# shellcheck disable=SC1090 # shellcheck disable=SC1090
. "$BASHIO_LIB" 2> /dev/null || true . "$BASHIO_LIB" 2>/dev/null || true
# shellcheck disable=SC1090 # shellcheck disable=SC1090
. "$script" . "$script"
) || _run_rc=$? ) || _run_rc=$?
@@ -451,7 +445,7 @@ if $PID1; then
_svc_rc=0 _svc_rc=0
( (
# shellcheck disable=SC1090 # shellcheck disable=SC1090
. "$BASHIO_LIB" 2> /dev/null || true . "$BASHIO_LIB" 2>/dev/null || true
# shellcheck disable=SC1090 # shellcheck disable=SC1090
. "$runfile" . "$runfile"
) || _svc_rc=$? ) || _svc_rc=$?

View File

@@ -147,7 +147,7 @@ manual actions with precedence. Prompts live in `.github/prompts/`, shared shell
in `.github/scripts/`. in `.github/scripts/`.
| Workflow | Model | Trigger | Role | | 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 | | `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`) | | `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 | | `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 ## Linting Rules
| Tool | Config | Key ignores | | Tool | Config | Key ignores |
| ------ | -------- | ------------ | |------|--------|------------|
| Hadolint | `.hadolint.yaml` | DL3002, DL3006-9, DL3018 (no pinning required) | | Hadolint | `.hadolint.yaml` | DL3002, DL3006-9, DL3018 (no pinning required) |
| ShellCheck | `.shellcheckrc` | SC2002 | | ShellCheck | `.shellcheckrc` | SC2002 |
| Markdownlint | `.markdownlint.yaml` | MD013 (line length), MD025, MD033, MD041 | | Markdownlint | `.markdownlint.yaml` | MD013 (line length), MD025, MD033, MD041 |

View File

@@ -109,7 +109,7 @@ never a default.
### Addon version numbering ### 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: The addon version is therefore derived from the upstream tag:

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -6,7 +6,7 @@ This addon is based on the docker image <https://github.com/lklynet/aurral>
## Configuration ## Configuration
| Option | Default | Description | | Option | Default | Description |
| --- | --- | --- | |---|---|---|
| `download_folder` | `/share/aurral/downloads` | Path where Aurral writes flow downloads. Must be under `/share`. | | `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`. | | `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.7 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

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

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

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

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@@ -1,3 +1,15 @@
## 20260917.1 (17-09-2026)
- Minor bugs fixed
## 20260917 (17-09-2026)
- Minor bugs fixed
## 20260916.2 (16-09-2026)
- Minor bugs fixed
## 20260916.1 (16-09-2026)
- Minor bugs fixed
## 20260916 (16-09-2026)
- Minor bugs fixed
## 20260915 (15-09-2026)
- Minor bugs fixed
## 20260912.2 (12-09-2026) ## 20260912.2 (12-09-2026)
- Minor bugs fixed - Minor bugs fixed
## 20260912 (12-09-2026) ## 20260912 (12-09-2026)

View File

@@ -75,7 +75,7 @@ Requires a second model to confirm the **first** detection of each bird species
The first detection of a species in a day is the weakest evidence the pipeline produces, and it is the one that creates a "new species today" entry. Asking two models to agree on just that one detection removes most spurious new-species entries without slowing anything else down. The first detection of a species in a day is the weakest evidence the pipeline produces, and it is the one that creates a "new species today" entry. Asking two models to agree on just that one detection removes most spurious new-species entries without slowing anything else down.
**Off by default.** Turn it on in the web UI under _Settings → Filters → First Daily Detection Consensus_, or in `config.yaml`: **Off by default.** Turn it on in the web UI under *Settings → Filters → First Daily Detection Consensus*, or in `config.yaml`:
```yaml ```yaml
realtime: realtime:
@@ -89,7 +89,7 @@ It deliberately does **nothing** in these cases, all of which keep today's singl
- you run only one bird model (the default) — a second opinion does not exist, so the rule can never trigger - you run only one bird model (the default) — a second opinion does not exist, so the rule can never trigger
- the species is not a bird — bats and the non-bird sound classes Perch reports (insects, amphibians, mammals, `power_tool`, and so on) - the species is not a bird — bats and the non-bird sound classes Perch reports (insects, amphibians, mammals, `power_tool`, and so on)
- the species is not known to _every_ active bird model analyzing that audio source — a species only one of them can name could never reach two confirmations. With several sources running different model combinations, this is decided per source, not add-on-wide - the species is not known to *every* active bird model analyzing that audio source — a species only one of them can name could never reach two confirmations. With several sources running different model combinations, this is decided per source, not add-on-wide
- a dynamic threshold has actually lowered the bar for that species, meaning you asked for a more permissive gate - a dynamic threshold has actually lowered the bar for that species, meaning you asked for a more permissive gate
- the taxonomy or the database cannot be consulted — it fails open and accepts the detection - the taxonomy or the database cannot be consulted — it fails open and accepts the detection

View File

@@ -127,5 +127,5 @@ slug: birdnet-go-dev
udev: true udev: true
url: https://github.com/alexbelgium/hassio-addons url: https://github.com/alexbelgium/hassio-addons
usb: true usb: true
version: "20260912.3" version: "20260917.1"
video: true video: true

View File

@@ -33,10 +33,7 @@ TARGET_DIR=""
while [ "$#" -gt 0 ]; do while [ "$#" -gt 0 ]; do
case "$1" in case "$1" in
--check) CHECK_ONLY=1 ;; --check) CHECK_ONLY=1 ;;
-*) -*) echo "unknown option: $1" >&2; exit 64 ;;
echo "unknown option: $1" >&2
exit 64
;;
*) *)
# Last-one-wins would silently clone into the wrong directory if a caller ever # Last-one-wins would silently clone into the wrong directory if a caller ever
# appended an argument; the pre-flag script used "${1}", so refuse rather than # appended an argument; the pre-flag script used "${1}", so refuse rather than
@@ -187,7 +184,7 @@ if [ "${CHECK_ONLY}" = "1" ]; then
fi fi
echo "!!! CHECK FAILED: ${#conflicting[@]} PR(s) would break the add-on build" >&2 echo "!!! CHECK FAILED: ${#conflicting[@]} PR(s) would break the add-on build" >&2
for entry in "${conflicting[@]}"; do for entry in "${conflicting[@]}"; do
IFS='|' read -r number scope files title <<< "${entry}" IFS='|' read -r number scope files title <<<"${entry}"
echo "!!! CONFLICT pr=#${number} conflicts-with=${scope} files=${files} title=${title}" >&2 echo "!!! CONFLICT pr=#${number} conflicts-with=${scope} files=${files} title=${title}" >&2
done done
echo "!!! conflicts-with=main -> the PR is stale against main; merge main into its branch and resolve there." >&2 echo "!!! conflicts-with=main -> the PR is stale against main; merge main into its branch and resolve there." >&2

View File

@@ -190,5 +190,5 @@ LOG_DIR="/config/logs"
if [ -d "$LOG_DIR" ]; then if [ -d "$LOG_DIR" ]; then
bashio::log.info "Trimming log files older than ${LOG_MAX_AGE_DAYS} days in ${LOG_DIR}" bashio::log.info "Trimming log files older than ${LOG_MAX_AGE_DAYS} days in ${LOG_DIR}"
ln -sf "$LOG_DIR" /logs 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 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 # 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. # written — a mysql block pointing at a different host was set manually.
# shellcheck disable=SC2016 # shellcheck disable=SC2016
CURRENT_MYSQL_HOST="$(yq -r '.output.mysql.host // empty' "$CONFIG_LOCATION" 2> /dev/null || true)" 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 \ if yq -e '.output.mysql.enabled == true' "$CONFIG_LOCATION" >/dev/null 2>&1 \
&& [ "${CURRENT_MYSQL_HOST}" = "${MYSQL_HOST}" ]; then && [ "${CURRENT_MYSQL_HOST}" = "${MYSQL_HOST}" ]; then
yq -i -y \ yq -i -y \
'.output.mysql.enabled = false '.output.mysql.enabled = false
@@ -71,7 +71,7 @@ bashio::log.green "---"
# Resolve MariaDB hostname to IPv4: on HAOS >=17.3 the Supervisor network # 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 # gained IPv6, but the MariaDB addon only grants its user from the IPv4
# subnet. Fall back to the raw hostname if resolution fails. # 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}}" MYSQL_HOST_RESOLVED="${MYSQL_HOST_RESOLVED:-${MYSQL_HOST}}"
if [ "${MYSQL_HOST_RESOLVED}" != "${MYSQL_HOST}" ]; then if [ "${MYSQL_HOST_RESOLVED}" != "${MYSQL_HOST}" ]; then
bashio::log.blue "Resolved ${MYSQL_HOST} -> ${MYSQL_HOST_RESOLVED} (forcing IPv4)" bashio::log.blue "Resolved ${MYSQL_HOST} -> ${MYSQL_HOST_RESOLVED} (forcing IPv4)"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -23,8 +23,8 @@
## nightly-20260615-2 (05-07-2026) ## nightly-20260615-2 (05-07-2026)
- Minor bugs fixed - 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) - 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 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 - 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 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) - Added backup_exclude for rotated logs, making Home Assistant backups smaller (SQLite journals are kept so hot backups stay consistent)

View File

@@ -186,5 +186,5 @@ LOG_DIR="/config/logs"
if [ -d "$LOG_DIR" ]; then if [ -d "$LOG_DIR" ]; then
bashio::log.info "Trimming log files older than ${LOG_MAX_AGE_DAYS} days in ${LOG_DIR}" bashio::log.info "Trimming log files older than ${LOG_MAX_AGE_DAYS} days in ${LOG_DIR}"
ln -sf "$LOG_DIR" /logs 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 fi

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -9,9 +9,9 @@ Here are some example tests I did (the whole threads are really interesting also
**My recommendation:** **My recommendation:**
- **Best entry system (< 50 €):** Boya By-lm40 (30 €) + deadcat (10 €) - **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 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 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:** **Sources for high end microphones in Europe:**

View File

@@ -112,7 +112,6 @@ def automatic_mqtt_publish(file, detection, path):
mqttc.publish(mqtt_topic, json_bird, 1) mqttc.publish(mqtt_topic, json_bird, 1)
log.info("Posted to MQTT: ok") log.info("Posted to MQTT: ok")
# Create MQTT client using legacy callback API when available for # Create MQTT client using legacy callback API when available for
# compatibility with paho-mqtt >= 2.0 # compatibility with paho-mqtt >= 2.0
callback_api = getattr(mqtt, "CallbackAPIVersion", None) 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:** **My recommendation:**
- **Best entry system (< 50 €):** Boya By-lm40 (30 €) + deadcat (10 €) - **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 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 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:** **Sources for high end microphones in Europe:**

View File

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

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

View File

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

View File

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

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

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

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

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

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

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

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

View File

@@ -21,19 +21,19 @@ export ROCKET_SECRET_KEY="${secret_key}"
# Find the matching log level # Find the matching log level
if bashio::config.has_value 'log_level'; then if bashio::config.has_value 'log_level'; then
case "$(bashio::string.lower "$(bashio::config 'log_level')")" in case "$(bashio::string.lower "$(bashio::config 'log_level')")" in
all | trace) all|trace)
log_level="trace" log_level="trace"
;; ;;
debug) debug)
log_level="debug" log_level="debug"
;; ;;
info | notice) info|notice)
log_level="info" log_level="info"
;; ;;
warning) warning)
log_level="warn" log_level="warn"
;; ;;
error | fatal) error|fatal)
log_level="error" log_level="error"
;; ;;
off) 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 ### Options
| Option | Type | Default | Description | | Option | Type | Default | Description |
| -------- | ------ | --------- | ------------- | |--------|------|---------|-------------|
| `PGID` | int | `0` | Group ID for file permissions | | `PGID` | int | `0` | Group ID for file permissions |
| `PUID` | int | `0` | User ID for file permissions | | `PUID` | int | `0` | User ID for file permissions |
| `TZ` | str | | Timezone (e.g., `Europe/London`) | | `TZ` | str | | Timezone (e.g., `Europe/London`) |

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

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

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

View File

@@ -42,7 +42,7 @@ args=(
# Container reality: sandbox usually breaks unless seccomp is fully disabled # Container reality: sandbox usually breaks unless seccomp is fully disabled
if [[ "${seccomp_mode}" != "0" ]]; then if [[ "${seccomp_mode}" != "0" ]]; then
args+=(--no-sandbox --test-type) args+=( --no-sandbox --test-type )
fi fi
# IMPORTANT: # IMPORTANT:

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -62,7 +62,7 @@ Default password: admin123
### Options ### Options
| Option | Type | Default | Description | | Option | Type | Default | Description |
| -------- | ------ | --------- | ------------- | |--------|------|---------|-------------|
| `PGID` | int | `0` | Group ID for file permissions | | `PGID` | int | `0` | Group ID for file permissions |
| `PUID` | int | `0` | User ID for file permissions | | `PUID` | int | `0` | User ID for file permissions |
| `TZ` | str | | Timezone (e.g., `Europe/London`) | | `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 ### 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`). 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. # 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 # 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. # 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) \ 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})" 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 # 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 # 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 # 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. # 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. # 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) \ 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})" bashio::log.warning "Could not reset the kepubify path, it will be applied at next start (${kepubify_error})"
fi fi
bashio::log.info "Default username:password is admin:admin123" bashio::log.info "Default username:password is admin:admin123"

View File

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

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -55,7 +55,7 @@
# shared memory into ordinary files for no benefit. If the size cannot be determined, keep the # 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. # flag — the crash it prevents is worse than the overhead it costs.
SHM_FLAGS="--disable-dev-shm-usage" 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 if [ -n "$SHM_KB" ] && [ "$SHM_KB" -ge 262144 ]; then
SHM_FLAGS="" SHM_FLAGS=""
fi fi

View File

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

View File

@@ -30,7 +30,7 @@ manage_claude_md_block() {
fi fi
elif [ -f "$CLAUDE_MD" ] && grep -qF "$begin" "$CLAUDE_MD"; then elif [ -f "$CLAUDE_MD" ] && grep -qF "$begin" "$CLAUDE_MD"; then
bashio::log.info "Removing ${name} guidance from CLAUDE.md" 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 os
import re import re
from pathlib import Path from pathlib import Path
@@ -58,7 +58,7 @@ PY
manage_settings_hook() { manage_settings_hook() {
# manage_settings_hook <event> <matcher> <command> <add|remove> # manage_settings_hook <event> <matcher> <command> <add|remove>
HOOK_EVENT="$1" HOOK_MATCHER="$2" HOOK_COMMAND="$3" HOOK_ACTION="$4" \ 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 json
import os import os
from pathlib import Path 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}" bashio::log.info "MCP servers for Claude Code: ${MCP_SERVERS_CODE}"
HEADROOM_ENABLED="$HEADROOM_ENABLED" HEADROOM_BIN="$(command -v headroom || echo headroom)" \ HEADROOM_ENABLED="$HEADROOM_ENABLED" HEADROOM_BIN="$(command -v headroom || echo headroom)" \
HEADROOM_HF_HOME="${HOME}/.headroom/hf" \ HEADROOM_HF_HOME="${HOME}/.headroom/hf" \
TOKENSAVE_ENABLED="$TOKENSAVE_ENABLED" TOKENSAVE_BIN="$(command -v tokensave || echo tokensave)" \ 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" \ 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" \ 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_PROXY_BIN="$(command -v mcp-proxy || echo mcp-proxy)" \
MCP_SERVERS_DESKTOP="$MCP_SERVERS_DESKTOP" MCP_SERVERS_CODE="$MCP_SERVERS_CODE" \ MCP_SERVERS_DESKTOP="$MCP_SERVERS_DESKTOP" MCP_SERVERS_CODE="$MCP_SERVERS_CODE" \
CLAUDE_DESKTOP_CONFIG="$CLAUDE_DESKTOP_CONFIG" CLAUDE_CODE_CONFIG="$CLAUDE_CODE_CONFIG" \ 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" python3 - <<'PY' || bashio::log.warning "Unable to update the MCP server registrations automatically"
import json import json
import os import os
from pathlib import Path from pathlib import Path
@@ -607,7 +607,7 @@ PY
# Guide Claude to actually use the Headroom compression tools so the MCP integration produces # Guide Claude to actually use the Headroom compression tools so the MCP integration produces
# real savings when transparent proxying is unavailable. # real savings when transparent proxying is unavailable.
if $HEADROOM_ENABLED; then if $HEADROOM_ENABLED; then
manage_claude_md_block headroom add << 'MD' manage_claude_md_block headroom add <<'MD'
## Headroom context compression ## Headroom context compression
A local Headroom proxy (127.0.0.1:8787) backs the `headroom` MCP tools. To save context tokens: 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 else
HEADROOM_ROUTE_ACTION="remove" HEADROOM_ROUTE_ACTION="remove"
fi 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 json
import os import os
from pathlib import Path 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 # Tell Claude Code that it can configure Home Assistant over the Core API via the shipped
# `ha-cli` helper (no /config filesystem mount needed). # `ha-cli` helper (no /config filesystem mount needed).
if bashio::config.true 'enable_ha_api_helper'; then 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 ## Configuring Home Assistant
You can configure this Home Assistant instance through its Core API using the `ha-cli` 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 # 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. # for a second agent, the same gap the Headroom block above exists to close.
if $CODEX_ENABLED; then if $CODEX_ENABLED; then
manage_claude_md_block codex add << 'MD' manage_claude_md_block codex add <<'MD'
## Delegating to ChatGPT Codex ## Delegating to ChatGPT Codex
The `codex` MCP server runs OpenAI's Codex agent locally, signed in with the user's ChatGPT 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 # 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. # is never deleted. Ownership of the written file is reconciled by 84-claude_runtime_ownership.sh.
mkdir -p "$(dirname "$SETTINGS_PATH")" 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 json
import os import os
from pathlib import Path 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 # archive stays unpatched, so every later boot would retry under a new pid and strand another
# copy until the container runs out of space. # copy until the container runs out of space.
cleanup_tmp() { 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 if output=$(timeout 120 node "$PATCHER" "$ASAR" 2>&1); then

View File

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

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

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

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

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -23,7 +23,7 @@ Integrations supported:
## Configuration ## Configuration
| Option | Description | | Option | Description |
| -------- | ------------- | |--------|-------------|
| `TZ` | Timezone (e.g. `Europe/Paris`). Defaults to `Europe/London`. | | `TZ` | Timezone (e.g. `Europe/Paris`). Defaults to `Europe/London`. |
| `PUID` | User ID to run the process as. Defaults to `0` (root). | | `PUID` | User ID to run the process as. Defaults to `0` (root). |
| `PGID` | Group ID to run the process as. Defaults to `0` (root). | | `PGID` | Group ID to run the process as. Defaults to `0` (root). |

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