mirror of
https://github.com/alexbelgium/hassio-addons.git
synced 2026-09-28 04:14:00 +02:00
Compare commits
56 Commits
7cf0ac8a52
...
create-pul
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
38d6a3b2db | ||
|
|
daa146a444 | ||
|
|
cda4232fa1 | ||
|
|
194f220224 | ||
|
|
be69aed455 | ||
|
|
502800f9bf | ||
|
|
1ea89fc150 | ||
|
|
69efa09f54 | ||
|
|
ae1f85229b | ||
|
|
3dbf317fa0 | ||
|
|
2c42b37819 | ||
|
|
358f6da616 | ||
|
|
2273c360fc | ||
|
|
9443356b36 | ||
|
|
500c083c5e | ||
|
|
e06b6d1c68 | ||
|
|
660982e641 | ||
|
|
38eac4c510 | ||
|
|
b97824acfc | ||
|
|
6d78111022 | ||
|
|
626991ec4d | ||
|
|
439f1e0e8f | ||
|
|
f2fef2e816 | ||
|
|
3089be5346 | ||
|
|
55f20e87b7 | ||
|
|
147ebeb3bc | ||
|
|
620dadf8fa | ||
|
|
7bfa34980b | ||
|
|
7fa4b08359 | ||
|
|
8338f5767c | ||
|
|
6424f68cff | ||
|
|
fe392aba1a | ||
|
|
287e95e5ab | ||
|
|
edf585e652 | ||
|
|
aa85189f69 | ||
|
|
d79cb11c50 | ||
|
|
81d10f3be8 | ||
|
|
49c6fbca2d | ||
|
|
8ad14ad6ce | ||
|
|
05f394d511 | ||
|
|
92ee1438dd | ||
|
|
c51c4c00a0 | ||
|
|
2ed1fdcdc6 | ||
|
|
c2bb8209ba | ||
|
|
d807435165 | ||
|
|
116131efd4 | ||
|
|
c66c41b596 | ||
|
|
ac19165c2f | ||
|
|
11706236ca | ||
|
|
b53dd0556e | ||
|
|
2135aa10f2 | ||
|
|
15c9da9fb3 | ||
|
|
8b742240b2 | ||
|
|
962c3b736d | ||
|
|
e8dc044d99 | ||
|
|
4e79ac84e5 |
@@ -33,8 +33,20 @@ Triage first, then one of two paths:
|
||||
the plan → implement → simplify → Codex reviews the code → **simplify again** → PR → resolve
|
||||
comments → verify in production → report.
|
||||
|
||||
Escalate mid-flight if a light task grows — touches a default, needs a new script or service, or
|
||||
reveals a deeper problem.
|
||||
Before implementing, on **either** path, ask yourself the proportionality question and answer
|
||||
it in the plan (a one-line plan, for a light task) — in writing, naming the smaller version you
|
||||
rejected and why:
|
||||
|
||||
> What is the smallest thing that solves this? What would you delete from what I just proposed?
|
||||
> If it had to be written in ten lines, in the idiom of the code it sits beside, what would be
|
||||
> lost — and is any of that a case I have actually demonstrated?
|
||||
|
||||
Escalate mid-flight if a light task grows — touches a default, needs a new script or service,
|
||||
reveals a deeper problem, or turns out to need more than the one file once you have honestly
|
||||
answered that question. A light task never skips it; it skips **Codex** only while the answer
|
||||
stays inside one file. Adding a **new file or a new mechanism** puts it on the full loop, however
|
||||
small the diff looks — a version bump and its CHANGELOG entry are release bookkeeping, not a new
|
||||
file for this purpose, and never trigger it on their own.
|
||||
|
||||
**Standing rule:** ship the simplest solution that works, and build it out of what already
|
||||
exists — a `.templates/` module, an existing cont-init script, the pattern a sibling add-on
|
||||
@@ -113,6 +125,16 @@ plan:
|
||||
Levels 4-6 need a reason that survives being said out loud ("upstream has no knob for this, and I
|
||||
checked" is one; "it felt cleaner" is not) and mean full loop.
|
||||
|
||||
**Full loop adds Codex to the proportionality question already asked above** — put it to Codex as
|
||||
a numbered question of its own, with the smaller alternative sketched out for it to argue for.
|
||||
Every other question in the loop asks what could go wrong, which only ever argues for more code;
|
||||
this is the one place that pushes the other way, and it is worthless unless asked outright — see
|
||||
`references/codex-review.md`. Nothing is built yet at this step, so "test" means against the real
|
||||
requirement, not against the other candidate — sketch the smaller one and check it against what
|
||||
the add-on actually needs; that sketch is what you go on to implement if it holds up, not one of
|
||||
two things you build. A sketch costs minutes and settles the argument with a measurement instead
|
||||
of a preference.
|
||||
|
||||
Attack your own plan before implementing:
|
||||
- What does this do on a host **unlike this one** — no GPU, small `/dev/shm`, aarch64, a VM?
|
||||
- What happens on **upgrade** to someone who configured this by hand?
|
||||
@@ -123,7 +145,8 @@ Attack your own plan before implementing:
|
||||
where that input arrives (the standing rule above) and go and look, before you write it.
|
||||
|
||||
Full loop only, before writing code: get Codex's independent read on the plan, delegated as above —
|
||||
`references/codex-review.md` has the invocation and how to write the prompt.
|
||||
`references/codex-review.md` has the invocation and how to write the prompt. The proportionality
|
||||
question above goes in that prompt every time, and in the step 6 prompt too.
|
||||
|
||||
## 4. Implement
|
||||
|
||||
@@ -145,7 +168,7 @@ path.
|
||||
|
||||
## 5. Simplify
|
||||
|
||||
Six questions over your own diff, before anyone else reads it:
|
||||
Seven questions over your own diff, before anyone else reads it:
|
||||
|
||||
- **Level** — did the diff stay at the ladder level chosen in step 3, or creep up one?
|
||||
- **Deletion** — can this be solved by deleting instead of adding?
|
||||
@@ -153,6 +176,10 @@ Six questions over your own diff, before anyone else reads it:
|
||||
- **Reuse** — does any hunk reimplement what `.templates/`, another script in this add-on, or a
|
||||
sibling add-on already does? If a future add-on hits this problem, will it find one way to solve
|
||||
it or two? Fold near-duplicates in, or justify the divergence in the PR body.
|
||||
- **Idiom** — does this look like the code it sits beside? The same kind of job done two ways in
|
||||
one file costs every later reader more than the nicer of the two ways saves. Reuse asks whether
|
||||
the mechanism already exists; Idiom asks whether you wrote yours the way the neighbours are
|
||||
written.
|
||||
- **Depth** — is this a special case bolted onto shared infrastructure? Fix the shared mechanism
|
||||
instead once more than one add-on hits it; generalising from a single case is how bespoke
|
||||
designs get built, so below that bar the special case is the right call.
|
||||
@@ -177,6 +204,12 @@ you have now demonstrated, or one you have merely been told about? Taking a
|
||||
correctness objection often deletes the code that made it necessary, and a fix that collapses back
|
||||
to fewer lines than you started the review with is the normal outcome, not a suspicious one.
|
||||
|
||||
Then ask the proportionality question again, of the diff this time — it is cheap, and by now
|
||||
there is real code to point at rather than a plan. This is where "build the smaller one and test
|
||||
both" is actually free: the larger candidate already exists as the diff, so sketch the smaller
|
||||
alternative and diff its output against the shipped code on the same inputs, the way #3061 was
|
||||
settled (`references/simplify.md`) — rather than against the requirement alone.
|
||||
|
||||
These edits land after step 4's checks already ran, so re-run them: `scripts/validate.sh <addon>
|
||||
--vs-master` plus the behavioural tests, over the final diff. Deleting a branch is exactly the
|
||||
kind of edit that leaves a stray `fi` behind.
|
||||
|
||||
@@ -27,6 +27,10 @@ codex exec --model gpt-5.6-sol --sandbox read-only --skip-git-repo-check \
|
||||
- **Plan review (step 3):** include the files to read, your measurements **with numbers**, the
|
||||
proposed changes, and explicit instructions to challenge you. Ask direct questions ("is this
|
||||
really add-on-fixable?", "give the precise flag set") rather than "review this".
|
||||
- **The proportionality question, in both reviews, as a numbered question of its own:** "is this
|
||||
the simplest way possible, and what exactly would you delete?" Sketch the smaller alternative in
|
||||
the prompt and ask it to argue for that, and quote the repo's standing simplicity rule so it has
|
||||
the bar to hold you to. Ask for lines and functions to delete, not a direction.
|
||||
- **Code review (step 6):** point it at `git diff origin/master...HEAD` plus the reasoning behind
|
||||
each hunk. Ask specifically what breaks: upgrade paths, hosts unlike this one, users who
|
||||
configured things by hand. Ask directly whether a simpler mechanism would achieve the same
|
||||
@@ -41,3 +45,8 @@ confirmations with the same scepticism as its objections — especially about th
|
||||
|
||||
**Its objections only ever argue for more code** — it is asked what could go wrong, never whether
|
||||
the branch it wants is reachable. Sort them before writing anything; SKILL.md step 6 is that pass.
|
||||
|
||||
**Unprompted it will never say "this is too much".** Asked outright it will, bluntly and usefully:
|
||||
on PR #3061 it answered "delete all 309 lines" of a helper both its earlier reviews had passed
|
||||
without comment. Expect it to reverse an earlier position when you ask it to re-examine one — a
|
||||
stance it drops that easily was never strongly held, which is itself the answer.
|
||||
|
||||
@@ -30,6 +30,17 @@ Being able to build the complicated thing is not a reason to.
|
||||
It took the maintainer asking "is this the simplest way possible?" to run the pass that step 6
|
||||
now requires.
|
||||
|
||||
- The immich updater fix (issue #3060, PR #3061) initially proposed a **309-line Python registry
|
||||
client** to read one OCI label, into a script whose neighbouring `dockerhub` branch does the same kind of
|
||||
job in a handful of inline `curl | jq` lines. Both the plan review and the code review passed
|
||||
it, because both were asked what could break rather than what it should cost. Asked "is this the
|
||||
simplest way possible?", Codex answered "delete all 309 lines"; the inline version came to 21
|
||||
and was measured to return byte-identical output on all five images. About a third of what went
|
||||
was defending cases never observed in this repo — an architecture mismatch, a hostile label, an
|
||||
unsupported registry — each of which already failed closed on its own. Second time the
|
||||
maintainer has had to ask the question, which is why step 3 now asks it before any code exists
|
||||
and step 5 asks whether the hunk looks like its neighbours.
|
||||
|
||||
## Where SKILL.md step 5's questions get hard
|
||||
|
||||
**Deletion is not automatically the safe direction.** The `/dev/shm` case in `evidence.md` is a
|
||||
|
||||
@@ -30,8 +30,14 @@ VAR="${1:?usage: env_trace.sh <VAR> [process-name-or-pid]}"
|
||||
TARGET="${2:-}"
|
||||
# VAR is interpolated into grep/sed patterns below — restrict it to a valid env var name
|
||||
case "$VAR" in
|
||||
[A-Za-z_]*) [ -z "${VAR//[A-Za-z0-9_]/}" ] || { echo "invalid env var name: $VAR" >&2; exit 1; } ;;
|
||||
*) echo "invalid env var name: $VAR" >&2; exit 1 ;;
|
||||
[A-Za-z_]*) [ -z "${VAR//[A-Za-z0-9_]/}" ] || {
|
||||
echo "invalid env var name: $VAR" >&2
|
||||
exit 1
|
||||
} ;;
|
||||
*)
|
||||
echo "invalid env var name: $VAR" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
echo "== tracing ${VAR} =="
|
||||
@@ -39,7 +45,7 @@ echo
|
||||
|
||||
echo "1. /data/options.json (the user's saved options)"
|
||||
if [ -f /data/options.json ]; then
|
||||
python3 - "$VAR" <<'PY'
|
||||
python3 - "$VAR" << 'PY'
|
||||
import json, sys
|
||||
var = sys.argv[1]
|
||||
try:
|
||||
@@ -80,7 +86,8 @@ echo "3. s6 container_environment (only read by services using #!/usr/bin/with-c
|
||||
seen3=0
|
||||
for d in /var/run/s6/container_environment /run/s6/container_environment; do
|
||||
if [ -f "$d/$VAR" ]; then
|
||||
echo " $d/$VAR = [$(cat "$d/$VAR")]"; seen3=1
|
||||
echo " $d/$VAR = [$(cat "$d/$VAR")]"
|
||||
seen3=1
|
||||
fi
|
||||
done
|
||||
[ "$seen3" -eq 0 ] && echo " not present in either envdir"
|
||||
@@ -122,8 +129,8 @@ else
|
||||
done
|
||||
fi
|
||||
# What it was actually launched with beats any theory about its environment.
|
||||
tr '\0' '\n' < "/proc/$pid/cmdline" 2> /dev/null | tail -n +2 |
|
||||
grep -iE "res|screen|${VAR}" | head -3 | sed 's/^/ argv: /'
|
||||
tr '\0' '\n' < "/proc/$pid/cmdline" 2> /dev/null | tail -n +2 \
|
||||
| grep -iE "res|screen|${VAR}" | head -3 | sed 's/^/ argv: /'
|
||||
done
|
||||
fi
|
||||
fi
|
||||
|
||||
@@ -31,7 +31,7 @@ awk '{t+=$2} END{printf " threads=%d\n", t}' <(ps -eo pid,nlwp --no-headers 2>
|
||||
echo
|
||||
echo "== per-process memory (top 20 by PSS) =="
|
||||
printf ' %-28s %8s %8s %8s\n' COMMAND RSS PSS PRIVATE
|
||||
python3 - "$OUT" <<'PY'
|
||||
python3 - "$OUT" << 'PY'
|
||||
import os, sys
|
||||
rows = []
|
||||
for pid in filter(str.isdigit, os.listdir('/proc')):
|
||||
@@ -62,17 +62,17 @@ PY
|
||||
|
||||
echo
|
||||
echo "== reserved-but-not-resident (lazy allocations, NOT leaks) =="
|
||||
ipcs -m 2>/dev/null | awk 'NR>3 && $5 ~ /^[0-9]+$/ && $5 > 50000000 {printf " SysV shm %.0f MB (owner %s) — check Rss in /proc/<pid>/smaps before calling it used\n", $5/1048576, $3}'
|
||||
ipcs -m 2> /dev/null | awk 'NR>3 && $5 ~ /^[0-9]+$/ && $5 > 50000000 {printf " SysV shm %.0f MB (owner %s) — check Rss in /proc/<pid>/smaps before calling it used\n", $5/1048576, $3}'
|
||||
|
||||
echo
|
||||
echo "== CPU over ${SAMPLE}s (idle unless you are driving the UI) =="
|
||||
# utime+stime. Parsed after the LAST ')' because field 2 is (comm) and may contain spaces —
|
||||
# a plain $14+$15 is wrong for anything like 'npm exec @foo' and silently reports a fabricated
|
||||
# number rather than failing.
|
||||
jiffies() { awk -F') ' '{n=split($NF,a," "); print a[12]+a[13]}' "/proc/$1/stat" 2>/dev/null; }
|
||||
jiffies() { awk -F') ' '{n=split($NF,a," "); print a[12]+a[13]}' "/proc/$1/stat" 2> /dev/null; }
|
||||
|
||||
# jiffies are USER_HZ units — almost always 100, but read it rather than assume it
|
||||
HZ=$(getconf CLK_TCK 2>/dev/null) && [ "$HZ" -gt 0 ] 2>/dev/null || HZ=100
|
||||
HZ=$(getconf CLK_TCK 2> /dev/null) && [ "$HZ" -gt 0 ] 2> /dev/null || HZ=100
|
||||
|
||||
# Sample EVERY readable process, not the top-N of ps.txt: that list is sorted by RSS,
|
||||
# and the busiest process is not necessarily a big one.
|
||||
@@ -86,14 +86,14 @@ for pid in "${!t0[@]}"; do
|
||||
[ -r "/proc/$pid/stat" ] || continue
|
||||
t1=$(jiffies "$pid") || continue
|
||||
[ -n "$t1" ] && [ -n "${t0[$pid]}" ] || continue
|
||||
delta=$(( t1 - ${t0[$pid]} ))
|
||||
delta=$((t1 - ${t0[$pid]}))
|
||||
[ "$delta" -gt 0 ] || continue
|
||||
pct=$(awk -v d="$delta" -v s="$SAMPLE" -v hz="$HZ" 'BEGIN{printf "%.2f", d*100/(hz*s)}')
|
||||
comm=$(tr -d '\0' < "/proc/$pid/comm" 2>/dev/null)
|
||||
comm=$(tr -d '\0' < "/proc/$pid/comm" 2> /dev/null)
|
||||
echo "$pct $pid $comm"
|
||||
done | sort -rn | head -12 | awk '{printf " %6s%% %-8s %s\n", $1, $2, $3}'
|
||||
|
||||
echo
|
||||
echo " established conns on :8082/:3000/:3001 = $(ss -tn 2>/dev/null | grep -cE 'ESTAB.*:(8082|3000|3001)')"
|
||||
echo " established conns on :8082/:3000/:3001 = $(ss -tn 2> /dev/null | grep -cE 'ESTAB.*:(8082|3000|3001)')"
|
||||
echo " (those are claude_desktop/webtop viewer ports; 0 here means CPU above is idle burn)"
|
||||
echo " raw ps: $OUT/ps.txt"
|
||||
|
||||
@@ -22,13 +22,20 @@ set -uo pipefail
|
||||
|
||||
REPO="${HASSIO_REPO:-}"
|
||||
[ -z "$REPO" ] && REPO=$(gh repo view --json nameWithOwner --jq .nameWithOwner 2> /dev/null)
|
||||
[ -z "$REPO" ] && { echo "cannot determine repo; set HASSIO_REPO=owner/name" >&2; exit 1; }
|
||||
[ -z "$REPO" ] && {
|
||||
echo "cannot determine repo; set HASSIO_REPO=owner/name" >&2
|
||||
exit 1
|
||||
}
|
||||
echo "repo: $REPO" >&2
|
||||
CMD="${1:-}"; PR="${2:-}"
|
||||
[ -z "$CMD" ] || [ -z "$PR" ] && { sed -n '2,20p' "$0" | sed 's/^# \?//'; exit 1; }
|
||||
CMD="${1:-}"
|
||||
PR="${2:-}"
|
||||
[ -z "$CMD" ] || [ -z "$PR" ] && {
|
||||
sed -n '2,20p' "$0" | sed 's/^# \?//'
|
||||
exit 1
|
||||
}
|
||||
|
||||
case "$CMD" in
|
||||
list)
|
||||
list)
|
||||
echo "== inline comments on #$PR =="
|
||||
gh api "repos/$REPO/pulls/$PR/comments" --paginate \
|
||||
--jq 'sort_by(.created_at)[] | "=== [\(.id)] \(.user.login) | \(.path):\(.line // .original_line) ===\n\(.body)\n"'
|
||||
@@ -36,14 +43,15 @@ list)
|
||||
gh api "repos/$REPO/pulls/$PR/reviews" \
|
||||
--jq '.[] | select(.body != "") | "--- \(.user.login) (\(.state)) ---\n\(.body[0:4000])\n"'
|
||||
;;
|
||||
status)
|
||||
status)
|
||||
gh pr checks "$PR" 2>&1 | head -15
|
||||
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}}}}}}}}" \
|
||||
--jq '.data.repository.pullRequest.reviewThreads.nodes[] | "\(if .isResolved then "resolved" else "OPEN " end) \(.id) \(.comments.nodes[0].author.login) \(.path)"'
|
||||
;;
|
||||
reply)
|
||||
ID="${3:?comment id}"; BODY="${4:?text or @file}"
|
||||
reply)
|
||||
ID="${3:?comment id}"
|
||||
BODY="${4:?text or @file}"
|
||||
if [ "${BODY#@}" != "$BODY" ]; then
|
||||
out=$(gh api "repos/$REPO/pulls/$PR/comments/$ID/replies" -F body=@"${BODY#@}" --jq '.id' 2>&1)
|
||||
rc=$?
|
||||
@@ -61,7 +69,7 @@ reply)
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
resolve)
|
||||
resolve)
|
||||
shift 2
|
||||
ids="$*"
|
||||
if [ "${ids:-}" = "--all" ]; then
|
||||
@@ -69,8 +77,12 @@ resolve)
|
||||
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}}}}}}}" \
|
||||
--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: '; read -r ok
|
||||
[ "$ok" = "yes" ] || { echo "aborted"; exit 1; }
|
||||
printf 'Type "yes" to resolve all: '
|
||||
read -r ok
|
||||
[ "$ok" = "yes" ] || {
|
||||
echo "aborted"
|
||||
exit 1
|
||||
}
|
||||
ids=""
|
||||
elif [ -z "$ids" ]; then
|
||||
echo "usage: pr_review.sh resolve <PR> <THREAD_ID...> (or --all, with confirmation)" >&2
|
||||
@@ -81,7 +93,10 @@ resolve)
|
||||
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')
|
||||
fi
|
||||
[ -z "$ids" ] && { echo "nothing unresolved"; exit 0; }
|
||||
[ -z "$ids" ] && {
|
||||
echo "nothing unresolved"
|
||||
exit 0
|
||||
}
|
||||
rfail=0
|
||||
for id in $ids; do
|
||||
r=$(gh api graphql -f query="mutation{resolveReviewThread(input:{threadId:\"$id\"}){thread{isResolved}}}" \
|
||||
@@ -92,7 +107,7 @@ resolve)
|
||||
# exiting 0 on a failed mutation would let a session believe threads were resolved
|
||||
exit "$rfail"
|
||||
;;
|
||||
watch)
|
||||
watch)
|
||||
MINS="${3:-180}" # the addon build alone has taken ~3h; 20 was far too short
|
||||
# Checks that are red on essentially every add-on PR here and gate nothing: master carries no
|
||||
# branch protection, and #3019, #3044 and #3050 all merged with Codacy failing. They are kept
|
||||
@@ -101,7 +116,9 @@ watch)
|
||||
# CI later counts as blocking until someone puts it here on purpose.
|
||||
ADVISORY_CHECKS="Codacy Static Code Analysis" # one per line if more are ever added
|
||||
wfail=2 # not 0: running out of minutes with checks still pending is not a pass
|
||||
c=""; bstates=""; adv=""
|
||||
c=""
|
||||
bstates=""
|
||||
adv=""
|
||||
for i in $(seq 1 "$MINS"); do
|
||||
# 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
|
||||
@@ -121,7 +138,8 @@ watch)
|
||||
# Normal in the first minutes after `gh pr create`, and also whenever gh errors.
|
||||
# Calling that "settled" would report success for checks that never ran.
|
||||
echo "[$i] no checks reported yet (gh returned nothing) — still waiting"
|
||||
sleep 60; continue
|
||||
sleep 60
|
||||
continue
|
||||
fi
|
||||
c=$(printf '%s\n' "$rows" | cut -f2 | tr '\n' ' ')
|
||||
echo "[$i] $c"
|
||||
@@ -131,12 +149,14 @@ watch)
|
||||
adv=$(printf '%s\n' "$rows" | awk -F'\t' '$1 == "A" { print $2 }' | tr '\n' ' ')
|
||||
if [ -z "$bstates" ]; then
|
||||
echo " only advisory checks have reported — no blocking check has run yet"
|
||||
sleep 60; continue
|
||||
sleep 60
|
||||
continue
|
||||
fi
|
||||
# Allowlist the good states rather than denylisting the bad ones: an unrecognised state
|
||||
# must land in the failure branch, because falling through to "passing" is this command's
|
||||
# worst outcome.
|
||||
nbad=0; npend=0
|
||||
nbad=0
|
||||
npend=0
|
||||
for s in $bstates; do
|
||||
case "$s" in
|
||||
pass | skipping) ;;
|
||||
@@ -146,13 +166,17 @@ watch)
|
||||
done
|
||||
if [ "$nbad" -gt 0 ]; then
|
||||
echo "settled — blocking checks FAILED:"
|
||||
printf '%s\n' "$rows" |
|
||||
awk -F'\t' '$1 == "B" && $3 != "pass" && $3 != "skipping" && $3 != "pending" { print " " $2 }'
|
||||
wfail=1; break
|
||||
printf '%s\n' "$rows" \
|
||||
| awk -F'\t' '$1 == "B" && $3 != "pass" && $3 != "skipping" && $3 != "pending" { print " " $2 }'
|
||||
wfail=1
|
||||
break
|
||||
elif [ "$npend" -gt 0 ]; then
|
||||
sleep 60; continue
|
||||
sleep 60
|
||||
continue
|
||||
else
|
||||
echo "settled — blocking checks passing"; wfail=0; break
|
||||
echo "settled — blocking checks passing"
|
||||
wfail=0
|
||||
break
|
||||
fi
|
||||
done
|
||||
[ "$wfail" -eq 2 ] && echo "gave up after ${MINS}m, checks still unsettled — NOT a pass"
|
||||
@@ -163,5 +187,8 @@ watch)
|
||||
echo "note: long queues here are usually account runner contention, not your diff."
|
||||
exit "$wfail"
|
||||
;;
|
||||
*) echo "unknown: $CMD"; exit 1 ;;
|
||||
*)
|
||||
echo "unknown: $CMD"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
@@ -59,7 +59,10 @@ if [ -n "${BUILD_VERSION:-}" ]; then
|
||||
if [ -z "$SLUG" ] && [ -n "${HOSTNAME:-}" ]; then
|
||||
base=$(printf '%s' "$HOSTNAME" | sed 's/^[0-9a-f]\{8\}-//')
|
||||
for cand in "$(printf '%s' "$base" | tr '-' '_')" "$base"; do
|
||||
[ -f "$REPO/$cand/config.yaml" ] && { SLUG="$cand"; break; }
|
||||
[ -f "$REPO/$cand/config.yaml" ] && {
|
||||
SLUG="$cand"
|
||||
break
|
||||
}
|
||||
done
|
||||
[ -z "$SLUG" ] && SLUG="$base"
|
||||
fi
|
||||
@@ -81,8 +84,8 @@ if [ -n "${BUILD_VERSION:-}" ]; then
|
||||
else
|
||||
echo " MISMATCH — this branch is NOT what is running."
|
||||
git fetch origin master --quiet 2> /dev/null
|
||||
master=$(git show origin/master:"$SLUG/config.yaml" 2> /dev/null |
|
||||
grep -E '^version:' | head -1 | tr -d "\"'" | awk '{print $2}')
|
||||
master=$(git show origin/master:"$SLUG/config.yaml" 2> /dev/null \
|
||||
| grep -E '^version:' | head -1 | tr -d "\"'" | awk '{print $2}')
|
||||
echo " origin/master version = ${master:-unknown}"
|
||||
echo " -> work from origin/master; analysing this branch will mislead you."
|
||||
echo
|
||||
|
||||
@@ -12,7 +12,10 @@
|
||||
set -uo pipefail
|
||||
|
||||
TRAPS="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)/references/traps.md"
|
||||
[ -f "$TRAPS" ] || { echo "not found: $TRAPS" >&2; exit 1; }
|
||||
[ -f "$TRAPS" ] || {
|
||||
echo "not found: $TRAPS" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
# The Contents list duplicates the headings; grep the headings themselves so the list cannot drift.
|
||||
list() {
|
||||
@@ -20,13 +23,16 @@ list() {
|
||||
grep '^## ' "$TRAPS" | grep -v '^## Contents' | sed 's/^## / /'
|
||||
}
|
||||
|
||||
[ $# -eq 0 ] && { list; exit 0; }
|
||||
[ $# -eq 0 ] && {
|
||||
list
|
||||
exit 0
|
||||
}
|
||||
|
||||
# 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
|
||||
# silent pick of the first.
|
||||
mapfile -t matches < <(grep '^## ' "$TRAPS" | grep -v '^## Contents' |
|
||||
grep -iF -- "$1" | sed 's/^## //')
|
||||
mapfile -t matches < <(grep '^## ' "$TRAPS" | grep -v '^## Contents' \
|
||||
| grep -iF -- "$1" | sed 's/^## //')
|
||||
|
||||
case "${#matches[@]}" in
|
||||
0)
|
||||
|
||||
@@ -13,22 +13,31 @@ set -uo pipefail
|
||||
if root=$(git rev-parse --show-toplevel 2> /dev/null); then cd "$root" || exit 1; fi
|
||||
|
||||
ADDON="${1:-}"
|
||||
[ "${ADDON:-}" = "--vs-master" ] && { ADDON=""; set -- --vs-master; }
|
||||
[ "${ADDON:-}" = "--vs-master" ] && {
|
||||
ADDON=""
|
||||
set -- --vs-master
|
||||
}
|
||||
VS_MASTER=false
|
||||
for a in "$@"; do [ "$a" = "--vs-master" ] && VS_MASTER=true; done
|
||||
|
||||
if [ -z "$ADDON" ]; then
|
||||
mapfile -t _dirs < <(git diff --name-only origin/master...HEAD 2> /dev/null |
|
||||
cut -d/ -f1 | sort -u | grep -vE '^\.' )
|
||||
mapfile -t _dirs < <(git diff --name-only origin/master...HEAD 2> /dev/null \
|
||||
| cut -d/ -f1 | sort -u | grep -vE '^\.')
|
||||
if [ "${#_dirs[@]}" -gt 1 ]; then
|
||||
echo "several changed dirs: ${_dirs[*]}"
|
||||
echo "pass one explicitly: validate.sh <addon-dir>"; exit 1
|
||||
echo "pass one explicitly: validate.sh <addon-dir>"
|
||||
exit 1
|
||||
fi
|
||||
ADDON="${_dirs[0]:-}"
|
||||
fi
|
||||
[ -z "$ADDON" ] && { echo "usage: validate.sh <addon-dir> [--vs-master]"; exit 1; }
|
||||
[ -z "$ADDON" ] && {
|
||||
echo "usage: validate.sh <addon-dir> [--vs-master]"
|
||||
exit 1
|
||||
}
|
||||
git rev-parse --verify origin/master > /dev/null 2>&1 || {
|
||||
echo "origin/master missing — run: git fetch origin master"; exit 1; }
|
||||
echo "origin/master missing — run: git fetch origin master"
|
||||
exit 1
|
||||
}
|
||||
export PYTHONDONTWRITEBYTECODE=1
|
||||
echo "== validating $ADDON =="
|
||||
|
||||
@@ -36,14 +45,22 @@ fail=0
|
||||
note() { printf ' %-13s %s\n' "$1" "$2"; }
|
||||
# execline `run`/`finish` files are not shell (25 of them here, across 21 add-ons). Neither
|
||||
# linter below can read one, so anything either says about it is noise.
|
||||
is_execline() { local l; IFS= read -r l < "$1" 2> /dev/null; [[ $l == '#!'*execlineb* ]]; }
|
||||
is_execline() {
|
||||
local l
|
||||
IFS= read -r l < "$1" 2> /dev/null
|
||||
[[ $l == '#!'*execlineb* ]]
|
||||
}
|
||||
|
||||
# Shell: bash -n then shellcheck -x (follows sourced files, as CI does). One list for both.
|
||||
files=()
|
||||
while IFS= read -r f; do is_execline "$f" || files+=("$f"); done \
|
||||
< <(find "$ADDON" -type f \( -name '*.sh' -o -name 'run' -o -name 'finish' -o -name 'autostart' \) 2> /dev/null)
|
||||
for f in "${files[@]}"; do
|
||||
if ! out=$(bash -n "$f" 2>&1); then note "bash -n" "FAIL $f"; echo "$out" | sed 's/^/ /'; fail=1; fi
|
||||
if ! out=$(bash -n "$f" 2>&1); then
|
||||
note "bash -n" "FAIL $f"
|
||||
echo "$out" | sed 's/^/ /'
|
||||
fail=1
|
||||
fi
|
||||
done
|
||||
[ "$fail" -eq 0 ] && note "bash -n" "${#files[@]} file(s) checked"
|
||||
|
||||
@@ -65,13 +82,16 @@ command -v hadolint > /dev/null 2>&1 && [ -f "$ADDON/Dockerfile" ] && {
|
||||
|
||||
if [ -f "$ADDON/config.yaml" ]; then
|
||||
# path passed as argv, never interpolated into Python source
|
||||
python3 - "$ADDON/config.yaml" <<'PY' || { note "config.yaml" "FAIL parse"; fail=1; }
|
||||
python3 - "$ADDON/config.yaml" << 'PY' || {
|
||||
import yaml,sys
|
||||
d=yaml.safe_load(open(sys.argv[1]))
|
||||
print(' %-13s ok (version=%s, %d options)' % ('config.yaml', d.get('version'), len(d.get('options') or {})))
|
||||
missing=[k for k in (d.get('options') or {}) if k not in (d.get('schema') or {})]
|
||||
if missing: print(' %-13s options with no schema entry: %s' % ('WARN', missing)); sys.exit(0)
|
||||
PY
|
||||
note "config.yaml" "FAIL parse"
|
||||
fail=1
|
||||
}
|
||||
command -v yamllint > /dev/null 2>&1 && {
|
||||
yl=$(yamllint -f parsable "$ADDON/config.yaml" 2>&1 | grep -c .)
|
||||
note "yamllint" "$yl finding(s) (compare with --vs-master)"
|
||||
@@ -79,7 +99,10 @@ PY
|
||||
fi
|
||||
|
||||
while IFS= read -r f; do
|
||||
python3 -m py_compile "$f" 2> /dev/null || { note "py_compile" "FAIL $f"; fail=1; }
|
||||
python3 -m py_compile "$f" 2> /dev/null || {
|
||||
note "py_compile" "FAIL $f"
|
||||
fail=1
|
||||
}
|
||||
done < <(find "$ADDON" -type f -name '*.py' 2> /dev/null)
|
||||
|
||||
$VS_MASTER && command -v npx > /dev/null 2>&1 && [ -f "$ADDON/CHANGELOG.md" ] && {
|
||||
@@ -96,7 +119,8 @@ if git diff --name-only origin/master...HEAD 2> /dev/null | grep -Fxq "$ADDON/CH
|
||||
note "CHANGELOG" "updated"
|
||||
else
|
||||
# This one IS gated: onpr_check-pr.yaml exits 1 without it.
|
||||
note "CHANGELOG" "NOT UPDATED for $ADDON — CI hard-gates this"; fail=1
|
||||
note "CHANGELOG" "NOT UPDATED for $ADDON — CI hard-gates this"
|
||||
fail=1
|
||||
fi
|
||||
if git diff origin/master...HEAD -- "$ADDON/config.yaml" 2> /dev/null | grep -q '^+version:'; then
|
||||
note "version" "bumped"
|
||||
@@ -110,7 +134,8 @@ note "docker build" "NOT tested locally (dockerd unavailable) — CI is the only
|
||||
if $VS_MASTER; then
|
||||
echo
|
||||
echo "== findings ADDED by this diff (pre-existing ones filtered out) =="
|
||||
tmp=$(mktemp -d); trap 'rm -rf "$tmp"' EXIT
|
||||
tmp=$(mktemp -d)
|
||||
trap 'rm -rf "$tmp"' EXIT
|
||||
added=0
|
||||
# Fed by process substitution, not a pipe: a pipeline runs this in a subshell, where the
|
||||
# findings below could never reach $fail and the verdict would contradict the list.
|
||||
@@ -125,14 +150,26 @@ if $VS_MASTER; then
|
||||
# cancel it out and report a false clean.
|
||||
case "$f" in
|
||||
*.sh | *autostart | */run | */finish)
|
||||
command -v shellcheck > /dev/null 2>&1 || { echo " $f: SKIPPED (shellcheck not installed)"; continue; }
|
||||
cmd() { shellcheck -x -f gcc "$1" 2>&1 | sed 's/^[^:]*:[0-9]*:[0-9]*://'; } ;;
|
||||
command -v shellcheck > /dev/null 2>&1 || {
|
||||
echo " $f: SKIPPED (shellcheck not installed)"
|
||||
continue
|
||||
}
|
||||
cmd() { shellcheck -x -f gcc "$1" 2>&1 | sed 's/^[^:]*:[0-9]*:[0-9]*://'; }
|
||||
;;
|
||||
*.yaml | *.yml)
|
||||
command -v yamllint > /dev/null 2>&1 || { echo " $f: SKIPPED (yamllint not installed)"; continue; }
|
||||
cmd() { yamllint -f parsable "$1" 2>&1 | sed 's/^[^:]*//; s/^:[0-9]*:[0-9]*//'; } ;;
|
||||
command -v yamllint > /dev/null 2>&1 || {
|
||||
echo " $f: SKIPPED (yamllint not installed)"
|
||||
continue
|
||||
}
|
||||
cmd() { yamllint -f parsable "$1" 2>&1 | sed 's/^[^:]*//; s/^:[0-9]*:[0-9]*//'; }
|
||||
;;
|
||||
*Dockerfile)
|
||||
command -v hadolint > /dev/null 2>&1 || { echo " $f: SKIPPED (hadolint not installed)"; continue; }
|
||||
cmd() { hadolint "$1" 2>&1 | sed 's/^[^:]*//; s/^:[0-9]*//'; } ;;
|
||||
command -v hadolint > /dev/null 2>&1 || {
|
||||
echo " $f: SKIPPED (hadolint not installed)"
|
||||
continue
|
||||
}
|
||||
cmd() { hadolint "$1" 2>&1 | sed 's/^[^:]*//; s/^:[0-9]*//'; }
|
||||
;;
|
||||
*) continue ;;
|
||||
esac
|
||||
b=$(cmd "$tmp/base" | sort)
|
||||
@@ -141,7 +178,8 @@ if $VS_MASTER; then
|
||||
[ -n "$new" ] && {
|
||||
echo " $f: $(printf '%s\n' "$new" | grep -c .) NEW finding(s)"
|
||||
printf '%s\n' "$new" | sed 's/^/ /' | head -5
|
||||
added=1; fail=1
|
||||
added=1
|
||||
fail=1
|
||||
}
|
||||
done < <(git diff --name-only origin/master...HEAD -- "$ADDON" 2> /dev/null)
|
||||
[ "$added" -eq 0 ] && echo " (none — this diff introduced no new lint findings)"
|
||||
|
||||
2
.github/prompts/issue-classify.md
vendored
2
.github/prompts/issue-classify.md
vendored
@@ -40,7 +40,7 @@ the check is a guard, not a common case.)
|
||||
## Rule 1 — pick exactly one verdict
|
||||
|
||||
| verdict | when |
|
||||
|---|---|
|
||||
| --- | --- |
|
||||
| `duplicate` | An existing open or closed issue reports the same thing. Set `duplicate_of`. |
|
||||
| `needs-info` | You cannot tell what is wrong without the add-on version, HA version, architecture, config, or the actual log output. |
|
||||
| `question` | A usage question answerable from `DOCS.md`, the wiki, or the add-on config. Not a defect. |
|
||||
|
||||
2
.github/prompts/issue-fix.md
vendored
2
.github/prompts/issue-fix.md
vendored
@@ -39,7 +39,7 @@ rest are on you: nothing checks them, so breaking one ships silently.
|
||||
that value, and compare:
|
||||
|
||||
| `version` vs `U` | what to do | example |
|
||||
|---|---|---|
|
||||
| --- | --- | --- |
|
||||
| identical | **append** `.1` | sonarr `4.0.19.3001` -> `4.0.19.3001.1` |
|
||||
| `U` + `.` + digits | **increment** those digits | radarr `6.3.0.10514.1` -> `6.3.0.10514.2` |
|
||||
| anything else | **leave it alone** | plex, readarr, joal |
|
||||
|
||||
2
.github/scripts/ai_guard_paths.sh
vendored
2
.github/scripts/ai_guard_paths.sh
vendored
@@ -44,7 +44,7 @@ for pr in $PRS; do
|
||||
echo "$BAD"
|
||||
# Ensure the label exists before adding it — with set -e a missing
|
||||
# label would abort the whole loop and skip any PRs behind this one.
|
||||
gh label create "ai:blocked" --repo "$REPO" --color ededed >/dev/null 2>&1 || true
|
||||
gh label create "ai:blocked" --repo "$REPO" --color ededed > /dev/null 2>&1 || true
|
||||
gh pr ready "$pr" --repo "$REPO" --undo || true
|
||||
gh pr edit "$pr" --repo "$REPO" --add-label "ai:blocked"
|
||||
gh pr comment "$pr" --repo "$REPO" --body \
|
||||
|
||||
22
.github/scripts/ai_triage_context.sh
vendored
22
.github/scripts/ai_triage_context.sh
vendored
@@ -21,14 +21,17 @@ TITLE=$(jq -r '.title' "$OUT/issue.json")
|
||||
|
||||
# ---------------------------------------------------------------- addon slug
|
||||
# Titles follow "🐛 [Immich Frame] ENV_VARS arent being picked up".
|
||||
RAW=$(sed -n 's/.*\[\([^]]*\)\].*/\1/p' <<<"$TITLE" | head -n1)
|
||||
RAW=$(sed -n 's/.*\[\([^]]*\)\].*/\1/p' <<< "$TITLE" | head -n1)
|
||||
ADDON=""
|
||||
if [ -n "$RAW" ]; then
|
||||
CAND=$(tr '[:upper:] ' '[:lower:]_' <<<"$RAW")
|
||||
CAND=$(tr '[:upper:] ' '[:lower:]_' <<< "$RAW")
|
||||
# Directory list without checking out any of them.
|
||||
git ls-tree -d --name-only HEAD > "$OUT/dirs.txt"
|
||||
for guess in "$CAND" "${CAND//_/-}" "${CAND//_/.}"; do
|
||||
if grep -qxF "$guess" "$OUT/dirs.txt"; then ADDON="$guess"; break; fi
|
||||
if grep -qxF "$guess" "$OUT/dirs.txt"; then
|
||||
ADDON="$guess"
|
||||
break
|
||||
fi
|
||||
done
|
||||
# Separator-insensitive exact match: a title like "[Calibre-web]" (hyphen)
|
||||
# against a directory named calibre_web (underscore) matches neither exact
|
||||
@@ -36,9 +39,12 @@ if [ -n "$RAW" ]; then
|
||||
# below, which picks the shorter "calibre" instead — the wrong add-on.
|
||||
# Stripping -, _, . from both sides before comparing catches this case.
|
||||
if [ -z "$ADDON" ]; then
|
||||
CAND_STRIPPED=$(tr -d '_.-' <<<"$CAND")
|
||||
CAND_STRIPPED=$(tr -d '_.-' <<< "$CAND")
|
||||
while IFS= read -r dir; do
|
||||
if [ "$(tr -d '_.-' <<<"$dir")" = "$CAND_STRIPPED" ]; then ADDON="$dir"; break; fi
|
||||
if [ "$(tr -d '_.-' <<< "$dir")" = "$CAND_STRIPPED" ]; then
|
||||
ADDON="$dir"
|
||||
break
|
||||
fi
|
||||
done < "$OUT/dirs.txt"
|
||||
fi
|
||||
# Last resort: longest directory name contained in the candidate.
|
||||
@@ -99,7 +105,7 @@ if [ -n "$ADDON" ]; then
|
||||
|
||||
echo
|
||||
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
|
||||
} >> "$CTX"
|
||||
fi
|
||||
@@ -108,14 +114,14 @@ fi
|
||||
{
|
||||
echo
|
||||
echo "## Similar existing issues (candidate duplicates)"
|
||||
KEYWORDS=$(tr -cs '[:alnum:]' ' ' <<<"$TITLE" \
|
||||
KEYWORDS=$(tr -cs '[:alnum:]' ' ' <<< "$TITLE" \
|
||||
| tr '[:upper:]' '[:lower:]' \
|
||||
| tr ' ' '\n' | awk 'length($0)>3' | head -n6 | paste -sd' ')
|
||||
# Excludes the issue being triaged: if it's already indexed by GitHub search
|
||||
# by the time this runs, keyword overlap with its own title would otherwise
|
||||
# list it as a "candidate duplicate" of itself.
|
||||
gh search issues --repo "$REPO" --limit 15 \
|
||||
--json number,title,state,url -- "$KEYWORDS" 2>/dev/null \
|
||||
--json number,title,state,url -- "$KEYWORDS" 2> /dev/null \
|
||||
| jq -r --argjson self "$ISSUE_NUMBER" \
|
||||
'.[] | select(.number != $self) | "- #\(.number) [\(.state)] \(.title)"' \
|
||||
|| echo "(search unavailable)"
|
||||
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -0,0 +1,2 @@
|
||||
# Bytecode of the python helpers in the addons, which rootfs/ copies into the image
|
||||
__pycache__/
|
||||
|
||||
@@ -6,7 +6,7 @@ set -e
|
||||
# Banner
|
||||
# ======================================================================
|
||||
|
||||
if ! bashio::supervisor.ping 2>/dev/null; then
|
||||
if ! bashio::supervisor.ping 2> /dev/null; then
|
||||
# Source standalone bashio first to provide function definitions
|
||||
if [ -f /usr/local/lib/bashio-standalone.sh ]; then
|
||||
source /usr/local/lib/bashio-standalone.sh
|
||||
@@ -19,8 +19,8 @@ if ! bashio::supervisor.ping 2>/dev/null; then
|
||||
if [ ! -f /usr/bin/bashio ]; then
|
||||
cp -rf /usr/local/lib/bashio-standalone.sh /usr/bin/bashio
|
||||
fi
|
||||
grep -rl "^#!.*bashio" /etc |
|
||||
while IFS= read -r f; do
|
||||
grep -rl "^#!.*bashio" /etc \
|
||||
| while IFS= read -r f; do
|
||||
grep -qF "source /usr/local/lib/bashio-standalone.sh" "$f" && continue
|
||||
sed -i '1a source /usr/local/lib/bashio-standalone.sh' "$f"
|
||||
done
|
||||
@@ -49,7 +49,7 @@ bashio::log.green ' Provided by: https://github.com/alexbelgium/hassio-addons '
|
||||
bashio::log.blue '-----------------------------------------------------------'
|
||||
|
||||
# Adapt user abc
|
||||
if command -v id &>/dev/null && id abc &>/dev/null; then
|
||||
if command -v id &> /dev/null && id abc &> /dev/null; then
|
||||
if bashio::config.has_value "PUID" && bashio::config.has_value "PGID"; then
|
||||
PUID="$(bashio::config "PUID")"
|
||||
PGID="$(bashio::config "PGID")"
|
||||
|
||||
@@ -152,15 +152,15 @@ if [[ "${1:-}" == "--self-test" ]]; then
|
||||
done
|
||||
fi
|
||||
|
||||
[[ "$self_test_rc" -eq 0 ]] &&
|
||||
echo "${#self_test_values[@]} values round-tripped unchanged (export block + dotenv)"
|
||||
[[ "$self_test_rc" -eq 0 ]] \
|
||||
&& echo "${#self_test_values[@]} values round-tripped unchanged (export block + dotenv)"
|
||||
exit "$self_test_rc"
|
||||
fi
|
||||
|
||||
################################################################################
|
||||
# Guard: only run inside Supervisor-managed add-ons
|
||||
################################################################################
|
||||
if ! bashio::supervisor.ping 2>/dev/null; then
|
||||
if ! bashio::supervisor.ping 2> /dev/null; then
|
||||
echo "..."
|
||||
exit 0
|
||||
fi
|
||||
@@ -179,7 +179,7 @@ ENV_FILE="/.env"
|
||||
ETC_ENV_FILE="/etc/environment"
|
||||
|
||||
[[ -f "$JSONSOURCE" ]] || bashio::exit.nok "Missing $JSONSOURCE"
|
||||
command -v jq >/dev/null || bashio::exit.nok "jq is required"
|
||||
command -v jq > /dev/null || bashio::exit.nok "jq is required"
|
||||
|
||||
mkdir -p /etc
|
||||
touch "$ETC_ENV_FILE"
|
||||
@@ -193,7 +193,10 @@ SECRETSOURCE=""
|
||||
|
||||
resolve_secret() {
|
||||
local v="$1" name line
|
||||
[[ "$v" =~ ^[[:space:]]*\!secret[[:space:]]+(.+)$ ]] || { printf '%s' "$v"; return; }
|
||||
[[ "$v" =~ ^[[:space:]]*\!secret[[:space:]]+(.+)$ ]] || {
|
||||
printf '%s' "$v"
|
||||
return
|
||||
}
|
||||
name="${BASH_REMATCH[1]}"
|
||||
[[ -n "$SECRETSOURCE" ]] || bashio::exit.nok "Secrets not mounted"
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
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"
|
||||
exit 0
|
||||
fi
|
||||
@@ -33,16 +33,16 @@ test_mount() {
|
||||
|
||||
_test_write() {
|
||||
local testfile="$mountpoint/.writetest_$$"
|
||||
if : >"$testfile" 2>/dev/null; then
|
||||
rm -f "$testfile" 2>/dev/null || true
|
||||
if : > "$testfile" 2> /dev/null; then
|
||||
rm -f "$testfile" 2> /dev/null || true
|
||||
return 0
|
||||
else
|
||||
rm -f "$testfile" 2>/dev/null || true
|
||||
rm -f "$testfile" 2> /dev/null || true
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
if _test_write 2>/dev/null; then
|
||||
if _test_write 2> /dev/null; then
|
||||
MOUNTED=true
|
||||
return 0
|
||||
fi
|
||||
@@ -53,9 +53,9 @@ test_mount() {
|
||||
echo "... retrying mount with noserverino"
|
||||
MOUNTOPTIONS="${orig_mountoptions},noserverino"
|
||||
|
||||
umount "$mountpoint" 2>/dev/null || true
|
||||
umount "$mountpoint" 2> /dev/null || true
|
||||
if mount_drive "$MOUNTOPTIONS"; then
|
||||
if _test_write 2>/dev/null; then
|
||||
if _test_write 2> /dev/null; then
|
||||
MOUNTED=true
|
||||
return 0
|
||||
fi
|
||||
@@ -66,9 +66,9 @@ test_mount() {
|
||||
echo "... retrying mount with noperm"
|
||||
MOUNTOPTIONS="${orig_mountoptions},noperm"
|
||||
|
||||
umount "$mountpoint" 2>/dev/null || true
|
||||
umount "$mountpoint" 2> /dev/null || true
|
||||
if mount_drive "$MOUNTOPTIONS"; then
|
||||
if _test_write 2>/dev/null; then
|
||||
if _test_write 2> /dev/null; then
|
||||
MOUNTED=true
|
||||
return 0
|
||||
fi
|
||||
@@ -88,9 +88,9 @@ mount_drive() {
|
||||
MOUNTOPTIONS="$1"
|
||||
|
||||
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
|
||||
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
|
||||
|
||||
if [[ "$MOUNTED" == "true" ]]; then
|
||||
@@ -106,7 +106,7 @@ retry_cifs_with_vers_ladder_on_dialect_failure() {
|
||||
|
||||
local err mountpoint
|
||||
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
|
||||
return 0
|
||||
@@ -132,7 +132,7 @@ retry_cifs_with_vers_ladder_on_dialect_failure() {
|
||||
for vers in "${vers_variants[@]}"; do
|
||||
for sectry in "${sec_variants[@]}"; do
|
||||
[[ "$MOUNTED" == "false" ]] || break
|
||||
umount "$mountpoint" 2>/dev/null || true
|
||||
umount "$mountpoint" 2> /dev/null || true
|
||||
try_opts="${base_opts}${vopt},vers=${vers}${sectry}"
|
||||
mount_drive "$try_opts"
|
||||
done
|
||||
@@ -154,7 +154,7 @@ retry_cifs_with_vers_ladder_on_dialect_failure() {
|
||||
for vers in "${vers_variants2[@]}"; do
|
||||
for sectry in "${sec_variants[@]}"; do
|
||||
[[ "$MOUNTED" == "false" ]] || break
|
||||
umount "$mountpoint" 2>/dev/null || true
|
||||
umount "$mountpoint" 2> /dev/null || true
|
||||
try_opts="${base_opts}${vopt},vers=${vers}${sectry}"
|
||||
mount_drive "$try_opts"
|
||||
done
|
||||
@@ -167,7 +167,7 @@ retry_cifs_with_vers_ladder_on_dialect_failure() {
|
||||
for vopt in "${opt_variants[@]}"; do
|
||||
for vers in "${vers_variants2[@]}"; do
|
||||
[[ "$MOUNTED" == "false" ]] || break
|
||||
umount "$mountpoint" 2>/dev/null || true
|
||||
umount "$mountpoint" 2> /dev/null || true
|
||||
try_opts="${base_opts}${vopt},vers=${vers},sec=ntlmssp"
|
||||
mount_drive "$try_opts"
|
||||
done
|
||||
@@ -246,13 +246,13 @@ if bashio::config.has_value 'networkdisks'; then
|
||||
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."
|
||||
continue
|
||||
fi
|
||||
|
||||
ERRORCODE_FILE="/tmp/mount_error_${diskname//[^a-zA-Z0-9._-]/_}.log"
|
||||
: >"$ERRORCODE_FILE" || true
|
||||
: > "$ERRORCODE_FILE" || true
|
||||
|
||||
MOUNTED=false
|
||||
CIFS_LADDER_ATTEMPTED=false
|
||||
@@ -266,13 +266,13 @@ if bashio::config.has_value 'networkdisks'; then
|
||||
if [[ "$FSTYPE" == "cifs" ]]; then
|
||||
if [[ ! "$disk" =~ ^//[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+/.+ ]]; then
|
||||
bashio::log.fatal "...... invalid CIFS path \"$disk\". Use //123.12.12.12/sharedfolder,//123.12.12.12/sharedfolder2"
|
||||
echo "Invalid CIFS path structure: $disk" >"$ERRORCODE_FILE" || true
|
||||
echo "Invalid CIFS path structure: $disk" > "$ERRORCODE_FILE" || true
|
||||
continue
|
||||
fi
|
||||
else
|
||||
if [[ ! "$disk" =~ ^[^/:]+:/.+ ]]; then
|
||||
bashio::log.fatal "...... invalid NFS path \"$disk\". Use server:/export/path or 123.12.12.12:/export/path"
|
||||
echo "Invalid NFS path structure: $disk" >"$ERRORCODE_FILE" || true
|
||||
echo "Invalid NFS path structure: $disk" > "$ERRORCODE_FILE" || true
|
||||
continue
|
||||
fi
|
||||
fi
|
||||
@@ -289,7 +289,7 @@ if bashio::config.has_value 'networkdisks'; then
|
||||
if [[ -n "${CIFSDOMAIN:-}" ]]; then
|
||||
printf 'domain=%s\n' "$CIFSDOMAIN"
|
||||
fi
|
||||
} >"$CRED_FILE"
|
||||
} > "$CRED_FILE"
|
||||
fi
|
||||
|
||||
if [[ "$FSTYPE" == "cifs" ]]; then
|
||||
@@ -303,8 +303,8 @@ if bashio::config.has_value 'networkdisks'; then
|
||||
|
||||
if [[ "$MOUNTED" == "false" ]]; then
|
||||
if [[ "$FSTYPE" == "cifs" ]]; then
|
||||
if command -v nmap >/dev/null 2>&1; then
|
||||
output="$(nmap -F "$server" -T5 -oG - 2>/dev/null || true)"
|
||||
if command -v nmap > /dev/null 2>&1; then
|
||||
output="$(nmap -F "$server" -T5 -oG - 2> /dev/null || true)"
|
||||
if ! echo "$output" | grep -q "445/open"; then
|
||||
if echo "$output" | grep -q "/open"; then
|
||||
bashio::log.fatal "...... $server is reachable but SMB port not opened, stopping script"
|
||||
@@ -312,7 +312,7 @@ if bashio::config.has_value 'networkdisks'; then
|
||||
bashio::log.fatal "...... fatal : $server not reachable, is it correct"
|
||||
fi
|
||||
cleanup_cred
|
||||
rm -f "$ERRORCODE_FILE" 2>/dev/null || true
|
||||
rm -f "$ERRORCODE_FILE" 2> /dev/null || true
|
||||
continue
|
||||
fi
|
||||
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"
|
||||
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)"
|
||||
|
||||
if echo "$OUTPUT" | grep -q "LOGON_FAILURE"; then
|
||||
bashio::log.fatal "...... incorrect Username, Password, or Domain! Script will stop."
|
||||
cleanup_cred
|
||||
rm -f "$ERRORCODE_FILE" 2>/dev/null || true
|
||||
rm -f "$ERRORCODE_FILE" 2> /dev/null || true
|
||||
bashio::addon.stop
|
||||
elif echo "$OUTPUT" | grep -q "tree connect failed" || echo "$OUTPUT" | grep -q "NT_STATUS_CONNECTION_DISCONNECTED"; then
|
||||
echo "... using SMBv1"
|
||||
@@ -344,8 +344,8 @@ if bashio::config.has_value 'networkdisks'; then
|
||||
|
||||
SMBRAW=""
|
||||
SMB1_DETECTED=false
|
||||
if command -v nmap >/dev/null 2>&1; then
|
||||
NMAP_OUTPUT="$(nmap --script smb-protocols -p 445 "$server" 2>/dev/null || true)"
|
||||
if command -v nmap > /dev/null 2>&1; then
|
||||
NMAP_OUTPUT="$(nmap --script smb-protocols -p 445 "$server" 2> /dev/null || true)"
|
||||
SMBRAW="$(
|
||||
echo "$NMAP_OUTPUT" \
|
||||
| awk '/SMB2_DIALECT_/ {print $NF}' \
|
||||
@@ -364,7 +364,7 @@ if bashio::config.has_value 'networkdisks'; then
|
||||
302) SMBVERS=",vers=3.02" ;;
|
||||
300) SMBVERS=",vers=3.0" ;;
|
||||
210) SMBVERS=",vers=2.1" ;;
|
||||
202|200) SMBVERS=",vers=2.0" ;;
|
||||
202 | 200) SMBVERS=",vers=2.0" ;;
|
||||
*) SMBVERS="" ;;
|
||||
esac
|
||||
|
||||
@@ -376,7 +376,7 @@ if bashio::config.has_value 'networkdisks'; then
|
||||
|
||||
if [[ -n "$SMBVERS" ]]; then
|
||||
echo "...... SMB version detected : ${SMBVERS#,vers=}"
|
||||
elif command -v smbclient >/dev/null 2>&1 && smbclient -t 2 -L "$server" -m NT1 -U "$USERNAME%$PASSWORD" $DOMAINCLIENT -c "exit" &>/dev/null; then
|
||||
elif command -v smbclient > /dev/null 2>&1 && smbclient -t 2 -L "$server" -m NT1 -U "$USERNAME%$PASSWORD" $DOMAINCLIENT -c "exit" &> /dev/null; then
|
||||
echo "...... SMB version : only SMBv1 is supported, this can lead to issues"
|
||||
SECVERS=",sec=ntlm"
|
||||
SMBVERS=",vers=1.0"
|
||||
@@ -405,8 +405,8 @@ if bashio::config.has_value 'networkdisks'; then
|
||||
fi
|
||||
|
||||
else
|
||||
if command -v nmap >/dev/null 2>&1; then
|
||||
output="$(nmap -F "$server" -T5 -oG - 2>/dev/null || true)"
|
||||
if command -v nmap > /dev/null 2>&1; then
|
||||
output="$(nmap -F "$server" -T5 -oG - 2> /dev/null || true)"
|
||||
if ! echo "$output" | grep -Eq '(2049|111)/open'; then
|
||||
bashio::log.fatal "...... $server is reachable but NFS ports not open"
|
||||
fi
|
||||
@@ -424,7 +424,7 @@ if bashio::config.has_value 'networkdisks'; then
|
||||
|
||||
if [[ "$MOUNTED" == "true" ]]; then
|
||||
bashio::log.info "...... $disk successfully mounted to /mnt/$diskname with options ${MOUNTOPTIONS/$PASSWORD/XXXXXXXXXX}"
|
||||
rm -f "$ERRORCODE_FILE" 2>/dev/null || true
|
||||
rm -f "$ERRORCODE_FILE" 2> /dev/null || true
|
||||
|
||||
if [[ "$FSTYPE" == "cifs" && "$MOUNTOPTIONS" == *"vers=1.0"* ]]; then
|
||||
bashio::log.warning ""
|
||||
@@ -437,14 +437,14 @@ if bashio::config.has_value 'networkdisks'; 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 "...... 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
|
||||
|
||||
else
|
||||
if [[ "$FSTYPE" == "cifs" ]]; then
|
||||
bashio::log.fatal "Error, unable to mount $disk to /mnt/$diskname with username $USERNAME. Please check share path, username/password/domain; try UID/GID 0."
|
||||
bashio::log.fatal "Here is some debugging info :"
|
||||
if command -v smbclient >/dev/null 2>&1; then
|
||||
if command -v smbclient > /dev/null 2>&1; then
|
||||
smbclient -t 2 -L "$server" -U "$USERNAME%$PASSWORD" -c "exit" $DOMAINCLIENT || true
|
||||
else
|
||||
bashio::log.warning "smbclient not available; cannot print SMB debugging info"
|
||||
@@ -459,13 +459,13 @@ if bashio::config.has_value 'networkdisks'; then
|
||||
mount_drive "rw"
|
||||
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"
|
||||
|
||||
umount "/mnt/$diskname" 2>/dev/null || true
|
||||
rmdir "/mnt/$diskname" 2>/dev/null || true
|
||||
umount "/mnt/$diskname" 2> /dev/null || true
|
||||
rmdir "/mnt/$diskname" 2> /dev/null || true
|
||||
cleanup_cred
|
||||
rm -f "$ERRORCODE_FILE" 2>/dev/null || true
|
||||
rm -f "$ERRORCODE_FILE" 2> /dev/null || true
|
||||
|
||||
bashio::addon.stop
|
||||
fi
|
||||
|
||||
@@ -138,9 +138,9 @@ append_unique_line() {
|
||||
# $1=file, $2=line
|
||||
local _file="$1"
|
||||
local _line="$2"
|
||||
mkdir -p "$(dirname "$_file")" 2>/dev/null || true
|
||||
touch "$_file" 2>/dev/null || true
|
||||
grep -qxF -- "$_line" "$_file" 2>/dev/null || echo "$_line" >> "$_file"
|
||||
mkdir -p "$(dirname "$_file")" 2> /dev/null || true
|
||||
touch "$_file" 2> /dev/null || true
|
||||
grep -qxF -- "$_line" "$_file" 2> /dev/null || echo "$_line" >> "$_file"
|
||||
}
|
||||
|
||||
while IFS= read -r line; do
|
||||
|
||||
@@ -11,14 +11,20 @@ pick_exec_dir() {
|
||||
for d in /dev/shm /run /var/run /mnt /root /; do
|
||||
if [ -d "$d" ] && [ -w "$d" ]; then
|
||||
local t="${d%/}/.exec_test_$$"
|
||||
printf '#!/bin/sh\necho ok\n' >"$t" 2>/dev/null || { rm -f "$t" 2>/dev/null || true; continue; }
|
||||
chmod 700 "$t" 2>/dev/null || { rm -f "$t" 2>/dev/null || true; continue; }
|
||||
if "$t" >/dev/null 2>&1; then
|
||||
rm -f "$t" 2>/dev/null || true
|
||||
printf '#!/bin/sh\necho ok\n' > "$t" 2> /dev/null || {
|
||||
rm -f "$t" 2> /dev/null || true
|
||||
continue
|
||||
}
|
||||
chmod 700 "$t" 2> /dev/null || {
|
||||
rm -f "$t" 2> /dev/null || true
|
||||
continue
|
||||
}
|
||||
if "$t" > /dev/null 2>&1; then
|
||||
rm -f "$t" 2> /dev/null || true
|
||||
echo "$d"
|
||||
return 0
|
||||
fi
|
||||
rm -f "$t" 2>/dev/null || true
|
||||
rm -f "$t" 2> /dev/null || true
|
||||
fi
|
||||
done
|
||||
return 1
|
||||
@@ -90,7 +96,7 @@ validate_shebang() {
|
||||
local errfile msg
|
||||
|
||||
# shellcheck disable=SC2206
|
||||
local cmd=( $candidate )
|
||||
local cmd=($candidate)
|
||||
local exe="${cmd[0]}"
|
||||
|
||||
if [ ! -x "$exe" ]; then
|
||||
@@ -103,18 +109,18 @@ validate_shebang() {
|
||||
{
|
||||
printf '#!%s\n' "$candidate"
|
||||
printf '%s\n' "$probe_script_content"
|
||||
} >"$tmp"
|
||||
chmod 700 "$tmp" 2>/dev/null || true
|
||||
} > "$tmp"
|
||||
chmod 700 "$tmp" 2> /dev/null || true
|
||||
|
||||
set +e
|
||||
out="$("$tmp" 2>"$errfile")"
|
||||
out="$("$tmp" 2> "$errfile")"
|
||||
rc=$?
|
||||
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
|
||||
rm -f "$errfile" 2>/dev/null || true
|
||||
rm -f "$errfile" 2> /dev/null || true
|
||||
return 0
|
||||
fi
|
||||
|
||||
@@ -126,7 +132,7 @@ validate_shebang() {
|
||||
msg+=$' stderr: <empty>\n'
|
||||
fi
|
||||
SHEBANG_ERRORS+=("$msg")
|
||||
rm -f "$errfile" 2>/dev/null || true
|
||||
rm -f "$errfile" 2> /dev/null || true
|
||||
return 1
|
||||
}
|
||||
|
||||
@@ -147,7 +153,7 @@ if [ -z "$shebang" ]; then
|
||||
exit 1
|
||||
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
|
||||
if [ -f "$f" ]; then
|
||||
# shellcheck disable=SC1090
|
||||
@@ -162,7 +168,7 @@ fi
|
||||
##################
|
||||
|
||||
# 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 "..."
|
||||
exit 0
|
||||
fi
|
||||
@@ -195,7 +201,7 @@ if [ ! -f "$CONFIGSOURCE" ]; then
|
||||
fi
|
||||
|
||||
# Convert scripts to linux
|
||||
dos2unix "$CONFIGSOURCE" >/dev/null 2>&1 || true
|
||||
dos2unix "$CONFIGSOURCE" > /dev/null 2>&1 || true
|
||||
chmod +x "$CONFIGSOURCE"
|
||||
|
||||
if grep -q 'bashio' "$CONFIGSOURCE"; then
|
||||
|
||||
@@ -46,7 +46,8 @@ _bashio_color() {
|
||||
}
|
||||
|
||||
_bashio_log() {
|
||||
local c="${1:-}"; shift || true
|
||||
local c="${1:-}"
|
||||
shift || true
|
||||
printf '%s%s%s\n' "$(_bashio_color "$c")" "$*" "$(_bashio_color reset)"
|
||||
}
|
||||
|
||||
@@ -55,7 +56,7 @@ _bashio_log() {
|
||||
# -----------------------------------------------------------------------------
|
||||
_bashio_is_true() {
|
||||
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 ;;
|
||||
esac
|
||||
}
|
||||
@@ -99,7 +100,7 @@ _bashio_env_get() {
|
||||
_bashio_env_has() {
|
||||
local key="${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.
|
||||
@@ -110,10 +111,10 @@ _bashio_json_get() {
|
||||
[ -n "$key" ] || return 0
|
||||
[ -n "$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
|
||||
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
|
||||
@@ -124,7 +125,7 @@ _bashio_tcp_wait() {
|
||||
local start now
|
||||
start="$(date +%s)"
|
||||
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<&-
|
||||
return 0
|
||||
fi
|
||||
@@ -138,10 +139,10 @@ _bashio_tcp_wait() {
|
||||
|
||||
# Prefer nc if present, fallback to /dev/tcp
|
||||
_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}"
|
||||
# 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
|
||||
}
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -173,7 +174,7 @@ bashio::supervisor.ping() {
|
||||
bashio::addon.name() { printf '%s' "${ADDON_NAME:-Standalone container}"; }
|
||||
bashio::addon.description() { printf '%s' "${ADDON_DESCRIPTION:-Running without Home Assistant Supervisor}"; }
|
||||
bashio::addon.version() { printf '%s' "${BUILD_VERSION:-1.0}"; }
|
||||
bashio::addon.version_latest(){ printf '%s' "${ADDON_VERSION_LATEST:-${BUILD_VERSION:-1.0}}"; }
|
||||
bashio::addon.version_latest() { printf '%s' "${ADDON_VERSION_LATEST:-${BUILD_VERSION:-1.0}}"; }
|
||||
|
||||
bashio::addon.update_available() {
|
||||
if [ -n "${ADDON_VERSION_LATEST:-}" ] && [ "${ADDON_VERSION_LATEST:-}" != "${BUILD_VERSION:-}" ]; then
|
||||
@@ -194,11 +195,11 @@ bashio::addon.port() {
|
||||
local arg="${1:-}"
|
||||
if [[ "$arg" =~ ^[0-9]+$ ]]; then
|
||||
local v=""
|
||||
v="$(_bashio_env_get "PORT_${arg}" 2>/dev/null || true)"
|
||||
[ -z "$v" ] && v="$(_bashio_env_get "ADDON_PORT_${arg}" 2>/dev/null || true)"
|
||||
v="$(_bashio_env_get "PORT_${arg}" 2> /dev/null || true)"
|
||||
[ -z "$v" ] && v="$(_bashio_env_get "ADDON_PORT_${arg}" 2> /dev/null || true)"
|
||||
printf '%s' "${v:-$arg}"
|
||||
else
|
||||
printf '%s' "$(_bashio_env_get "$arg" 2>/dev/null || true)"
|
||||
printf '%s' "$(_bashio_env_get "$arg" 2> /dev/null || true)"
|
||||
fi
|
||||
}
|
||||
|
||||
@@ -207,13 +208,13 @@ bashio::addon.option() {
|
||||
local key="${1:-}" value="${2-__BASHIO_UNSET__}" file="${STANDALONE_OPTIONS_JSON:-}"
|
||||
[ -n "$key" ] || return 0
|
||||
|
||||
if [ -n "$file" ] && [ -f "$file" ] && command -v jq >/dev/null 2>&1; then
|
||||
if [ -n "$file" ] && [ -f "$file" ] && command -v jq > /dev/null 2>&1; then
|
||||
local tmp
|
||||
tmp="$(mktemp)"
|
||||
if [ "$value" = "__BASHIO_UNSET__" ]; then
|
||||
jq --arg k "$key" 'delpath(($k|split(".")))' "$file" >"$tmp" && mv "$tmp" "$file"
|
||||
jq --arg k "$key" 'delpath(($k|split(".")))' "$file" > "$tmp" && mv "$tmp" "$file"
|
||||
else
|
||||
jq --arg k "$key" --arg v "$value" 'setpath(($k|split(".")); $v)' "$file" >"$tmp" && mv "$tmp" "$file"
|
||||
jq --arg k "$key" --arg v "$value" 'setpath(($k|split(".")); $v)' "$file" > "$tmp" && mv "$tmp" "$file"
|
||||
fi
|
||||
return 0
|
||||
fi
|
||||
@@ -238,7 +239,7 @@ bashio::info.operating_system() {
|
||||
}
|
||||
bashio::info.arch() { 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"; }
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -246,11 +247,14 @@ bashio::info.supervisor() { printf '%s' "standalone"; }
|
||||
# -----------------------------------------------------------------------------
|
||||
bashio::config() {
|
||||
local key="${1:-}"
|
||||
[ -n "$key" ] || { printf '%s' ""; return 0; }
|
||||
[ -n "$key" ] || {
|
||||
printf '%s' ""
|
||||
return 0
|
||||
}
|
||||
|
||||
local v=""
|
||||
if _bashio_env_get "$key" >/dev/null 2>&1; then
|
||||
v="$(_bashio_env_get "$key" 2>/dev/null || true)"
|
||||
if _bashio_env_get "$key" > /dev/null 2>&1; then
|
||||
v="$(_bashio_env_get "$key" 2> /dev/null || true)"
|
||||
fi
|
||||
[ -z "$v" ] && v="$(_bashio_json_get "$key")"
|
||||
printf '%s' "${v:-}"
|
||||
@@ -271,8 +275,8 @@ bashio::config.exists() {
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [ -n "$file" ] && [ -f "$file" ] && command -v jq >/dev/null 2>&1; then
|
||||
jq -e --arg k "$key" 'haspath(($k|split(".")))' "$file" >/dev/null 2>&1
|
||||
if [ -n "$file" ] && [ -f "$file" ] && command -v jq > /dev/null 2>&1; then
|
||||
jq -e --arg k "$key" 'haspath(($k|split(".")))' "$file" > /dev/null 2>&1
|
||||
return $?
|
||||
fi
|
||||
|
||||
@@ -293,8 +297,8 @@ bashio::config.array() {
|
||||
raw="$(bashio::config "$key")"
|
||||
[ -n "$raw" ] || return 0
|
||||
|
||||
if command -v jq >/dev/null 2>&1 && printf '%s' "$raw" | jq -e . >/dev/null 2>&1; then
|
||||
printf '%s' "$raw" | jq -r '.[]' 2>/dev/null && return 0
|
||||
if command -v jq > /dev/null 2>&1 && printf '%s' "$raw" | jq -e . > /dev/null 2>&1; then
|
||||
printf '%s' "$raw" | jq -r '.[]' 2> /dev/null && return 0
|
||||
fi
|
||||
|
||||
if printf '%s' "$raw" | grep -q ','; then
|
||||
@@ -319,7 +323,7 @@ bashio::fs.file_exists() { [ -f "${1:-}" ]; }
|
||||
bashio::fs.directory_exists() { [ -d "${1:-}" ]; }
|
||||
bashio::fs.file_contains() {
|
||||
local f="${1:-}" p="${2:-}"
|
||||
[ -f "$f" ] && grep -q -- "$p" "$f" 2>/dev/null
|
||||
[ -f "$f" ] && grep -q -- "$p" "$f" 2> /dev/null
|
||||
}
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -336,16 +340,16 @@ bashio::net.wait_for() {
|
||||
bashio::dns.host() {
|
||||
local h="${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}'
|
||||
else
|
||||
nslookup "$h" 2>/dev/null | awk '/^Address: /{print $2; exit}'
|
||||
nslookup "$h" 2> /dev/null | awk '/^Address: /{print $2; exit}'
|
||||
fi
|
||||
}
|
||||
|
||||
# Hostname
|
||||
bashio::host.hostname() {
|
||||
command -v hostname >/dev/null 2>&1 && hostname || printf '%s' "${HOSTNAME:-unknown}"
|
||||
command -v hostname > /dev/null 2>&1 && hostname || printf '%s' "${HOSTNAME:-unknown}"
|
||||
}
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -356,7 +360,10 @@ bashio::host.hostname() {
|
||||
# bashio::services.available "mqtt"
|
||||
bashio::services() {
|
||||
local svc="${1:-}" key="${2:-}"
|
||||
[ -n "$svc" ] && [ -n "$key" ] || { printf '%s' ""; return 0; }
|
||||
[ -n "$svc" ] && [ -n "$key" ] || {
|
||||
printf '%s' ""
|
||||
return 0
|
||||
}
|
||||
|
||||
local upper svc_upper var v=""
|
||||
upper="$(printf '%s' "$key" | tr '[:lower:]' '[:upper:]')"
|
||||
@@ -369,15 +376,15 @@ bashio::services() {
|
||||
MQTT:USERNAME) var="MQTT_USER" ;;
|
||||
MQTT:PASSWORD) var="MQTT_PASSWORD" ;;
|
||||
MQTT:TLS) var="MQTT_TLS" ;;
|
||||
MYSQL:HOST|MARIADB:HOST) var="DB_HOST" ;;
|
||||
MYSQL:PORT|MARIADB:PORT) var="DB_PORT" ;;
|
||||
MYSQL:USERNAME|MARIADB:USERNAME) var="DB_USER" ;;
|
||||
MYSQL:PASSWORD|MARIADB:PASSWORD) var="DB_PASSWORD" ;;
|
||||
MYSQL:DATABASE|MARIADB:DATABASE) var="DB_NAME" ;;
|
||||
MYSQL:HOST | MARIADB:HOST) var="DB_HOST" ;;
|
||||
MYSQL:PORT | MARIADB:PORT) var="DB_PORT" ;;
|
||||
MYSQL:USERNAME | MARIADB:USERNAME) var="DB_USER" ;;
|
||||
MYSQL:PASSWORD | MARIADB:PASSWORD) var="DB_PASSWORD" ;;
|
||||
MYSQL:DATABASE | MARIADB:DATABASE) var="DB_NAME" ;;
|
||||
*) var="${svc_upper}_${upper}" ;;
|
||||
esac
|
||||
|
||||
v="$(_bashio_env_get "$var" 2>/dev/null || true)"
|
||||
v="$(_bashio_env_get "$var" 2> /dev/null || true)"
|
||||
if [ -z "$v" ]; then
|
||||
v="$(_bashio_json_get "services.${svc}.${key}")"
|
||||
[ -z "$v" ] && v="$(_bashio_json_get "${svc}.${key}")"
|
||||
@@ -402,15 +409,15 @@ bashio::cache.set() { printf '%s' "${2:-}" > "$BASHIO_CACHE_DIR/${1}.cache";
|
||||
# -----------------------------------------------------------------------------
|
||||
# jq wrapper (some add-ons call bashio::jq)
|
||||
# -----------------------------------------------------------------------------
|
||||
bashio::jq() { command -v jq >/dev/null 2>&1 && jq "$@"; }
|
||||
bashio::jq() { command -v jq > /dev/null 2>&1 && jq "$@"; }
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Home Assistant token
|
||||
# -----------------------------------------------------------------------------
|
||||
bashio::homeassistant.token() {
|
||||
local t="${HOMEASSISTANT_TOKEN:-${HASS_TOKEN:-}}"
|
||||
if [ -z "$t" ] && [ -n "${STANDALONE_OPTIONS_JSON:-}" ] && [ -f "${STANDALONE_OPTIONS_JSON:-}" ] && command -v jq >/dev/null 2>&1; then
|
||||
t="$(jq -er '.homeassistant.token // empty' "$STANDALONE_OPTIONS_JSON" 2>/dev/null || true)"
|
||||
if [ -z "$t" ] && [ -n "${STANDALONE_OPTIONS_JSON:-}" ] && [ -f "${STANDALONE_OPTIONS_JSON:-}" ] && command -v jq > /dev/null 2>&1; then
|
||||
t="$(jq -er '.homeassistant.token // empty' "$STANDALONE_OPTIONS_JSON" 2> /dev/null || true)"
|
||||
fi
|
||||
printf '%s' "${t:-}"
|
||||
}
|
||||
@@ -419,7 +426,11 @@ bashio::homeassistant.token() {
|
||||
# Exit helpers
|
||||
# -----------------------------------------------------------------------------
|
||||
bashio::exit.ok() { exit 0; }
|
||||
bashio::exit.nok() { local m="${1:-}"; [ -n "$m" ] && bashio::log.red "$m"; exit 1; }
|
||||
bashio::exit.nok() {
|
||||
local m="${1:-}"
|
||||
[ -n "$m" ] && bashio::log.red "$m"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Core config check shim
|
||||
|
||||
@@ -41,14 +41,14 @@ restore_apt_sources_if_missing() {
|
||||
mirror="http://ports.ubuntu.com/ubuntu-ports"
|
||||
security_mirror="$mirror"
|
||||
fi
|
||||
cat > /etc/apt/sources.list <<EOF2
|
||||
cat > /etc/apt/sources.list << EOF2
|
||||
deb ${mirror} ${codename} main restricted universe multiverse
|
||||
deb ${mirror} ${codename}-updates main restricted universe multiverse
|
||||
deb ${security_mirror} ${codename}-security main restricted universe multiverse
|
||||
EOF2
|
||||
;;
|
||||
debian)
|
||||
cat > /etc/apt/sources.list <<EOF2
|
||||
cat > /etc/apt/sources.list << EOF2
|
||||
deb http://deb.debian.org/debian ${codename} main contrib non-free non-free-firmware
|
||||
deb http://deb.debian.org/debian ${codename}-updates main contrib non-free non-free-firmware
|
||||
deb http://deb.debian.org/debian-security ${codename}-security main contrib non-free non-free-firmware
|
||||
|
||||
@@ -184,7 +184,7 @@ done
|
||||
if [ "$PACKMANAGER" = "apt" ]; then apt-get update > /dev/null; fi
|
||||
if [ "$PACKMANAGER" = "pacman" ]; then pacman -Sy > /dev/null; fi
|
||||
if [ "$PACKMANAGER" = "apk" ] && [ -f /etc/apk/repositories ] && ! grep -q "community" /etc/apk/repositories; then
|
||||
ALPINE_VER=$(cat /etc/alpine-release 2>/dev/null | cut -d. -f1,2)
|
||||
ALPINE_VER=$(cat /etc/alpine-release 2> /dev/null | cut -d. -f1,2)
|
||||
if [ -n "$ALPINE_VER" ]; then
|
||||
echo "https://dl-cdn.alpinelinux.org/alpine/v${ALPINE_VER}/community" >> /etc/apk/repositories
|
||||
apk update > /dev/null
|
||||
@@ -228,7 +228,7 @@ for files in "/etc/services.d" "/etc/cont-init.d"; do
|
||||
if ! ls $files 1> /dev/null 2>&1; then continue; fi
|
||||
|
||||
# Bashio
|
||||
if grep -q -rnw "${files}" -e 'bashio' 2>/dev/null && [ ! -f "/usr/bin/bashio" ]; then
|
||||
if grep -q -rnw "${files}" -e 'bashio' 2> /dev/null && [ ! -f "/usr/bin/bashio" ]; then
|
||||
[ "$VERBOSE" = true ] && echo "install bashio"
|
||||
BASHIO_VERSION="v0.17.5"
|
||||
mkdir -p /tmp/bashio
|
||||
|
||||
@@ -10,7 +10,7 @@ if [ "$$" -eq 1 ]; then
|
||||
PID1=true
|
||||
echo "Starting as entrypoint"
|
||||
if [ -d /command ]; then
|
||||
ln -sf /command/* /usr/bin/ 2>/dev/null || true
|
||||
ln -sf /command/* /usr/bin/ 2> /dev/null || true
|
||||
fi
|
||||
else
|
||||
echo "Starting custom scripts"
|
||||
@@ -37,19 +37,19 @@ terminate() {
|
||||
# fails whenever the stop arrives before the database is up.
|
||||
set +e
|
||||
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
|
||||
[ -n "$pid" ] || continue
|
||||
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)
|
||||
else
|
||||
for p in /proc/[0-9]*/; do
|
||||
local_pid="${p#/proc/}"
|
||||
local_pid="${local_pid%/}"
|
||||
if [ "$local_pid" -ne 1 ] && grep -q "^PPid:[[:space:]]*$$" "/proc/$local_pid/status" 2>/dev/null; then
|
||||
if [ "$local_pid" -ne 1 ] && grep -q "^PPid:[[:space:]]*$$" "/proc/$local_pid/status" 2> /dev/null; then
|
||||
echo "Terminating child PID $local_pid"
|
||||
kill -TERM "$local_pid" 2>/dev/null || echo "Failed to terminate PID $local_pid"
|
||||
kill -TERM "$local_pid" 2> /dev/null || echo "Failed to terminate PID $local_pid"
|
||||
fi
|
||||
done
|
||||
fi
|
||||
@@ -76,14 +76,20 @@ pick_exec_dir() {
|
||||
if [ -d "$d" ] && [ -w "$d" ]; then
|
||||
# Create a tiny test executable to confirm "exec" works
|
||||
local t="${d%/}/.exec_test_$$"
|
||||
printf '#!/bin/sh\necho ok\n' >"$t" 2>/dev/null || { rm -f "$t" 2>/dev/null || true; continue; }
|
||||
chmod 700 "$t" 2>/dev/null || { rm -f "$t" 2>/dev/null || true; continue; }
|
||||
if "$t" >/dev/null 2>&1; then
|
||||
rm -f "$t" 2>/dev/null || true
|
||||
printf '#!/bin/sh\necho ok\n' > "$t" 2> /dev/null || {
|
||||
rm -f "$t" 2> /dev/null || true
|
||||
continue
|
||||
}
|
||||
chmod 700 "$t" 2> /dev/null || {
|
||||
rm -f "$t" 2> /dev/null || true
|
||||
continue
|
||||
}
|
||||
if "$t" > /dev/null 2>&1; then
|
||||
rm -f "$t" 2> /dev/null || true
|
||||
echo "$d"
|
||||
return 0
|
||||
fi
|
||||
rm -f "$t" 2>/dev/null || true
|
||||
rm -f "$t" 2> /dev/null || true
|
||||
fi
|
||||
done
|
||||
return 1
|
||||
@@ -157,7 +163,7 @@ validate_shebang() {
|
||||
local errfile msg
|
||||
|
||||
# shellcheck disable=SC2206
|
||||
local cmd=( $candidate )
|
||||
local cmd=($candidate)
|
||||
local exe="${cmd[0]}"
|
||||
|
||||
if [ ! -x "$exe" ]; then
|
||||
@@ -170,18 +176,18 @@ validate_shebang() {
|
||||
{
|
||||
printf '#!%s\n' "$candidate"
|
||||
printf '%s\n' "$probe_script_content"
|
||||
} >"$tmp"
|
||||
chmod 700 "$tmp" 2>/dev/null || true
|
||||
} > "$tmp"
|
||||
chmod 700 "$tmp" 2> /dev/null || true
|
||||
|
||||
set +e
|
||||
out="$("$tmp" 2>"$errfile")"
|
||||
out="$("$tmp" 2> "$errfile")"
|
||||
rc=$?
|
||||
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
|
||||
rm -f "$errfile" 2>/dev/null || true
|
||||
rm -f "$errfile" 2> /dev/null || true
|
||||
return 0
|
||||
fi
|
||||
|
||||
@@ -193,7 +199,7 @@ validate_shebang() {
|
||||
msg+=$' stderr: <empty>\n'
|
||||
fi
|
||||
SHEBANG_ERRORS+=("$msg")
|
||||
rm -f "$errfile" 2>/dev/null || true
|
||||
rm -f "$errfile" 2> /dev/null || true
|
||||
return 1
|
||||
}
|
||||
|
||||
@@ -257,14 +263,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
|
||||
# a symlink, but it would empty anything bind-mounted at this exact path -- not a configuration
|
||||
# any add-on uses, and not one s6 would tolerate either.
|
||||
if rm -rf "$S6_CONTAINER_ENV" "$S6_CONTAINER_ENV.tmp" && mkdir -p "$S6_CONTAINER_ENV.tmp" &&
|
||||
s6-dumpenv -- "$S6_CONTAINER_ENV.tmp" && mv "$S6_CONTAINER_ENV.tmp" "$S6_CONTAINER_ENV"; then
|
||||
if rm -rf "$S6_CONTAINER_ENV" "$S6_CONTAINER_ENV.tmp" && mkdir -p "$S6_CONTAINER_ENV.tmp" \
|
||||
&& s6-dumpenv -- "$S6_CONTAINER_ENV.tmp" && mv "$S6_CONTAINER_ENV.tmp" "$S6_CONTAINER_ENV"; then
|
||||
echo "Populated $S6_CONTAINER_ENV for with-contenv"
|
||||
else
|
||||
# Leaves the directory absent, which is exactly how this fails today -- so the failure mode is
|
||||
# unchanged, not newly degraded. Never fatal, a read-only /run must still let the add-on boot,
|
||||
# but never silent either, since the shebang failure it leaves behind says nothing on its own.
|
||||
rm -rf "$S6_CONTAINER_ENV" "$S6_CONTAINER_ENV.tmp" 2>/dev/null || true
|
||||
rm -rf "$S6_CONTAINER_ENV" "$S6_CONTAINER_ENV.tmp" 2> /dev/null || true
|
||||
echo -e "\e[38;5;214m$(date) WARNING: could not populate $S6_CONTAINER_ENV; scripts with a with-contenv shebang will fail at their shebang, as they did before this was attempted\e[0m"
|
||||
fi
|
||||
fi
|
||||
@@ -325,7 +331,7 @@ wait_for_supervisor() {
|
||||
[ -n "${SUPERVISOR_TOKEN:-}" ] || return 0
|
||||
[ "${BASHIO_LIB_FULL:-false}" = "true" ] || return 0
|
||||
# bashio's own curl carries no --max-time, so each attempt is bounded from the outside.
|
||||
command -v timeout >/dev/null 2>&1 || return 0
|
||||
command -v timeout > /dev/null 2>&1 || return 0
|
||||
# Digits only, then forced to base 10: `test -gt` accepts a zero-padded override like 08, but
|
||||
# arithmetic expansion reads it as octal and fails, which would leave the deadline empty and
|
||||
# spin the loop below forever.
|
||||
@@ -353,7 +359,7 @@ wait_for_supervisor() {
|
||||
# logging is dropped because a failed attempt here is expected, not news.
|
||||
# shellcheck disable=SC2016
|
||||
if timeout "$attempt" bash -c '. "$1" && [ -n "$(bashio::addon.ip_address)" ]' \
|
||||
_ "$BASHIO_LIB" >/dev/null 2>&1; then
|
||||
_ "$BASHIO_LIB" > /dev/null 2>&1; then
|
||||
[ "$announced" -eq 0 ] || echo "Supervisor API ready after $((SECONDS - started))s"
|
||||
return 0
|
||||
fi
|
||||
@@ -405,7 +411,7 @@ run_one_script() {
|
||||
_run_rc=0
|
||||
(
|
||||
# shellcheck disable=SC1090
|
||||
. "$BASHIO_LIB" 2>/dev/null || true
|
||||
. "$BASHIO_LIB" 2> /dev/null || true
|
||||
# shellcheck disable=SC1090
|
||||
. "$script"
|
||||
) || _run_rc=$?
|
||||
@@ -445,7 +451,7 @@ if $PID1; then
|
||||
_svc_rc=0
|
||||
(
|
||||
# shellcheck disable=SC1090
|
||||
. "$BASHIO_LIB" 2>/dev/null || true
|
||||
. "$BASHIO_LIB" 2> /dev/null || true
|
||||
# shellcheck disable=SC1090
|
||||
. "$runfile"
|
||||
) || _svc_rc=$?
|
||||
|
||||
@@ -147,7 +147,7 @@ manual actions with precedence. Prompts live in `.github/prompts/`, shared shell
|
||||
in `.github/scripts/`.
|
||||
|
||||
| Workflow | Model | Trigger | Role |
|
||||
|---|---|---|---|
|
||||
| --- | --- | --- | --- |
|
||||
| `on_issues_ai_triage.yaml` | Sonnet-low | issue opened (+ author reply, daily catch-up) | Tier 1: classify, dedupe, answer, ask for info; label `ai-triage` for real add-on bugs |
|
||||
| `daily_ai_fix.yaml` | Opus 5-xhigh | daily 03:00 | Tier 2: diagnose the `ai-triage` batch; small+confident → ready PR (`ai:fixed`); else write a plan (`ai:plan-pending`) |
|
||||
| `on_issue_approved.yaml` | Opus 5-high | maintainer adds `ai:approved` | Tier 3: execute the approved plan → ready PR |
|
||||
@@ -176,7 +176,7 @@ prompt-only, not machine-enforced.
|
||||
## Linting Rules
|
||||
|
||||
| Tool | Config | Key ignores |
|
||||
|------|--------|------------|
|
||||
| ------ | -------- | ------------ |
|
||||
| Hadolint | `.hadolint.yaml` | DL3002, DL3006-9, DL3018 (no pinning required) |
|
||||
| ShellCheck | `.shellcheckrc` | SC2002 |
|
||||
| Markdownlint | `.markdownlint.yaml` | MD013 (line length), MD025, MD033, MD041 |
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
- Migrate legacy add-on configuration map names to current app configuration terminology.
|
||||
## 2026.09 (2026-09-12)
|
||||
|
||||
- New `source: container` for addons that build `FROM` an image published by someone else. The version is read from the `org.opencontainers.image.version` label of the exact image reference in `updater.json`, instead of from the releases of the application's own repository. Written against `ghcr.io`; an image with no such label leaves the addon untouched
|
||||
- `source: container` also records the manifest digest in `upstream_digest` and rebuilds when it moves, so a publisher republishing the same version under a new image is no longer invisible
|
||||
## 2026.08 (2026-08-01)
|
||||
|
||||
- Addon versions written in config.yaml now always comply with Home Assistant versioning: an upstream tag Home Assistant cannot order (`version-bf9e0b4f`, `ubuntu-2026-06-01`, ...) or would sort as older (`1.2.3-2`, `1.2.3+4`) no longer lands in config.yaml. The addon number is incremented instead, while the raw upstream tag stays in updater.json so the same release is never published twice
|
||||
|
||||
@@ -67,15 +67,49 @@ You can add the following tags in the file :
|
||||
- repository: 'name/repo' coming from github
|
||||
- paused: true # Pauses the updates
|
||||
- slug: the slug name from your addon
|
||||
- source: dockerhub/github,gitlab,bitbucket,pip,hg,sf,website-feed,local,helm_chart,wiki,system,wp,codeberg (Codeberg is supported via its Gitea API, which is configured automatically)
|
||||
- upstream_repo: name/repo, example is 'linuxserver/docker-emby'
|
||||
- source: container/dockerhub/github,gitlab,bitbucket,pip,hg,sf,website-feed,local,helm_chart,wiki,system,wp,codeberg (Codeberg is supported via its Gitea API, which is configured automatically)
|
||||
- upstream_repo: name/repo, example is 'linuxserver/docker-emby'. With `source: container` it is instead a full image reference, example is 'ghcr.io/imagegenius/immich:3'
|
||||
- upstream_version: automatically populated, corresponds to the current upstream version referenced in the addon
|
||||
- dockerhub_by_date: in dockerhub, uses the last_update date instead of the version
|
||||
- dockerhub_list_size: in dockerhub, how many containers to consider for latest version
|
||||
|
||||
### Addons built on someone else's image
|
||||
|
||||
An addon that does not build the application itself, but builds `FROM` an image a third party
|
||||
publishes, must publish the version that image holds and not the newest release of the
|
||||
application. `ghcr.io/imagegenius/immich:3` is a rebuild of immich lagging its upstream by days
|
||||
to weeks: tracking `immich-app/immich` published a version the addon did not contain, and left
|
||||
the addon with no reason to rebuild once the image finally caught up.
|
||||
|
||||
`source: container` reads the version out of the image itself. Set `upstream_repo` to the exact
|
||||
image reference used in `build.json`, tag included:
|
||||
|
||||
```json
|
||||
{
|
||||
"source": "container",
|
||||
"upstream_repo": "ghcr.io/imagegenius/immich:3"
|
||||
}
|
||||
```
|
||||
|
||||
The version comes from the image's `org.opencontainers.image.version` label, read from the first
|
||||
linux image the tag publishes, and the addon is left alone when there is no label to read. Written
|
||||
against `ghcr.io`, which serves anonymous pull tokens from `https://ghcr.io/token`; a registry
|
||||
authenticating differently, such as Docker Hub, yields no version and the addon is skipped.
|
||||
|
||||
The manifest digest is recorded next to the version, in `upstream_digest`, and the addon is
|
||||
rebuilt when either of the two moves. A publisher rebuilding the same version, for a base image
|
||||
security fix or a packaging revision, repoints the tag at new content under an unchanged label,
|
||||
and the digest is the only thing that says so. `upstream_digest` is populated automatically; seed
|
||||
it by hand when migrating an addon to this source, or the first run counts the unknown digest as
|
||||
a change and rebuilds once for nothing.
|
||||
|
||||
The label is metadata the publisher chooses. Some images carry none, and some record their own
|
||||
packaging revision rather than the application version, so this source is opt-in per addon and
|
||||
never a default.
|
||||
|
||||
### 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:
|
||||
|
||||
|
||||
@@ -30,4 +30,4 @@ schema:
|
||||
slug: updater
|
||||
udev: true
|
||||
url: https://github.com/alexbelgium/hassio-addons/tree/master/addons_updater
|
||||
version: "2026.08"
|
||||
version: "2026.09"
|
||||
|
||||
@@ -109,6 +109,7 @@ for f in */; do
|
||||
EXCLUDE_TEXT="${EXCLUDE_TEXT:-zzzzzzzzzzzzzzzz}"
|
||||
PAUSED=$(jq -r .paused updater.json)
|
||||
DATE="$(date "$DATE_FORMAT")"
|
||||
LASTDIGEST=""
|
||||
BYDATE=$(jq -r .dockerhub_by_date updater.json)
|
||||
|
||||
# Number of elements to check in dockerhub
|
||||
@@ -195,6 +196,45 @@ for f in */; do
|
||||
&& LASTVERSION="$LASTVERSION-$DATE"
|
||||
LOGINFO="... $SLUG : bydate is true, version is $LASTVERSION" && if [ "$VERBOSE" = true ]; then bashio::log.info "$LOGINFO"; fi
|
||||
|
||||
elif [[ "$SOURCE" = container ]]; then
|
||||
# The addon builds FROM an image someone else publishes rather
|
||||
# than from the application itself, so the version to publish is
|
||||
# the one inside that image and not the newest release of the
|
||||
# application repository, which the image can lag by weeks
|
||||
LOGINFO="... Source is container" && if [ "$VERBOSE" = true ]; then bashio::log.info "$LOGINFO"; fi
|
||||
|
||||
ACCEPT="application/vnd.oci.image.index.v1+json,application/vnd.docker.distribution.manifest.list.v2+json,application/vnd.oci.image.manifest.v1+json,application/vnd.docker.distribution.manifest.v2+json"
|
||||
REGISTRY="${UPSTREAM%%/*}"
|
||||
IMAGE="${UPSTREAM#*/}"
|
||||
IMAGEREPO="${IMAGE%:*}"
|
||||
BASEURL="https://$REGISTRY/v2/$IMAGEREPO"
|
||||
HEADERS="$(mktemp)"
|
||||
|
||||
# The digest the tag resolves to is the one the registry answers
|
||||
# with, and it moves whenever anything behind the tag does
|
||||
TOKEN="$(curl -f -L -s "https://$REGISTRY/token?scope=repository:$IMAGEREPO:pull&service=$REGISTRY" | jq -r '.token // empty')" || true
|
||||
MANIFEST="$(curl -f -L -s -D "$HEADERS" -H "Authorization: Bearer $TOKEN" -H "Accept: $ACCEPT" "$BASEURL/manifests/${IMAGE##*:}")" || true
|
||||
LASTDIGEST="$(sed -n 's/^[Dd]ocker-[Cc]ontent-[Dd]igest:[[:space:]]*//p' "$HEADERS" | tr -d '\r')"
|
||||
rm -f "$HEADERS"
|
||||
|
||||
# A multi architecture tag answers with an index, whose first
|
||||
# image is read: the attestations buildx attaches alongside
|
||||
# describe no architecture and hold no image configuration
|
||||
CHILD="$(echo "$MANIFEST" | jq -r 'first(.manifests[]? | select(.platform.os == "linux" and .platform.architecture != "unknown") | .digest) // empty')" || true
|
||||
if [ -n "$CHILD" ]; then
|
||||
MANIFEST="$(curl -f -L -s -H "Authorization: Bearer $TOKEN" -H "Accept: $ACCEPT" "$BASEURL/manifests/$CHILD")" || true
|
||||
fi
|
||||
|
||||
# Publishers record the application version of an image in its
|
||||
# OCI label, which is what the addon actually ships
|
||||
LASTVERSION="$(curl -f -L -s -H "Authorization: Bearer $TOKEN" "$BASEURL/blobs/$(echo "$MANIFEST" | jq -r '.config.digest // empty')" \
|
||||
| jq -r '.config.Labels["org.opencontainers.image.version"] // empty')" || true
|
||||
|
||||
if [ -z "$LASTVERSION" ] || [ -z "$LASTDIGEST" ]; then
|
||||
bashio::log.warning "... $SLUG : no version could be read from $UPSTREAM, skipping"
|
||||
continue
|
||||
fi
|
||||
|
||||
else
|
||||
|
||||
# Use source as upstream
|
||||
@@ -344,8 +384,19 @@ for f in */; do
|
||||
continue
|
||||
fi
|
||||
|
||||
# What the addon was last built against. A container source adds the
|
||||
# digest of the image: a publisher rebuilding the same version, for a
|
||||
# base image fix or a packaging revision, repoints the tag at new
|
||||
# content under an unchanged label, and only the digest says so
|
||||
CURRENTSTATE="$CURRENT"
|
||||
LASTSTATE="$LASTVERSION"
|
||||
if [[ "$SOURCE" = container ]]; then
|
||||
CURRENTSTATE="$CURRENT $(jq -r '.upstream_digest // ""' updater.json)"
|
||||
LASTSTATE="$LASTVERSION $LASTDIGEST"
|
||||
fi
|
||||
|
||||
# Update if needed
|
||||
if [ "${CURRENT}" != "${LASTVERSION}" ]; then
|
||||
if [ "${CURRENTSTATE}" != "${LASTSTATE}" ]; then
|
||||
LOGINFO="... $SLUG : update from ${CURRENT} to ${LASTVERSION}" && if [ "$VERBOSE" = true ]; then bashio::log.info "$LOGINFO"; fi
|
||||
|
||||
ADDONFOLDER="/data/${BASENAME}/${SLUG}"
|
||||
@@ -405,7 +456,9 @@ for f in */; do
|
||||
|
||||
# Replace upstream tag and date, keeping the file intact if jq
|
||||
# fails as a truncated updater.json would lose the addon source
|
||||
if ! UPDATERJSON="$(jq --arg version "$LASTVERSION" --arg date "$DATE" '.upstream_version = $version | .last_update = $date' "$ADDONFOLDER/updater.json")"; then
|
||||
if ! UPDATERJSON="$(jq --arg version "$LASTVERSION" --arg date "$DATE" --arg digest "${LASTDIGEST:-}" \
|
||||
'.upstream_version = $version | .last_update = $date | if $digest == "" then . else .upstream_digest = $digest end' \
|
||||
"$ADDONFOLDER/updater.json")"; then
|
||||
bashio::log.error "... $SLUG : updater.json could not be updated, reverting"
|
||||
git checkout -- "$ADDONFOLDER"
|
||||
continue
|
||||
|
||||
@@ -6,7 +6,7 @@ This addon is based on the docker image <https://github.com/lklynet/aurral>
|
||||
## Configuration
|
||||
|
||||
| Option | Default | Description |
|
||||
|---|---|---|
|
||||
| --- | --- | --- |
|
||||
| `download_folder` | `/share/aurral/downloads` | Path where Aurral writes flow downloads. Must be under `/share`. |
|
||||
| `weekly_flow_folder` | `weekly-flow` | Subfolder name appended to `download_folder` for weekly flow files. The full path will be `download_folder/weekly_flow_folder`. |
|
||||
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
|
||||
## 1.86.0 (2026-09-12)
|
||||
- Update to latest version from autobrr/autobrr (changelog : https://github.com/autobrr/autobrr/releases)
|
||||
|
||||
## 1.85.0 (2026-08-29)
|
||||
- Update to latest version from autobrr/autobrr (changelog : https://github.com/autobrr/autobrr/releases)
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
|
||||
ARG BUILD_FROM
|
||||
ARG BUILD_VERSION
|
||||
ARG BUILD_UPSTREAM="1.85.0"
|
||||
ARG BUILD_UPSTREAM="1.86.0"
|
||||
FROM ${BUILD_FROM}
|
||||
|
||||
##################
|
||||
|
||||
@@ -67,7 +67,7 @@ Default credentials: `admin` / `password` (change after first login).
|
||||
### Options
|
||||
|
||||
| Option | Type | Default | Description |
|
||||
|--------|------|---------|-------------|
|
||||
| -------- | ------ | --------- | ------------- |
|
||||
| `PGID` | int | `0` | Group ID for file permissions |
|
||||
| `PUID` | int | `0` | User ID for file permissions |
|
||||
| `TZ` | str | | Timezone (e.g., `Europe/London`) |
|
||||
|
||||
@@ -108,4 +108,4 @@ schema:
|
||||
slug: autobrr
|
||||
udev: true
|
||||
url: https://github.com/alexbelgium/hassio-addons/tree/master/autobrr
|
||||
version: "1.85.0"
|
||||
version: "1.86.0"
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"last_update": "2026-08-29",
|
||||
"last_update": "2026-09-12",
|
||||
"repository": "alexbelgium/hassio-addons",
|
||||
"slug": "autborr",
|
||||
"source": "github",
|
||||
"upstream_repo": "autobrr/autobrr",
|
||||
"upstream_version": "1.85.0"
|
||||
"upstream_version": "1.86.0"
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ Configurations can be done through the app webUI, except for the following optio
|
||||
### Options
|
||||
|
||||
| Option | Type | Default | Description |
|
||||
|--------|------|---------|-------------|
|
||||
| -------- | ------ | --------- | ------------- |
|
||||
| `PGID` | int | `0` | Group ID for file permissions |
|
||||
| `PUID` | int | `0` | User ID for file permissions |
|
||||
| `TZ` | str | | Timezone (e.g., `Europe/London`) |
|
||||
|
||||
0
bazarr/rootfs/etc/services.d/nginx/run
Normal file → Executable file
0
bazarr/rootfs/etc/services.d/nginx/run
Normal file → Executable file
@@ -17,7 +17,7 @@ A privacy-first PDF toolkit running entirely in your browser — no uploads, no
|
||||
### Organize & Edit
|
||||
|
||||
| Tool | Tool | Tool |
|
||||
|------|------|------|
|
||||
| ------ | ------ | ------ |
|
||||
| Merge PDF | Split PDF | Organize PDF |
|
||||
| Delete Pages | Extract Pages | Reverse Pages |
|
||||
| Rotate PDF | Rotate Custom | Crop PDF |
|
||||
@@ -28,7 +28,7 @@ A privacy-first PDF toolkit running entirely in your browser — no uploads, no
|
||||
### Convert TO PDF
|
||||
|
||||
| Tool | Tool | Tool |
|
||||
|------|------|------|
|
||||
| ------ | ------ | ------ |
|
||||
| Word to PDF | Excel to PDF | PowerPoint to PDF |
|
||||
| Image to PDF | JPG to PDF | PNG to PDF |
|
||||
| BMP to PDF | TIFF to PDF | WEBP to PDF |
|
||||
@@ -44,7 +44,7 @@ A privacy-first PDF toolkit running entirely in your browser — no uploads, no
|
||||
### Convert FROM PDF
|
||||
|
||||
| Tool | Tool | Tool |
|
||||
|------|------|------|
|
||||
| ------ | ------ | ------ |
|
||||
| PDF to DOCX | PDF to Excel | PDF to JPG |
|
||||
| PDF to PNG | PDF to BMP | PDF to TIFF |
|
||||
| PDF to WEBP | PDF to SVG | PDF to Text |
|
||||
@@ -54,7 +54,7 @@ A privacy-first PDF toolkit running entirely in your browser — no uploads, no
|
||||
### Security & Metadata
|
||||
|
||||
| Tool | Tool | Tool |
|
||||
|------|------|------|
|
||||
| ------ | ------ | ------ |
|
||||
| Encrypt PDF | Decrypt PDF | Change Permissions |
|
||||
| Remove Restrictions | Sign PDF | Digital Sign PDF |
|
||||
| Validate Signature | Edit Metadata | View Metadata |
|
||||
@@ -64,7 +64,7 @@ A privacy-first PDF toolkit running entirely in your browser — no uploads, no
|
||||
### Enhance & Process
|
||||
|
||||
| Tool | Tool | Tool |
|
||||
|------|------|------|
|
||||
| ------ | ------ | ------ |
|
||||
| Compress PDF | OCR PDF | Deskew PDF |
|
||||
| Rasterize PDF | Linearize PDF | PDF to PDF/A |
|
||||
| Adjust Colors | Invert Colors | Text Color |
|
||||
@@ -76,7 +76,7 @@ A privacy-first PDF toolkit running entirely in your browser — no uploads, no
|
||||
### Forms & More
|
||||
|
||||
| Tool | Tool | Tool |
|
||||
|------|------|------|
|
||||
| ------ | ------ | ------ |
|
||||
| Form Creator | Form Filler | Table of Contents |
|
||||
| Bookmark | PDF Editor | Extract Images |
|
||||
| Extract Tables | Extract Attachments | Edit Attachments |
|
||||
|
||||
0
bentopdf/rootfs/etc/s6-overlay/s6-rc.d/bentopdf/run
Normal file → Executable file
0
bentopdf/rootfs/etc/s6-overlay/s6-rc.d/bentopdf/run
Normal file → Executable file
@@ -1,3 +1,7 @@
|
||||
## 20260912.2 (12-09-2026)
|
||||
- Minor bugs fixed
|
||||
## 20260912 (12-09-2026)
|
||||
- Minor bugs fixed
|
||||
## 20260910.4 (11-09-2026)
|
||||
- Minor bugs fixed
|
||||
## 20260910.3 (11-09-2026)
|
||||
|
||||
@@ -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.
|
||||
|
||||
**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
|
||||
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
|
||||
- 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
|
||||
- the taxonomy or the database cannot be consulted — it fails open and accepts the detection
|
||||
|
||||
|
||||
@@ -127,5 +127,5 @@ slug: birdnet-go-dev
|
||||
udev: true
|
||||
url: https://github.com/alexbelgium/hassio-addons
|
||||
usb: true
|
||||
version: "20260910.4"
|
||||
version: "20260912.3"
|
||||
video: true
|
||||
|
||||
@@ -33,7 +33,10 @@ TARGET_DIR=""
|
||||
while [ "$#" -gt 0 ]; do
|
||||
case "$1" in
|
||||
--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
|
||||
# appended an argument; the pre-flag script used "${1}", so refuse rather than
|
||||
@@ -184,7 +187,7 @@ if [ "${CHECK_ONLY}" = "1" ]; then
|
||||
fi
|
||||
echo "!!! CHECK FAILED: ${#conflicting[@]} PR(s) would break the add-on build" >&2
|
||||
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
|
||||
done
|
||||
echo "!!! conflicts-with=main -> the PR is stale against main; merge main into its branch and resolve there." >&2
|
||||
|
||||
@@ -190,5 +190,5 @@ LOG_DIR="/config/logs"
|
||||
if [ -d "$LOG_DIR" ]; then
|
||||
bashio::log.info "Trimming log files older than ${LOG_MAX_AGE_DAYS} days in ${LOG_DIR}"
|
||||
ln -sf "$LOG_DIR" /logs
|
||||
find "$LOG_DIR" -type f -name "*.log*" -mtime +"$LOG_MAX_AGE_DAYS" -delete 2>/dev/null || true
|
||||
find "$LOG_DIR" -type f -name "*.log*" -mtime +"$LOG_MAX_AGE_DAYS" -delete 2> /dev/null || true
|
||||
fi
|
||||
|
||||
@@ -44,8 +44,8 @@ if ! bashio::config.true 'mariadb_auto_config'; then
|
||||
# Only revert if config.yaml points at the HA MariaDB host we would have
|
||||
# written — a mysql block pointing at a different host was set manually.
|
||||
# shellcheck disable=SC2016
|
||||
CURRENT_MYSQL_HOST="$(yq -r '.output.mysql.host // empty' "$CONFIG_LOCATION" 2>/dev/null || true)"
|
||||
if yq -e '.output.mysql.enabled == true' "$CONFIG_LOCATION" >/dev/null 2>&1 \
|
||||
CURRENT_MYSQL_HOST="$(yq -r '.output.mysql.host // empty' "$CONFIG_LOCATION" 2> /dev/null || true)"
|
||||
if yq -e '.output.mysql.enabled == true' "$CONFIG_LOCATION" > /dev/null 2>&1 \
|
||||
&& [ "${CURRENT_MYSQL_HOST}" = "${MYSQL_HOST}" ]; then
|
||||
yq -i -y \
|
||||
'.output.mysql.enabled = false
|
||||
@@ -71,7 +71,7 @@ bashio::log.green "---"
|
||||
# Resolve MariaDB hostname to IPv4: on HAOS >=17.3 the Supervisor network
|
||||
# gained IPv6, but the MariaDB addon only grants its user from the IPv4
|
||||
# subnet. Fall back to the raw hostname if resolution fails.
|
||||
MYSQL_HOST_RESOLVED="$(getent ahostsv4 "${MYSQL_HOST}" 2>/dev/null | awk '{print $1; exit}')"
|
||||
MYSQL_HOST_RESOLVED="$(getent ahostsv4 "${MYSQL_HOST}" 2> /dev/null | awk '{print $1; exit}')"
|
||||
MYSQL_HOST_RESOLVED="${MYSQL_HOST_RESOLVED:-${MYSQL_HOST}}"
|
||||
if [ "${MYSQL_HOST_RESOLVED}" != "${MYSQL_HOST}" ]; then
|
||||
bashio::log.blue "Resolved ${MYSQL_HOST} -> ${MYSQL_HOST_RESOLVED} (forcing IPv4)"
|
||||
|
||||
@@ -23,8 +23,8 @@
|
||||
|
||||
## nightly-20260615-2 (05-07-2026)
|
||||
- Minor bugs fixed
|
||||
- Fix detections/database not persisting across restarts on a fresh install: upstream's default `config.yaml` ships `output.sqlite.path: birdnet.db` (relative) explicitly, so the missing-only (`//=`) seeding introduced previously never rewrote it to an absolute path. A relative path resolves against the app's ephemeral working directory, so the database was silently recreated empty on every restart. Any relative `output.sqlite.path` is now rewritten to live under the persistent `/config` on startup; values already set to an absolute path are left untouched. (https://github.com/tphakala/birdnet-go/discussions/3774)
|
||||
- MQTT auto-config now also enables BirdNET-Go's native Home Assistant MQTT auto-discovery: detection sensors appear in Home Assistant automatically with no manual YAML (existing UI/config.yaml edits are preserved)
|
||||
- Fix detections/database not persisting across restarts on a fresh install: upstream's default `config.yaml` ships `output.sqlite.path: birdnet.db` (relative) explicitly, so the missing-only (`//=`) seeding introduced previously never rewrote it to an absolute path. A relative path resolves against the app's ephemeral working directory, so the database was silently recreated empty on every restart. Any relative `output.sqlite.path` is now rewritten to live under the persistent `/config` on startup; values already set to an absolute path are left untouched. (https://github.com/tphakala/birdnet-go/discussions/3774)
|
||||
- MQTT auto-config now also enables BirdNET-Go's native Home Assistant MQTT auto-discovery: detection sensors appear in Home Assistant automatically with no manual YAML (existing UI/config.yaml edits are preserved)
|
||||
- MQTT auto-config seeds `realtime.mqtt.retain: true` (only when unset) so sensor states survive Home Assistant restarts
|
||||
- Added supervisor watchdog (tcp://[HOST]:[PORT:8080]) so the add-on is automatically restarted if BirdNET-Go stops responding
|
||||
- Added backup_exclude for rotated logs, making Home Assistant backups smaller (SQLite journals are kept so hot backups stay consistent)
|
||||
|
||||
@@ -186,5 +186,5 @@ LOG_DIR="/config/logs"
|
||||
if [ -d "$LOG_DIR" ]; then
|
||||
bashio::log.info "Trimming log files older than ${LOG_MAX_AGE_DAYS} days in ${LOG_DIR}"
|
||||
ln -sf "$LOG_DIR" /logs
|
||||
find "$LOG_DIR" -type f -name "*.log*" -mtime +"$LOG_MAX_AGE_DAYS" -delete 2>/dev/null || true
|
||||
find "$LOG_DIR" -type f -name "*.log*" -mtime +"$LOG_MAX_AGE_DAYS" -delete 2> /dev/null || true
|
||||
fi
|
||||
|
||||
@@ -9,9 +9,9 @@ Here are some example tests I did (the whole threads are really interesting also
|
||||
|
||||
**My recommendation:**
|
||||
|
||||
- **Best entry system (< 50 €):** Boya By-lm40 (30 €) + deadcat (10 €)
|
||||
- **Best middle end system (< 150 €):** Clippy EM272 TRS/TRRS (55 €) + Rode AI micro trs/trrs to usb (70 €) + Rycote deadcat (27 €)
|
||||
- **Best high end system (<400 €):** Clippy EM272 XLR (85 €) or LOM Ucho Pro (75 €) + Focusrite Scarlet 2i2 4th Gen (200 €) + Bubblebee Pro Extreme deadcat (45 €)
|
||||
- **Best entry system (< 50 €):** Boya By-lm40 (30 €) + deadcat (10 €)
|
||||
- **Best middle end system (< 150 €):** Clippy EM272 TRS/TRRS (55 €) + Rode AI micro trs/trrs to usb (70 €) + Rycote deadcat (27 €)
|
||||
- **Best high end system (<400 €):** Clippy EM272 XLR (85 €) or LOM Ucho Pro (75 €) + Focusrite Scarlet 2i2 4th Gen (200 €) + Bubblebee Pro Extreme deadcat (45 €)
|
||||
|
||||
**Sources for high end microphones in Europe:**
|
||||
|
||||
|
||||
@@ -112,6 +112,7 @@ def automatic_mqtt_publish(file, detection, path):
|
||||
mqttc.publish(mqtt_topic, json_bird, 1)
|
||||
log.info("Posted to MQTT: ok")
|
||||
|
||||
|
||||
# Create MQTT client using legacy callback API when available for
|
||||
# compatibility with paho-mqtt >= 2.0
|
||||
callback_api = getattr(mqtt, "CallbackAPIVersion", None)
|
||||
|
||||
@@ -9,9 +9,9 @@ Here are some example tests I did (the whole threads are really interesting also
|
||||
|
||||
**My recommendation:**
|
||||
|
||||
- **Best entry system (< 50 €):** Boya By-lm40 (30 €) + deadcat (10 €)
|
||||
- **Best middle end system (< 150 €):** Clippy EM272 TRS/TRRS (55 €) + Rode AI micro trs/trrs to usb (70 €) + Rycote deadcat (27 €)
|
||||
- **Best high end system (<400 €):** Clippy EM272 XLR (85 €) or LOM Ucho Pro (75 €) + Focusrite Scarlet 2i2 4th Gen (200 €) + Bubblebee Pro Extreme deadcat (45 €)
|
||||
- **Best entry system (< 50 €):** Boya By-lm40 (30 €) + deadcat (10 €)
|
||||
- **Best middle end system (< 150 €):** Clippy EM272 TRS/TRRS (55 €) + Rode AI micro trs/trrs to usb (70 €) + Rycote deadcat (27 €)
|
||||
- **Best high end system (<400 €):** Clippy EM272 XLR (85 €) or LOM Ucho Pro (75 €) + Focusrite Scarlet 2i2 4th Gen (200 €) + Bubblebee Pro Extreme deadcat (45 €)
|
||||
|
||||
**Sources for high end microphones in Europe:**
|
||||
|
||||
|
||||
@@ -112,6 +112,7 @@ def automatic_mqtt_publish(file, detection, path):
|
||||
mqttc.publish(mqtt_topic, json_bird, 1)
|
||||
log.info("Posted to MQTT: ok")
|
||||
|
||||
|
||||
# Create MQTT client using legacy callback API when available for
|
||||
# compatibility with paho-mqtt >= 2.0
|
||||
callback_api = getattr(mqtt, "CallbackAPIVersion", None)
|
||||
|
||||
@@ -73,7 +73,7 @@ map:
|
||||
name: BirdNET-PiPy
|
||||
options:
|
||||
env_vars: []
|
||||
ICECAST_PASSWORD: ''
|
||||
ICECAST_PASSWORD: ""
|
||||
data_location: /config/data
|
||||
panel_icon: mdi:bird
|
||||
ports:
|
||||
|
||||
@@ -7,7 +7,7 @@ DATA_LOCATION="$(bashio::config 'data_location' || true)"
|
||||
DATA_LOCATION="${DATA_LOCATION:-$DEFAULT_LOCATION}"
|
||||
|
||||
case "${DATA_LOCATION}" in
|
||||
/config/*|/share/*|/data/*) ;;
|
||||
/config/* | /share/* | /data/*) ;;
|
||||
*)
|
||||
bashio::log.warning "Invalid data_location '${DATA_LOCATION}', falling back to ${DEFAULT_LOCATION}"
|
||||
DATA_LOCATION="${DEFAULT_LOCATION}"
|
||||
@@ -19,11 +19,11 @@ LEGACY2="/data"
|
||||
|
||||
mkdir -p "${DATA_LOCATION}"
|
||||
|
||||
if [ -z "$(ls -A "${DATA_LOCATION}" 2>/dev/null || true)" ]; then
|
||||
if [ -d "${LEGACY1}" ] && [ -n "$(ls -A "${LEGACY1}" 2>/dev/null || true)" ]; then
|
||||
if [ -z "$(ls -A "${DATA_LOCATION}" 2> /dev/null || true)" ]; then
|
||||
if [ -d "${LEGACY1}" ] && [ -n "$(ls -A "${LEGACY1}" 2> /dev/null || true)" ]; then
|
||||
bashio::log.notice "Migrating legacy data from ${LEGACY1} to ${DATA_LOCATION}"
|
||||
cp -a "${LEGACY1}/." "${DATA_LOCATION}/" || true
|
||||
elif [ -d "${LEGACY2}" ] && [ "${LEGACY2}" != "${DATA_LOCATION}" ] && [ -n "$(ls -A "${LEGACY2}" 2>/dev/null || true)" ]; then
|
||||
elif [ -d "${LEGACY2}" ] && [ "${LEGACY2}" != "${DATA_LOCATION}" ] && [ -n "$(ls -A "${LEGACY2}" 2> /dev/null || true)" ]; then
|
||||
bashio::log.notice "Migrating legacy data from ${LEGACY2} to ${DATA_LOCATION}"
|
||||
cp -a "${LEGACY2}/." "${DATA_LOCATION}/" || true
|
||||
fi
|
||||
|
||||
@@ -3,20 +3,20 @@
|
||||
set -euo pipefail
|
||||
|
||||
SND_GID=""
|
||||
if [ -e /dev/snd ] && command -v stat >/dev/null 2>&1; then
|
||||
SND_GID="$(stat -c '%g' /dev/snd 2>/dev/null || true)"
|
||||
if [ -e /dev/snd ] && command -v stat > /dev/null 2>&1; then
|
||||
SND_GID="$(stat -c '%g' /dev/snd 2> /dev/null || true)"
|
||||
fi
|
||||
|
||||
if [ -n "${SND_GID}" ] && getent group audio >/dev/null 2>&1; then
|
||||
if [ -n "${SND_GID}" ] && getent group audio > /dev/null 2>&1; then
|
||||
current_gid="$(getent group audio | cut -d: -f3 || true)"
|
||||
if [ -n "${current_gid}" ] && [ "${current_gid}" != "${SND_GID}" ]; then
|
||||
groupmod -g "${SND_GID}" audio 2>/dev/null || true
|
||||
groupmod -g "${SND_GID}" audio 2> /dev/null || true
|
||||
fi
|
||||
fi
|
||||
|
||||
for u in root nginx www-data icecast2; do
|
||||
if id "${u}" >/dev/null 2>&1; then
|
||||
usermod -aG audio "${u}" 2>/dev/null || true
|
||||
if id "${u}" > /dev/null 2>&1; then
|
||||
usermod -aG audio "${u}" 2> /dev/null || true
|
||||
fi
|
||||
done
|
||||
|
||||
@@ -30,7 +30,7 @@ if [ -n "${PULSE_SOCK}" ] && [ -S "${PULSE_SOCK}" ] && [ ! -S /run/pulse/native
|
||||
fi
|
||||
|
||||
# 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)"
|
||||
if [ -n "${ICECAST_HOME}" ]; then
|
||||
for cookie in /config/.config/pulse/cookie /root/.config/pulse/cookie; do
|
||||
|
||||
0
birdnet-pipy/rootfs/etc/services.d/api/run
Normal file → Executable file
0
birdnet-pipy/rootfs/etc/services.d/api/run
Normal file → Executable file
0
birdnet-pipy/rootfs/etc/services.d/icecast/run
Normal file → Executable file
0
birdnet-pipy/rootfs/etc/services.d/icecast/run
Normal file → Executable file
0
birdnet-pipy/rootfs/etc/services.d/main/run
Normal file → Executable file
0
birdnet-pipy/rootfs/etc/services.d/main/run
Normal file → Executable file
0
birdnet-pipy/rootfs/etc/services.d/model/run
Normal file → Executable file
0
birdnet-pipy/rootfs/etc/services.d/model/run
Normal file → Executable file
0
birdnet-pipy/rootfs/etc/services.d/nginx/run
Normal file → Executable file
0
birdnet-pipy/rootfs/etc/services.d/nginx/run
Normal file → Executable file
0
bitwarden/rootfs/etc/s6-overlay/s6-rc.d/init-nginx/run
Normal file → Executable file
0
bitwarden/rootfs/etc/s6-overlay/s6-rc.d/init-nginx/run
Normal file → Executable file
0
bitwarden/rootfs/etc/s6-overlay/s6-rc.d/nginx/run
Normal file → Executable file
0
bitwarden/rootfs/etc/s6-overlay/s6-rc.d/nginx/run
Normal file → Executable file
6
bitwarden/rootfs/etc/s6-overlay/s6-rc.d/vaultwarden/run
Normal file → Executable file
6
bitwarden/rootfs/etc/s6-overlay/s6-rc.d/vaultwarden/run
Normal file → Executable file
@@ -21,19 +21,19 @@ export ROCKET_SECRET_KEY="${secret_key}"
|
||||
# Find the matching log level
|
||||
if bashio::config.has_value 'log_level'; then
|
||||
case "$(bashio::string.lower "$(bashio::config 'log_level')")" in
|
||||
all|trace)
|
||||
all | trace)
|
||||
log_level="trace"
|
||||
;;
|
||||
debug)
|
||||
log_level="debug"
|
||||
;;
|
||||
info|notice)
|
||||
info | notice)
|
||||
log_level="info"
|
||||
;;
|
||||
warning)
|
||||
log_level="warn"
|
||||
;;
|
||||
error|fatal)
|
||||
error | fatal)
|
||||
log_level="error"
|
||||
;;
|
||||
off)
|
||||
|
||||
@@ -64,7 +64,7 @@ Default credentials are shown in the startup logs.
|
||||
### Options
|
||||
|
||||
| Option | Type | Default | Description |
|
||||
|--------|------|---------|-------------|
|
||||
| -------- | ------ | --------- | ------------- |
|
||||
| `PGID` | int | `0` | Group ID for file permissions |
|
||||
| `PUID` | int | `0` | User ID for file permissions |
|
||||
| `TZ` | str | | Timezone (e.g., `Europe/London`) |
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
|
||||
## 1.95.101-ls128 (2026-09-12)
|
||||
- Update to latest version from linuxserver/docker-brave (changelog : https://github.com/linuxserver/docker-brave/releases)
|
||||
- Migrate legacy add-on configuration map names to current app configuration terminology.
|
||||
|
||||
## 1.94.121-ls127 (2026-09-05)
|
||||
|
||||
@@ -69,5 +69,5 @@ slug: brave
|
||||
tmpfs: true
|
||||
udev: true
|
||||
url: https://github.com/alexbelgium/hassio-addons
|
||||
version: "1.94.121-ls127"
|
||||
version: "1.95.101-ls128"
|
||||
video: true
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
{
|
||||
"github_fulltag": "true",
|
||||
"last_update": "2026-09-05",
|
||||
"last_update": "2026-09-12",
|
||||
"repository": "alexbelgium/hassio-addons",
|
||||
"slug": "brave",
|
||||
"source": "github",
|
||||
"upstream_repo": "linuxserver/docker-brave",
|
||||
"upstream_version": "1.94.121-ls127"
|
||||
"upstream_version": "1.95.101-ls128"
|
||||
}
|
||||
|
||||
1
browser_chromium/rootfs/etc/cont-init.d/20-folders.sh
Executable file → Normal file
1
browser_chromium/rootfs/etc/cont-init.d/20-folders.sh
Executable file → Normal file
@@ -7,7 +7,6 @@ set -e
|
||||
PUID=$(bashio::config "PUID")
|
||||
PGID=$(bashio::config "PGID")
|
||||
|
||||
|
||||
# Create cache
|
||||
#mkdir -p /.cache
|
||||
#chmod 755 /.cache
|
||||
|
||||
@@ -42,7 +42,7 @@ args=(
|
||||
|
||||
# Container reality: sandbox usually breaks unless seccomp is fully disabled
|
||||
if [[ "${seccomp_mode}" != "0" ]]; then
|
||||
args+=( --no-sandbox --test-type )
|
||||
args+=(--no-sandbox --test-type)
|
||||
fi
|
||||
|
||||
# IMPORTANT:
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
|
||||
## 2.56.7 (2026-09-12)
|
||||
- Update to latest version from browserless/chrome (changelog : https://github.com/browserless/chrome/releases)
|
||||
- Migrate legacy add-on configuration map names to current app configuration terminology.
|
||||
|
||||
## 2.56.3 (2026-09-05)
|
||||
|
||||
@@ -86,5 +86,5 @@ schema:
|
||||
slug: browserless_chrome
|
||||
udev: true
|
||||
url: https://github.com/alexbelgium/hassio-addons/tree/master/browserless_chrome
|
||||
version: "2.56.3"
|
||||
version: "2.56.7"
|
||||
webui: "[PROTO:ssl]://[HOST]:[PORT:3000]/docs"
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
{
|
||||
"github_tagfilter": "v",
|
||||
"last_update": "2026-09-05",
|
||||
"last_update": "2026-09-12",
|
||||
"repository": "alexbelgium/hassio-addons",
|
||||
"slug": "browserless_chrome",
|
||||
"source": "github",
|
||||
"upstream_repo": "browserless/chrome",
|
||||
"upstream_version": "2.56.3"
|
||||
"upstream_version": "2.56.7"
|
||||
}
|
||||
|
||||
@@ -62,7 +62,7 @@ Please read the upstream container documentation for further info: https://githu
|
||||
### Options
|
||||
|
||||
| Option | Type | Default | Description |
|
||||
|--------|------|---------|-------------|
|
||||
| -------- | ------ | --------- | ------------- |
|
||||
| `PGID` | int | `0` | Group ID for file permissions |
|
||||
| `PUID` | int | `0` | User ID for file permissions |
|
||||
| `TZ` | str | | Timezone (e.g., `Europe/London`) |
|
||||
|
||||
@@ -62,7 +62,7 @@ Default password: admin123
|
||||
### Options
|
||||
|
||||
| Option | Type | Default | Description |
|
||||
|--------|------|---------|-------------|
|
||||
| -------- | ------ | --------- | ------------- |
|
||||
| `PGID` | int | `0` | Group ID for file permissions |
|
||||
| `PUID` | int | `0` | User ID for file permissions |
|
||||
| `TZ` | str | | Timezone (e.g., `Europe/London`) |
|
||||
@@ -99,7 +99,7 @@ This addon supports mounting both local drives and remote SMB shares:
|
||||
|
||||
### Optional Calibre-Web features
|
||||
|
||||
Calibre-Web documents optional extras that a manual installation adds with `pip install calibreweb[metadata]` and similar. **You do not need to install anything here**: the LinuxServer base image this add-on builds on installs Calibre-Web's `requirements.txt` *and* its full `optional-requirements.txt` into the application's virtualenv, so the gdrive, gmail, goodreads, ldap, oauth, metadata, comics and kobo dependencies are all present already. Running `pip install calibreweb[...]` inside the container is not a supported way to enable them: it installs the PyPI distribution of Calibre-Web over an installation that already has those dependencies, and it can disturb the versions the base image pinned. It is also thrown away, because the Supervisor recreates the add-on container on restart.
|
||||
Calibre-Web documents optional extras that a manual installation adds with `pip install calibreweb[metadata]` and similar. **You do not need to install anything here**: the LinuxServer base image this add-on builds on installs Calibre-Web's `requirements.txt` _and_ its full `optional-requirements.txt` into the application's virtualenv, so the gdrive, gmail, goodreads, ldap, oauth, metadata, comics and kobo dependencies are all present already. Running `pip install calibreweb[...]` inside the container is not a supported way to enable them: it installs the PyPI distribution of Calibre-Web over an installation that already has those dependencies, and it can disturb the versions the base image pinned. It is also thrown away, because the Supervisor recreates the add-on container on restart.
|
||||
|
||||
Optional features are switched on in the Calibre-Web web interface, not in the add-on options, under `Admin` -> `Basic Configuration` -> `Feature Configuration` (for example `Enable Uploads`, `Enable Kobo sync`, `Use Goodreads`).
|
||||
|
||||
|
||||
@@ -30,16 +30,16 @@ else
|
||||
# duplicates it can leave behind are entries calibre-web skips or already trusts.
|
||||
# The column only exists once calibre-web 0.6.27+ has migrated app.db and cont-init runs
|
||||
# before calibre-web, so a failure here is not fatal : the next start applies it.
|
||||
trusted_ips_error=$(sqlite3 /config/app.db "update settings set config_reverse_proxy_trusted_ips='127.0.0.1,::1,::ffff:127.0.0.1,172.30.32.0/23,::ffff:172.30.32.0/119,'||coalesce(config_reverse_proxy_trusted_ips,'') where coalesce(config_reverse_proxy_trusted_ips,'') not like '%::ffff:172.30.32.0/119%'" 2>&1) ||
|
||||
bashio::log.warning "Could not set the ingress trusted ip list, it will be applied at next start (${trusted_ips_error})"
|
||||
trusted_ips_error=$(sqlite3 /config/app.db "update settings set config_reverse_proxy_trusted_ips='127.0.0.1,::1,::ffff:127.0.0.1,172.30.32.0/23,::ffff:172.30.32.0/119,'||coalesce(config_reverse_proxy_trusted_ips,'') where coalesce(config_reverse_proxy_trusted_ips,'') not like '%::ffff:172.30.32.0/119%'" 2>&1) \
|
||||
|| bashio::log.warning "Could not set the ingress trusted ip list, it will be applied at next start (${trusted_ips_error})"
|
||||
|
||||
# Calibre-web autodetects kepubify only while this setting is still NULL. On every install
|
||||
# that predates the Dockerfile fix above, that detection already ran, found nothing usable and
|
||||
# stored an empty string, so it is never retried. Put an empty value back to NULL and
|
||||
# calibre-web detects /opt/kepubify itself when it starts, a few seconds after this runs.
|
||||
# A path the user set by hand is not empty and is left alone.
|
||||
kepubify_error=$(sqlite3 /config/app.db "update settings set config_kepubifypath = NULL where config_kepubifypath = ''" 2>&1) ||
|
||||
bashio::log.warning "Could not reset the kepubify path, it will be applied at next start (${kepubify_error})"
|
||||
kepubify_error=$(sqlite3 /config/app.db "update settings set config_kepubifypath = NULL where config_kepubifypath = ''" 2>&1) \
|
||||
|| bashio::log.warning "Could not reset the kepubify path, it will be applied at next start (${kepubify_error})"
|
||||
fi
|
||||
|
||||
bashio::log.info "Default username:password is admin:admin123"
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
|
||||
## 0.60.4 (2026-09-12)
|
||||
- Update to latest version from linuxserver/docker-changedetection.io (changelog : https://github.com/linuxserver/docker-changedetection.io/releases)
|
||||
|
||||
## 0.60.3 (2026-09-05)
|
||||
- Update to latest version from linuxserver/docker-changedetection.io (changelog : https://github.com/linuxserver/docker-changedetection.io/releases)
|
||||
|
||||
|
||||
@@ -52,7 +52,7 @@ You can add a shortcut pointing to your Changedetection.io instance with the fol
|
||||
### Options
|
||||
|
||||
| Option | Type | Default | Description |
|
||||
|--------|------|---------|-------------|
|
||||
| -------- | ------ | --------- | ------------- |
|
||||
| `PGID` | int | `0` | Group ID for file permissions |
|
||||
| `PUID` | int | `0` | User ID for file permissions |
|
||||
| `TZ` | str | | Timezone (e.g., `Europe/London`) |
|
||||
|
||||
@@ -34,4 +34,4 @@ schema:
|
||||
slug: changedetection.io
|
||||
udev: true
|
||||
url: https://github.com/alexbelgium/hassio-addons/tree/master/changedetection.io
|
||||
version: "0.60.3"
|
||||
version: "0.60.4"
|
||||
|
||||
0
changedetection.io/rootfs/etc/services.d/nginx/run
Normal file → Executable file
0
changedetection.io/rootfs/etc/services.d/nginx/run
Normal file → Executable file
@@ -1,9 +1,9 @@
|
||||
{
|
||||
"github_fulltag": "false",
|
||||
"last_update": "2026-09-05",
|
||||
"last_update": "2026-09-12",
|
||||
"repository": "alexbelgium/hassio-addons",
|
||||
"slug": "changedetection.io",
|
||||
"source": "github",
|
||||
"upstream_repo": "linuxserver/docker-changedetection.io",
|
||||
"upstream_version": "0.60.3"
|
||||
"upstream_version": "0.60.4"
|
||||
}
|
||||
|
||||
@@ -1,3 +1,11 @@
|
||||
|
||||
## 07308545.8 (2026-09-12)
|
||||
- Update to latest version from aaddrick/claude-desktop-debian (changelog : https://github.com/aaddrick/claude-desktop-debian/releases)
|
||||
- Upstream tag : v3.2.4+claude1.52386.3
|
||||
|
||||
## 07308545.7 (2026-09-12)
|
||||
- Update to latest version from aaddrick/claude-desktop-debian (changelog : https://github.com/aaddrick/claude-desktop-debian/releases)
|
||||
- Upstream tag : v3.2.4+claude1.49585.0
|
||||
- Migrate legacy add-on configuration map names to current app configuration terminology.
|
||||
|
||||
## 07308545.6 (2026-09-05)
|
||||
|
||||
@@ -136,5 +136,5 @@ schema:
|
||||
slug: claude_desktop
|
||||
udev: true
|
||||
url: https://github.com/alexbelgium/hassio-addons
|
||||
version: "07308545.6"
|
||||
version: "07308545.8"
|
||||
video: true
|
||||
|
||||
@@ -55,7 +55,7 @@
|
||||
# shared memory into ordinary files for no benefit. If the size cannot be determined, keep the
|
||||
# flag — the crash it prevents is worse than the overhead it costs.
|
||||
SHM_FLAGS="--disable-dev-shm-usage"
|
||||
SHM_KB="$(df -k /dev/shm 2> /dev/null | awk 'NR==2 {print $2}')"
|
||||
SHM_KB="$(df -k /dev/shm 2>/dev/null | awk 'NR==2 {print $2}')"
|
||||
if [ -n "$SHM_KB" ] && [ "$SHM_KB" -ge 262144 ]; then
|
||||
SHM_FLAGS=""
|
||||
fi
|
||||
|
||||
@@ -141,7 +141,7 @@ release_info=""
|
||||
if curl -fsSL --retry 3 --retry-delay 2 --connect-timeout 10 --max-time 30 \
|
||||
-o "$release_metadata" "$CODEX_RELEASE_API"; then
|
||||
release_info="$(
|
||||
CODEX_ASSET="$CODEX_ASSET" python3 - "$release_metadata" <<'PY' 2> /dev/null || true
|
||||
CODEX_ASSET="$CODEX_ASSET" python3 - "$release_metadata" << 'PY' 2> /dev/null || true
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
@@ -249,7 +249,7 @@ fi
|
||||
{
|
||||
printf '#!/usr/bin/env bash\n'
|
||||
printf 'CODEX_REAL=%q\n' "$CODEX_REAL"
|
||||
cat <<'SH'
|
||||
cat << 'SH'
|
||||
RUNTIME_HOME="$(getent passwd abc | cut -d: -f6)"
|
||||
if [ -z "$RUNTIME_HOME" ]; then
|
||||
echo "codex: unable to resolve the abc runtime home" >&2
|
||||
@@ -338,8 +338,7 @@ ln -sfn "$CODEX_BIN" "$CODEX_LINK"
|
||||
CODEX_SANDBOX_MODE="$(bashio::config 'codex_sandbox_mode' 'danger-full-access')"
|
||||
run_as_runtime_user mkdir -p "$RUNTIME_HOME/.codex"
|
||||
CODEX_SANDBOX_MODE="$CODEX_SANDBOX_MODE" RUNTIME_HOME="$RUNTIME_HOME" \
|
||||
run_as_runtime_user python3 - <<'PY' \
|
||||
|| bashio::log.warning "Unable to update the managed Codex configuration block"
|
||||
run_as_runtime_user python3 - << 'PY' || bashio::log.warning "Unable to update the managed Codex configuration block"
|
||||
import os
|
||||
import re
|
||||
import tomllib
|
||||
|
||||
@@ -30,7 +30,7 @@ manage_claude_md_block() {
|
||||
fi
|
||||
elif [ -f "$CLAUDE_MD" ] && grep -qF "$begin" "$CLAUDE_MD"; then
|
||||
bashio::log.info "Removing ${name} guidance from CLAUDE.md"
|
||||
CLAUDE_MD="$CLAUDE_MD" BLOCK_NAME="$name" python3 - <<'PY' || bashio::log.warning "Unable to remove the ${name} guidance automatically"
|
||||
CLAUDE_MD="$CLAUDE_MD" BLOCK_NAME="$name" python3 - << 'PY' || bashio::log.warning "Unable to remove the ${name} guidance automatically"
|
||||
import os
|
||||
import re
|
||||
from pathlib import Path
|
||||
@@ -58,7 +58,7 @@ PY
|
||||
manage_settings_hook() {
|
||||
# manage_settings_hook <event> <matcher> <command> <add|remove>
|
||||
HOOK_EVENT="$1" HOOK_MATCHER="$2" HOOK_COMMAND="$3" HOOK_ACTION="$4" \
|
||||
python3 - <<'PY' || bashio::log.warning "Unable to update the $1 hook for '$3'"
|
||||
python3 - << 'PY' || bashio::log.warning "Unable to update the $1 hook for '$3'"
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
@@ -389,14 +389,14 @@ bashio::log.info "MCP servers for Claude Desktop: ${MCP_SERVERS_DESKTOP}"
|
||||
bashio::log.info "MCP servers for Claude Code: ${MCP_SERVERS_CODE}"
|
||||
|
||||
HEADROOM_ENABLED="$HEADROOM_ENABLED" HEADROOM_BIN="$(command -v headroom || echo headroom)" \
|
||||
HEADROOM_HF_HOME="${HOME}/.headroom/hf" \
|
||||
TOKENSAVE_ENABLED="$TOKENSAVE_ENABLED" TOKENSAVE_BIN="$(command -v tokensave || echo tokensave)" \
|
||||
CODEX_ENABLED="$CODEX_ENABLED" CODEX_BIN="$CODEX_BIN" CODEX_SANDBOX_MODE="$CODEX_SANDBOX_MODE" \
|
||||
HA_MCP_ENABLED="$HA_MCP_ENABLED" HA_MCP_URL="$HA_MCP_URL" HA_MCP_TOKEN="$HA_MCP_TOKEN" \
|
||||
MCP_PROXY_BIN="$(command -v mcp-proxy || echo mcp-proxy)" \
|
||||
MCP_SERVERS_DESKTOP="$MCP_SERVERS_DESKTOP" MCP_SERVERS_CODE="$MCP_SERVERS_CODE" \
|
||||
CLAUDE_DESKTOP_CONFIG="$CLAUDE_DESKTOP_CONFIG" CLAUDE_CODE_CONFIG="$CLAUDE_CODE_CONFIG" \
|
||||
python3 - <<'PY' || bashio::log.warning "Unable to update the MCP server registrations automatically"
|
||||
HEADROOM_HF_HOME="${HOME}/.headroom/hf" \
|
||||
TOKENSAVE_ENABLED="$TOKENSAVE_ENABLED" TOKENSAVE_BIN="$(command -v tokensave || echo tokensave)" \
|
||||
CODEX_ENABLED="$CODEX_ENABLED" CODEX_BIN="$CODEX_BIN" CODEX_SANDBOX_MODE="$CODEX_SANDBOX_MODE" \
|
||||
HA_MCP_ENABLED="$HA_MCP_ENABLED" HA_MCP_URL="$HA_MCP_URL" HA_MCP_TOKEN="$HA_MCP_TOKEN" \
|
||||
MCP_PROXY_BIN="$(command -v mcp-proxy || echo mcp-proxy)" \
|
||||
MCP_SERVERS_DESKTOP="$MCP_SERVERS_DESKTOP" MCP_SERVERS_CODE="$MCP_SERVERS_CODE" \
|
||||
CLAUDE_DESKTOP_CONFIG="$CLAUDE_DESKTOP_CONFIG" CLAUDE_CODE_CONFIG="$CLAUDE_CODE_CONFIG" \
|
||||
python3 - << 'PY' || bashio::log.warning "Unable to update the MCP server registrations automatically"
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
@@ -607,7 +607,7 @@ PY
|
||||
# Guide Claude to actually use the Headroom compression tools so the MCP integration produces
|
||||
# real savings when transparent proxying is unavailable.
|
||||
if $HEADROOM_ENABLED; then
|
||||
manage_claude_md_block headroom add <<'MD'
|
||||
manage_claude_md_block headroom add << 'MD'
|
||||
## Headroom context compression
|
||||
|
||||
A local Headroom proxy (127.0.0.1:8787) backs the `headroom` MCP tools. To save context tokens:
|
||||
@@ -636,7 +636,7 @@ if $HEADROOM_ENABLED && bashio::config.true 'headroom_wrap_claude_code'; then
|
||||
else
|
||||
HEADROOM_ROUTE_ACTION="remove"
|
||||
fi
|
||||
HEADROOM_ROUTE_ACTION="$HEADROOM_ROUTE_ACTION" python3 - <<'PY' || bashio::log.warning "Unable to manage the Claude Code proxy routing env"
|
||||
HEADROOM_ROUTE_ACTION="$HEADROOM_ROUTE_ACTION" python3 - << 'PY' || bashio::log.warning "Unable to manage the Claude Code proxy routing env"
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
@@ -703,7 +703,7 @@ manage_settings_hook PostToolUse "Bash|Grep|Glob|WebFetch" "$HEADROOM_HOOK_CMD"
|
||||
# Tell Claude Code that it can configure Home Assistant over the Core API via the shipped
|
||||
# `ha-cli` helper (no /config filesystem mount needed).
|
||||
if bashio::config.true 'enable_ha_api_helper'; then
|
||||
manage_claude_md_block ha-api-helper add <<'MD'
|
||||
manage_claude_md_block ha-api-helper add << 'MD'
|
||||
## Configuring Home Assistant
|
||||
|
||||
You can configure this Home Assistant instance through its Core API using the `ha-cli`
|
||||
@@ -731,7 +731,7 @@ fi
|
||||
# Registering the MCP server is not enough on its own: without guidance the model rarely reaches
|
||||
# for a second agent, the same gap the Headroom block above exists to close.
|
||||
if $CODEX_ENABLED; then
|
||||
manage_claude_md_block codex add <<'MD'
|
||||
manage_claude_md_block codex add << 'MD'
|
||||
## Delegating to ChatGPT Codex
|
||||
|
||||
The `codex` MCP server runs OpenAI's Codex agent locally, signed in with the user's ChatGPT
|
||||
|
||||
@@ -19,7 +19,7 @@ esac
|
||||
# only while it still holds one of those managed values — a defaultMode the user set by hand
|
||||
# is never deleted. Ownership of the written file is reconciled by 84-claude_runtime_ownership.sh.
|
||||
mkdir -p "$(dirname "$SETTINGS_PATH")"
|
||||
PERMISSION_MODE="$PERMISSION_MODE" SETTINGS_PATH="$SETTINGS_PATH" python3 - <<'PY'
|
||||
PERMISSION_MODE="$PERMISSION_MODE" SETTINGS_PATH="$SETTINGS_PATH" python3 - << 'PY'
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
@@ -36,7 +36,7 @@ fi
|
||||
# archive stays unpatched, so every later boot would retry under a new pid and strand another
|
||||
# copy until the container runs out of space.
|
||||
cleanup_tmp() {
|
||||
find "$(dirname "$ASAR")" -maxdepth 1 -name ".$(basename "$ASAR").addon-tmp.*" -delete 2>/dev/null || true
|
||||
find "$(dirname "$ASAR")" -maxdepth 1 -name ".$(basename "$ASAR").addon-tmp.*" -delete 2> /dev/null || true
|
||||
}
|
||||
|
||||
if output=$(timeout 120 node "$PATCHER" "$ASAR" 2>&1); then
|
||||
|
||||
@@ -45,7 +45,7 @@ if [ "$(bashio::config 'permission_mode')" = "bypass" ]; then
|
||||
fi
|
||||
|
||||
section "Claude Code permission state"
|
||||
RUNTIME_HOME="$RUNTIME_HOME" python3 - <<'PY'
|
||||
RUNTIME_HOME="$RUNTIME_HOME" python3 - << 'PY'
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
@@ -66,7 +66,7 @@ else:
|
||||
PY
|
||||
|
||||
section "MCP registrations (environment values redacted)"
|
||||
RUNTIME_HOME="$RUNTIME_HOME" python3 - <<'PY'
|
||||
RUNTIME_HOME="$RUNTIME_HOME" python3 - << 'PY'
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
@@ -104,7 +104,7 @@ for path in paths:
|
||||
PY
|
||||
|
||||
section "Claude Code hooks"
|
||||
RUNTIME_HOME="$RUNTIME_HOME" python3 - <<'PY'
|
||||
RUNTIME_HOME="$RUNTIME_HOME" python3 - << 'PY'
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
0
claude_desktop/rootfs/usr/local/bin/codex-login
Executable file → Normal file
0
claude_desktop/rootfs/usr/local/bin/codex-login
Executable file → Normal file
14
claude_desktop/rootfs/usr/local/bin/ha-cli
Executable file → Normal file
14
claude_desktop/rootfs/usr/local/bin/ha-cli
Executable file → Normal file
@@ -96,7 +96,7 @@ def resolve_endpoint():
|
||||
def _url(base, path):
|
||||
path = path.lstrip("/")
|
||||
if path.startswith("api/"):
|
||||
path = path[len("api/"):]
|
||||
path = path[len("api/") :]
|
||||
return base.rstrip("/") + "/" + path
|
||||
|
||||
|
||||
@@ -133,7 +133,9 @@ def rest(method, base, token, path, body=None):
|
||||
if body is not None:
|
||||
data = json.dumps(body).encode("utf-8")
|
||||
headers["Content-Type"] = "application/json"
|
||||
req = urllib.request.Request(_url(base, path), data=data, method=method, headers=headers)
|
||||
req = urllib.request.Request(
|
||||
_url(base, path), data=data, method=method, headers=headers
|
||||
)
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=30) as resp:
|
||||
text = resp.read().decode("utf-8")
|
||||
@@ -141,7 +143,9 @@ def rest(method, base, token, path, body=None):
|
||||
detail = exc.read().decode("utf-8", "replace").strip()
|
||||
sys.exit(f"ha-cli: HTTP {exc.code} {exc.reason} on {method} {path}\n{detail}")
|
||||
except urllib.error.URLError as exc:
|
||||
sys.exit(f"ha-cli: cannot reach Home Assistant ({exc.reason}) on {method} {path}")
|
||||
sys.exit(
|
||||
f"ha-cli: cannot reach Home Assistant ({exc.reason}) on {method} {path}"
|
||||
)
|
||||
try:
|
||||
return json.loads(text) if text.strip() else None
|
||||
except ValueError:
|
||||
@@ -214,7 +218,9 @@ def main(argv):
|
||||
sys.exit("usage: ha-cli call <domain.service> [BODY]")
|
||||
domain, service = args[0].split(".", 1)
|
||||
body = _read_body(args[1]) if len(args) > 1 else None
|
||||
_print(rest("POST", rest_base, token, f"services/{domain}/{service}", body or {}))
|
||||
_print(
|
||||
rest("POST", rest_base, token, f"services/{domain}/{service}", body or {})
|
||||
)
|
||||
elif cmd == "states":
|
||||
path = f"states/{args[0]}" if args else "states"
|
||||
_print(rest("GET", rest_base, token, path))
|
||||
|
||||
14
claude_desktop/rootfs/usr/local/bin/headroom-posttooluse-compress.py
Executable file → Normal file
14
claude_desktop/rootfs/usr/local/bin/headroom-posttooluse-compress.py
Executable file → Normal file
@@ -76,7 +76,11 @@ def main() -> int:
|
||||
|
||||
# Find big string/array fields before paying the headroom import cost.
|
||||
def is_string_array(value):
|
||||
return isinstance(value, list) and len(value) > ARRAY_KEEP and all(isinstance(v, str) for v in value)
|
||||
return (
|
||||
isinstance(value, list)
|
||||
and len(value) > ARRAY_KEEP
|
||||
and all(isinstance(v, str) for v in value)
|
||||
)
|
||||
|
||||
if isinstance(response, str):
|
||||
string_candidates = ["__whole__"] if len(response) >= MIN_CHARS else []
|
||||
@@ -85,10 +89,14 @@ def main() -> int:
|
||||
string_candidates = [
|
||||
key
|
||||
for key, value in response.items()
|
||||
if key not in SKIP_KEYS and isinstance(value, str) and len(value) >= MIN_CHARS
|
||||
if key not in SKIP_KEYS
|
||||
and isinstance(value, str)
|
||||
and len(value) >= MIN_CHARS
|
||||
]
|
||||
array_candidates = [
|
||||
key for key, value in response.items() if key not in SKIP_KEYS and is_string_array(value)
|
||||
key
|
||||
for key, value in response.items()
|
||||
if key not in SKIP_KEYS and is_string_array(value)
|
||||
]
|
||||
else:
|
||||
string_candidates = []
|
||||
|
||||
@@ -4,6 +4,6 @@
|
||||
"github_fulltag": true,
|
||||
"slug": "claude_desktop",
|
||||
"paused": false,
|
||||
"upstream_version": "v3.2.3+claude1.40609.1",
|
||||
"last_update": "2026-09-05"
|
||||
"upstream_version": "v3.2.4+claude1.52386.3",
|
||||
"last_update": "2026-09-12"
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ Integrations supported:
|
||||
## Configuration
|
||||
|
||||
| Option | Description |
|
||||
|--------|-------------|
|
||||
| -------- | ------------- |
|
||||
| `TZ` | Timezone (e.g. `Europe/Paris`). Defaults to `Europe/London`. |
|
||||
| `PUID` | User ID to run the process as. Defaults to `0` (root). |
|
||||
| `PGID` | Group ID to run the process as. Defaults to `0` (root). |
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
arch:
|
||||
- aarch64
|
||||
- amd64
|
||||
description:
|
||||
Automatically removes stuck and unwanted downloads from your *arr and download clients
|
||||
description: Automatically removes stuck and unwanted downloads from your *arr and download clients
|
||||
devices:
|
||||
- /dev/dri
|
||||
- /dev/dri/card0
|
||||
|
||||
@@ -33,7 +33,7 @@ mkdir -p "$HA_DATA_DIR"
|
||||
# Symlink /app/config → HA persistent storage
|
||||
# The .NET app uses /app/config, NOT /config at the filesystem root
|
||||
if [ -d /app/config ] && [ ! -L /app/config ]; then
|
||||
cp -rn /app/config/. "$HA_DATA_DIR/" 2>/dev/null || true
|
||||
cp -rn /app/config/. "$HA_DATA_DIR/" 2> /dev/null || true
|
||||
rm -rf /app/config
|
||||
fi
|
||||
ln -sfn "$HA_DATA_DIR" /app/config
|
||||
|
||||
@@ -40,7 +40,7 @@ Configurations can be done through the app webUI, except for the following optio
|
||||
### Options
|
||||
|
||||
| Option | Type | Default | Description |
|
||||
|--------|------|---------|-------------|
|
||||
| -------- | ------ | --------- | ------------- |
|
||||
| `CUSTOM_OPTIONS` | str | | Custom CLI options (e.g., `--name Homeassistant`) |
|
||||
| `DROPBOX_TOKEN` | str | | Dropbox integration token (see https://cloudcmd.io/) |
|
||||
| `localdisks` | str | | Local drives to mount (e.g., `sda1,sdb1,MYNAS`) |
|
||||
|
||||
@@ -103,7 +103,7 @@ for candidate in "${cloudcmd_candidates[@]}"; do
|
||||
fi
|
||||
done
|
||||
|
||||
if [ -z "$CLOUDCMD_BIN" ] && command -v cloudcmd >/dev/null 2>&1; then
|
||||
if [ -z "$CLOUDCMD_BIN" ] && command -v cloudcmd > /dev/null 2>&1; then
|
||||
CLOUDCMD_BIN=$(command -v cloudcmd)
|
||||
fi
|
||||
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
|
||||
## 2.3.0 (2026-09-12)
|
||||
- Update to latest version from ajslater/codex (changelog : https://github.com/ajslater/codex/releases)
|
||||
- Migrate legacy add-on configuration map names to current app configuration terminology.
|
||||
|
||||
## 2.2.11 (2026-08-29)
|
||||
|
||||
@@ -62,7 +62,7 @@ You can place the user folder from the theme/skeleton in /share/codex/www/user,
|
||||
## Options
|
||||
|
||||
| Option | Description | Default | Example |
|
||||
|--------|-------------|---------|---------|
|
||||
| -------- | ------------- | --------- | --------- |
|
||||
| `PGID` | Group ID for file permissions | `0` | `1000` |
|
||||
| `PUID` | User ID for file permissions | `0` | `1000` |
|
||||
| `TZ` | Timezone in long format | - | `America/Los_Angeles` |
|
||||
|
||||
@@ -101,4 +101,4 @@ schema:
|
||||
slug: codex
|
||||
udev: true
|
||||
url: https://github.com/alexbelgium/hassio-addons
|
||||
version: "2.2.11"
|
||||
version: "2.3.0"
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
{
|
||||
"github_beta": "true",
|
||||
"last_update": "2026-08-29",
|
||||
"last_update": "2026-09-12",
|
||||
"repository": "alexbelgium/hassio-addons",
|
||||
"slug": "codex",
|
||||
"source": "github",
|
||||
"upstream_repo": "ajslater/codex",
|
||||
"upstream_version": "2.2.11"
|
||||
"upstream_version": "2.3.0"
|
||||
}
|
||||
|
||||
@@ -51,7 +51,7 @@ Webui can be found at `https://homeassistant:9980/browser/dist/admin/admin.html`
|
||||
### Options
|
||||
|
||||
| Option | Type | Default | Description |
|
||||
|--------|------|---------|-------------|
|
||||
| -------- | ------ | --------- | ------------- |
|
||||
| `aliasgroup1` | str | | External address of the **Nextcloud** server allowed to use this Collabora (e.g. `https://nextcloud_domain\.com:443`) |
|
||||
| `aliasgroup2` | str | | A second Nextcloud server, same format as `aliasgroup1` |
|
||||
| `aliasgroup3` | str | | A third Nextcloud server, same format as `aliasgroup1` |
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user